A “composição sobre herança” está violando o “princípio seco”?

36

Por exemplo, considere que tenho uma classe para outras classes estender:

public class LoginPage {
    public String userId;
    public String session;
    public boolean checkSessionValid() {
    }
}

e algumas subclasses:

public class HomePage extends LoginPage {

}

public class EditInfoPage extends LoginPage {

}

De fato, a subclasse não possui métodos para substituir, também não acessaria a HomePage de maneira genérica, ou seja: Eu não faria algo como:

for (int i = 0; i < loginPages.length; i++) {
    loginPages[i].doSomething();
}

Eu só quero reutilizar a página de login. Mas de acordo com https://stackoverflow.com/a/53354 , eu devo preferir composição aqui porque não preciso da interface LoginPage, portanto não uso herança aqui:

public class HomePage {
    public LoginPage loginPage;
}

public class EditInfoPage {
    public LoginPage loginPage;
}

mas o problema vem aqui, na nova versão, o código:

public LoginPage loginPage;

duplica quando uma nova classe é adicionada. E se o LoginPage precisar de setter e getter, mais códigos deverão ser copiados:

public LoginPage loginPage;

private LoginPage getLoginPage() {
    return this.loginPage;
}
private void setLoginPage(LoginPage loginPage) {
    this.loginPage = loginPage;
}

Portanto, minha pergunta é: "composição sobre herança" está violando "princípio seco"?

ocomfd
fonte
13
Às vezes você não precisa de herança nem composição, ou seja, às vezes uma classe com poucos objetos faz o trabalho.
Erik Eidt 12/02
23
Por que você deseja que todas as suas páginas sejam a página de login? Talvez apenas tenha uma página de login.
Sr. Cochese
29
Mas, com herança, você duplicou em extends LoginPagetodo o lugar. VERIFICAR MATE!
el.pescado
2
Se você tiver muito o problema no último snippet, poderá estar usando demais getters e setters. Eles tendem a violar o encapsulamento.
Brian McCutchon
4
Portanto, faça com que sua página seja decorada e faça com que seja LoginPageum decorador. Chega de duplicação, apenas um simples page = new LoginPage(new EditInfoPage())e pronto. Ou você usa o princípio aberto-fechado para criar o módulo de autenticação que pode ser adicionado a qualquer página dinamicamente. Existem várias maneiras de lidar com a duplicação de código, principalmente envolvendo encontrar uma nova abstração. LoginPageé provavelmente um nome ruim, o que você deseja garantir é que o usuário seja autenticado enquanto navega nessa página, sendo redirecionado para a LoginPagemensagem de erro adequada ou mostrada uma mensagem de erro adequada.
Polygnome

Respostas:

46

Er espere, você está preocupado que repetir

public LoginPage loginPage;

em dois lugares viola SECO? Por essa lógica

int x;

agora só pode existir em um objeto em toda a base de código. Bleh.

SECA é uma coisa boa a ter em mente, mas vamos lá. Além de

... extends LoginPage

está sendo duplicado em sua alternativa, mesmo sendo anal sobre DRY não fará sentido.

Preocupações DRY válidas tendem a se concentrar no comportamento idêntico necessário em vários locais, sendo definido em vários locais, de modo que a necessidade de alterar esse comportamento acaba exigindo uma alteração em vários locais. Tome decisões em um só lugar e você só precisará alterá-las em um só lugar. Isso não significa que apenas um objeto possa manter uma referência à sua LoginPage.

DRY não deve ser seguido cegamente. Se você está duplicando porque copiar e colar é mais fácil do que pensar em um bom método ou nome de classe, provavelmente está errado.

Mas se você deseja colocar o mesmo código em um local diferente, porque esse local diferente está sujeito a uma responsabilidade diferente e provavelmente precisará mudar de forma independente, provavelmente é aconselhável relaxar a aplicação do DRY e deixar esse comportamento idêntico ter uma identidade diferente . É o mesmo tipo de pensamento que proíbe os números mágicos.

DRY não é apenas sobre a aparência do código. Trata-se de não espalhar os detalhes de uma idéia com repetição irracional, forçando os mantenedores a consertar as coisas usando repetição irracional. É quando você tenta dizer a si mesmo que a repetição irracional é apenas sua convenção de que as coisas estão indo de uma maneira muito ruim.

O que eu acho que você realmente está tentando reclamar é chamado código clichê. Sim, usar composição em vez de herança exige código padrão. Nada é exposto de graça, você precisa escrever um código que o exponha. Com esse clichê, vem a flexibilidade do estado, a capacidade de restringir a interface exposta, de dar nomes diferentes às coisas que são apropriados para seu nível de abstração, bom e indireto, e você está usando o que é composto de fora, não o por dentro, então você está enfrentando sua interface normal.

Mas sim, é muita digitação extra no teclado. Desde que eu possa impedir o problema do ioiô de subir e descer uma pilha de herança enquanto eu leio o código, vale a pena.

Agora não é que eu me recuso a usar herança. Um dos meus usos favoritos é dar novos nomes às exceções:

public class MyLoginPageWasNull extends NullPointerException{}
candied_orange
fonte
int x;pode não mais do que zero vezes existem em uma base de código
K. Alan Bates
@ K.AlanBates com licença ?
Candied_orange 27/0218
... eu sabia que você ia responder com a classe Point lol. Ninguém se importa com a classe Point. Se eu entrar na sua base de código e ver int a; int b; int x;, pressionarei para remover 'você'.
27718 Alan K. Bates
@ K.AlanBates Wow. é triste que você pense que esse tipo de comportamento faria de você um bom programador. O melhor codificador não é aquele que pode encontrar o máximo de coisas sobre os outros. É aquele que melhora o resto.
27218 Candied_orange
O uso de nomes sem sentido não é obrigatório e provavelmente torna o desenvolvedor desse código um passivo e não um ativo.
27318 Alan K. Bates
127

Um mal-entendido comum com o princípio DRY é que, de alguma forma, está relacionado à não repetição de linhas de código. O princípio DRY é "Todo conhecimento deve ter uma representação única, inequívoca e autorizada dentro de um sistema" . É sobre conhecimento, não código.

LoginPagesabe como desenhar a página para efetuar login. Se EditInfoPagesoubesse fazer isso, isso seria uma violação. A inclusão LoginPagevia composição não é de forma alguma uma violação do princípio DRY.

O princípio DRY é talvez o princípio mais mal utilizado na engenharia de software e deve sempre ser pensado não como um princípio para não duplicar o código, mas como um princípio para não duplicar o conhecimento abstrato do domínio. Na verdade, em muitos casos, se você aplicar o DRY corretamente, duplicará o código , e isso não é necessariamente uma coisa ruim.

wasatz
fonte
27
O "princípio de responsabilidade única" é muito mais mal utilizado. "Não repetir-se" pode facilmente entrar como número dois :-)
gnasher729
28
"SECO" é um acrônimo útil para "Não se repita", que é um slogan, mas não o nome do princípio, o nome real do princípio é "Uma vez e apenas uma vez", que por sua vez é um nome cativante por um princípio que leva alguns parágrafos para descrever. O importante é entender o par de parágrafos, não memorizando as três letras D, R e Y.
Jörg W Mittag
4
@ gnasher729 Você sabe, eu realmente concordo com você que o SRP provavelmente é mal usado muito mais. Embora seja justo, em muitos casos, acho que muitas vezes são mal utilizados juntos. Minha teoria é que os programadores em geral não devem confiar em usar siglas com "nomes fáceis".
wasatz
17
IMHO esta é uma boa resposta. No entanto, para ser justo: para minha experiência, a forma mais frequente de violação do DRY é causada pela programação de copiar e colar e apenas duplicar o código. Alguém me disse uma vez "sempre que você copiar e colar algum código, pense duas vezes se a parte duplicada não puder ser extraída em uma função comum" - que foi um ótimo conselho para o IMHO.
Doc Brown
9
O abuso de copiar e colar é certamente uma força motriz para violar o DRY, mas simplesmente proibir o copiar e colar é uma má orientação para seguir o DRY. Eu não sentiria nenhum remorso por ter dois métodos com código idêntico quando esses métodos representam partes diferentes de conhecimento. Eles servem duas responsabilidades diferentes. Eles simplesmente têm código idêntico hoje. Eles devem estar livres para mudar de forma independente. Sim, conhecimento, não código. Bem colocado. Eu escrevi uma das outras respostas, mas vou me curvar a essa. +1.
Candied_orange
12

Resposta curta: sim, sim - em um grau menor e aceitável.

À primeira vista, às vezes a herança pode economizar algumas linhas de código, pois tem o efeito de dizer "minha classe de reutilização conterá todos os métodos e atributos públicos de maneira 1: 1". Portanto, se houver uma lista de 10 métodos em um componente, não será necessário repeti-los no código da classe herdada. Quando, em um cenário de composição, 9 desses 10 métodos devem ser expostos publicamente por meio de um componente de reutilização, é necessário anotar 9 chamadas de delegação e deixar a restante fora, não há maneira de contornar isso.

Por que isso é tolerável? Observe os métodos que são replicados em um cenário de composição - esses métodos delegam exclusivamente chamadas à interface do componente, portanto, sem lógica real.

O coração do princípio DRY é evitar ter dois lugares no código em que as mesmas regras lógicas são codificadas - porque quando essas regras lógicas mudam, no código não-DRY, é fácil adaptar um desses locais e esquecer o outro, o que introduz um erro.

Porém, como delegar chamadas não contém lógica, elas normalmente não estão sujeitas a essa alteração, portanto não causam um problema real ao "preferir composição a herança". E mesmo que a interface do componente seja alterada, o que pode induzir mudanças formais em todas as classes usando o componente, em uma linguagem compilada, o compilador nos dirá quando esquecemos de alterar um dos chamadores.

Uma observação para o seu exemplo: não sei como você HomePagee sua EditInfoPageaparência, mas se eles tiverem funcionalidade de login e um HomePage(ou um EditInfoPage) for um LoginPage , a herança poderá ser a ferramenta correta aqui. Um exemplo menos discutível, em que a composição será a melhor ferramenta de uma maneira mais óbvia, provavelmente tornaria as coisas mais claras.

Supondo que HomePagenem EditInfoPageseja um LoginPage, e se deseja reutilizá-lo, como você escreveu, é provável que exija apenas algumas partes do LoginPage, não tudo. Se for esse o caso, uma abordagem melhor do que usar a composição da maneira mostrada pode ser

  • extrair a parte reutilizável LoginPageem um componente próprio

  • reutilize esse componente HomePagee EditInfoPageda mesma maneira que é usado agora dentroLoginPage

Dessa forma, normalmente ficará muito mais claro por que e quando a composição sobre herança é a abordagem correta.

Doc Brown
fonte