Sofro de uso excessivo de encapsulamento?


11

Percebi algo no meu código em vários projetos que me parece um cheiro de código e algo ruim para fazer, mas não consigo lidar com isso.

Ao tentar escrever "código limpo", costumo usar métodos privados em excesso para facilitar a leitura do código. O problema é que o código é realmente mais limpo, mas também é mais difícil de testar (sim, eu sei que posso testar métodos particulares ...) e, em geral, parece um mau hábito para mim.

Aqui está um exemplo de uma classe que lê alguns dados de um arquivo .csv e retorna um grupo de clientes (outro objeto com vários campos e atributos).

public class GroupOfCustomersImporter {
    //... Call fields ....
    public GroupOfCustomersImporter(String filePath) {
        this.filePath = filePath;
        customers = new HashSet<Customer>();
        createCSVReader();
        read();
        constructTTRP_Instance();
    }

    private void createCSVReader() {
        //....
    }

    private void read() {
        //.... Reades the file and initializes the class attributes
    }

    private void readFirstLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readSecondLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readCustomerLine(String[] inputLine) { 
        //.... Method used by the read() method
    }

    private void constructGroupOfCustomers() {
        //this.groupOfCustomers = new GroupOfCustomers(**attributes of the class**);
    }

    public GroupOfCustomers getConstructedGroupOfCustomers() {
        return this.GroupOfCustomers;
    }
}

Como você pode ver, a classe tem apenas um construtor que chama alguns métodos particulares para realizar o trabalho, sei que isso não é uma boa prática em geral, mas prefiro encapsular toda a funcionalidade da classe em vez de tornar os métodos públicos. um cliente deve trabalhar desta maneira:

GroupOfCustomersImporter importer = new GroupOfCustomersImporter(filepath)
importer.createCSVReader();
read();
GroupOfCustomer group = constructGoupOfCustomerInstance();

Prefiro isso porque não quero colocar linhas de código inúteis no código do lado do cliente, incomodando a classe cliente com detalhes de implementação.

Então, isso é realmente um mau hábito? Se sim, como posso evitá-lo? Observe que o acima é apenas um exemplo simples. Imagine a mesma situação acontecendo em algo um pouco mais complexo.

Respostas:


17

Eu acho que você está realmente no caminho certo, tanto quanto quer esconder detalhes de implementação do cliente. Você deseja projetar sua classe para que a interface que o cliente veja seja a API mais simples e concisa possível. Não apenas o cliente "não será incomodado" com os detalhes da implementação, como também estará livre para refatorar o código subjacente sem se preocupar que os chamadores desse código sejam modificados. Reduzir o acoplamento entre diferentes módulos traz algum benefício real e você deve definitivamente se esforçar para isso.

Portanto, se você seguir os conselhos que acabei de fornecer, você terminará com algo que já notou em seu código, um monte de lógica que permanece oculta por trás de uma interface pública e não é facilmente acessível.

Idealmente, você deve poder testar sua classe unicamente com relação à interface pública e, se houver dependências externas, poderá precisar introduzir objetos falsos / simulados / stub para isolar o código que está sendo testado.

No entanto, se você fizer isso e ainda achar que não pode testar facilmente todas as partes da turma, é muito provável que sua turma esteja fazendo muito. No espírito do princípio SRP , você pode seguir o que Michael Feathers chama de "Padrão de classe de brotamento" e extrair um pedaço da sua classe original para uma nova classe.

No seu exemplo, você tem uma classe importadora que também é responsável pela leitura de um arquivo de texto. Uma de suas opções é extrair um pedaço do código de leitura de arquivo em uma classe InputFileParser separada. Agora todas essas funções privadas tornam-se públicas e, portanto, facilmente testáveis. Ao mesmo tempo, a classe do analisador não é algo visível para clientes externos (marque-a como "interna", não publique arquivos de cabeçalho ou simplesmente não os anuncie como parte da sua API), pois eles continuarão usando o original importador cuja interface continuará curta e doce.


1

Como alternativa para colocar muitas chamadas de método em seu construtor de classe - o que eu concordo é um hábito geralmente evitar - você pode criar um método de fábrica para lidar com todo o material extra de inicialização que você não deseja incomodar o cliente lado com. Isso significa que você estaria expondo um método para se parecer com o seu constructGroupOfCustomers()método como público e usando-o como um método estático da sua classe:

GroupOfCustomersImporter importer = 
    new GroupOfCustomersImporter.CreateInstance();

ou como método de uma classe de fábrica separada, talvez:

ImporterFactory factory = new ImporterFactory();
GroupOfCustomersImporter importer = factory.CreateGroupOfCustomersImporter();

Essas são as primeiras opções pensadas diretamente em cima da minha cabeça.

Também vale a pena considerar que seus instintos estão tentando lhe dizer uma coisa. Quando seu intestino diz que o código está começando a cheirar, provavelmente cheira e é melhor fazer algo antes que cheire! Certamente, na superfície, pode não haver nada intrinsecamente errado com seu código, mas uma rápida verificação e um refator ajudará a esclarecer seus pensamentos sobre o problema, para que você possa passar para outras coisas com confiança, sem se preocupar se estiver criando um código. potencial castelo de cartas. Nesse caso específico, delegar algumas das responsabilidades do construtor para - ou ser chamado por - uma fábrica garante que você possa exercer um maior grau de controle da instanciação da sua classe e de seus futuros descendentes em potencial.


1

Adicionando minha própria resposta, pois acabou demorando muito para ser comentado.

Você está absolutamente certo de que o encapsulamento e o teste são bem contrários - se você esconder quase tudo, é difícil testar.

No entanto, você pode tornar o exemplo mais testável fornecendo um fluxo em vez de nome de arquivo - dessa maneira, você apenas fornece um csv conhecido da memória ou um arquivo, se desejar. Isso também tornará sua classe mais robusta quando você precisar adicionar suporte http;)

Além disso, observe o uso do lazy-init:

public class Csv
{
  private CustomerList list;

  public CustomerList get()
  {
    if(list == null)
        load();
     return list;
  }
}

1

O construtor não deve fazer muito, exceto inicializar algumas variáveis ​​ou o estado do objeto. Fazer muito no construtor pode gerar exceções de tempo de execução. Eu tentaria evitar que o cliente fizesse algo assim:

MyClass a;
try {
   a = new MyClass();
} catch (MyException e) {
   //do something
}

Em vez de fazer:

MyClass a = new MyClass(); // Or a factory method
try {
   a.doSomething();
} catch (MyException e) {
   //do something
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.