Encadeamento de método vs encapsulamento

17

Há o problema clássico de POO do encadeamento de métodos versus métodos de "ponto de acesso único":

main.getA().getB().getC().transmogrify(x, y)

vs

main.getA().transmogrifyMyC(x, y)

A primeira parece ter a vantagem de que cada classe é responsável apenas por um conjunto menor de operações e torna tudo muito mais modular - adicionar um método a C não requer nenhum esforço em A, B ou C para expô-lo.

A desvantagem, é claro, é o encapsulamento mais fraco , que o segundo código resolve. Agora, A tem controle de todos os métodos que passam por ele e pode delegá-lo a seus campos, se desejar.

Percebo que não há uma solução única e, é claro, depende do contexto, mas eu realmente gostaria de ouvir algumas opiniões sobre outras diferenças importantes entre os dois estilos e sob quais circunstâncias devo preferir um deles - porque agora, quando tento Para projetar algum código, sinto que não estou usando os argumentos para decidir de uma maneira ou de outra.

Carvalho
fonte

Respostas:

25

Eu acho que a Lei de Demeter fornece uma orientação importante nisso (com suas vantagens e desvantagens, que, como sempre, devem ser medidas em uma base por caso).

A vantagem de seguir a Lei de Demeter é que o software resultante tende a ser mais sustentável e adaptável. Como os objetos são menos dependentes da estrutura interna de outros objetos, os contêineres de objetos podem ser alterados sem retrabalhar seus chamadores.

Uma desvantagem da Lei de Demeter é que, às vezes, é necessário escrever um grande número de pequenos métodos "wrapper" para propagar chamadas de método para os componentes. Além disso, a interface de uma classe pode se tornar volumosa, pois hospeda métodos para classes contidas, resultando em uma classe sem uma interface coesa. Mas isso também pode ser um sinal de mau design de OO.

Péter Török
fonte
Esqueci essa lei, obrigado por me lembrar. Mas o que estou perguntando aqui é principalmente quais são as vantagens e desvantagens, ou mais precisamente, como devo decidir usar um estilo em detrimento do outro.
Oak
@ Oak, eu adicionei citações descrevendo as vantagens e desvantagens.
Péter Török
10

Em geral, tento manter o método encadeado o mais limitado possível (com base na Lei de Demeter )

A única exceção que faço é para interfaces fluentes / programação interna no estilo DSL.

Martin Fowler faz a mesma distinção em Idiomas Específicos de Domínio, mas por razões de violação da separação de consultas de comando, que afirma:

que todo método deve ser um comando que executa uma ação ou uma consulta que retorna dados ao chamador, mas não os dois.

Fowler em seu livro na página 70 diz:

A separação de comandos e consultas é um princípio extremamente valioso na programação, e eu incentivo fortemente as equipes a usá-las. Uma das conseqüências do uso de Method Chaining em DSLs internas é que ele geralmente quebra esse princípio - cada método altera o estado, mas retorna um objeto para continuar a cadeia. Eu usei muitos decibéis menosprezando as pessoas que não seguem a separação entre comando e consulta e o farão novamente. Mas as interfaces fluentes seguem um conjunto diferente de regras, por isso estou feliz em permitir isso aqui.

KeesDijk
fonte
3

Penso que a questão é se você está usando uma abstração adequada.

No primeiro caso, temos

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Onde principal é do tipo IHasGetA. A questão é: essa abstração é adequada. A resposta não é trivial. E, neste caso, parece um pouco estranho, mas é um exemplo teórico de qualquer maneira. Mas para construir um exemplo diferente:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

Geralmente é melhor que

main.superTransmogrify(v, w, x, y, z);

Porque no último exemplo ambos thise maindependem dos tipos de v, w, x, ye z. Além disso, o código realmente não parece muito melhor, se toda declaração de método tiver meia dúzia de argumentos.

Um localizador de serviço realmente requer a primeira abordagem. Você não deseja acessar a instância criada por meio do localizador de serviço.

Portanto, "alcançar" através de um objeto pode criar muita dependência, ainda mais se for baseado nas propriedades das classes reais.
No entanto, criar uma abstração, que consiste em fornecer um objeto, é uma coisa totalmente diferente.

Por exemplo, você poderia ter:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

Onde mainé uma instância de Main. Se o conhecimento da classe mainreduz a dependência para, em IHasGetAvez de Main, você encontrará o acoplamento realmente muito baixo. O código de chamada nem sabe que está realmente chamando o último método no objeto original, o que ilustra o grau de dissociação.
Você alcança um caminho de abstrações concisas e ortogonais, em vez de se aprofundar nas partes internas de uma implementação.

back2dos
fonte
Ponto muito interessante sobre o grande aumento no número de parâmetros.
Oak
2

A Lei de Demeter , como aponta Péter Török, sugere a forma "compacta".

Além disso, quanto mais métodos você mencionar explicitamente em seu código, mais classes elas dependem, aumentando os problemas de manutenção. No seu exemplo, o formato compacto depende de duas classes, enquanto o formato mais longo depende de quatro classes. A forma mais longa não só viola a Lei de Demeter; isso também fará com que você altere seu código toda vez que alterar qualquer um dos quatro métodos referenciados (em oposição a dois no formato compacto).

CesarGon
fonte
Por outro lado, seguir cegamente essa lei significa que o número de métodos Aexplodirá com muitos métodos que Apodem delegar de qualquer maneira. Ainda assim, eu concordo com as dependências - isso reduz drasticamente a quantidade de dependências necessárias do código do cliente.
Oak
1
@ Oak: Fazer qualquer coisa às cegas nunca é bom. É preciso considerar prós e contras e tomar decisões com base em evidências. Isso inclui a Lei de Deméter também.
CesarGon
2

Eu mesmo lutei com esse problema. A desvantagem de "alcançar" profundamente objetos diferentes é que, quando você refatorar, terá que alterar uma enorme quantidade de código, pois existem muitas dependências. Além disso, seu código se torna um pouco inchado e mais difícil de ler.

Por outro lado, ter classes que simplesmente "transmitem" métodos também significa uma sobrecarga de ter que declarar vários métodos em mais de um local.

Uma solução que atenua isso e é apropriada em alguns casos é ter uma classe de fábrica que cria um tipo de objeto de fachada, copiando dados / objetos das classes apropriadas. Dessa forma, você pode codificar no objeto de fachada e, quando refatorar, simplesmente altera a lógica da fábrica.

Homde
fonte
1

Costumo achar que é mais fácil entender a lógica de um programa com métodos encadeados. Para mim, customer.getLastInvoice().itemCount()se encaixa melhor no meu cérebro customer.countLastInvoiceItems().

Vale a pena a dor de cabeça na manutenção de ter o acoplamento extra. (Também gosto de funções pequenas em turmas pequenas, por isso costumo encadear. Não estou dizendo que está certo - é exatamente o que faço.)

JT Grimes
fonte
deve ser customer.NrLastInvoices ou customer.LastInvoice.NrItems da IMO. Essa corrente não é muito longo, então não é provavelmente vale achatamento se a quantidade de combinações são um pouco grande
Homde