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

36

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

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 {

}

Na verdade, a subclasse não tem nenhum método para substituir, também eu 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 link , eu deveria preferir composição aqui porque eu não preciso da interface LoginPage, então eu 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 precisam ser copiados:

public LoginPage loginPage;

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

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

    
por mmmaaa 12.02.2018 / 04:04
fonte

3 respostas

46

Er, espere que você esteja preocupado com a repetição

public LoginPage loginPage;

em dois lugares viola DRY? Por essa lógica

int x;

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

DRY é bom ter em mente, mas vamos lá. Além de

... extends LoginPage

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

As preocupações com DRY válidas tendem a se concentrar no comportamento idêntico necessário em vários locais que estão sendo definidos em vários lugares, de modo que a necessidade de alterar esse comportamento acaba exigindo uma alteração em vários lugares. 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 ao seu LoginPage.

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

Mas se você quiser colocar o mesmo código em um lugar diferente, porque esse lugar diferente está sujeito a uma responsabilidade diferente e é provável que precise mudar de forma independente, é aconselhável relaxar a aplicação de DRY e permitir que esse comportamento idêntico tenha uma identidade diferente. É o mesmo tipo de pensamento que entra em proibir números mágicos.

DRY não é apenas sobre o aspecto do código. Trata-se de não espalhar os detalhes de uma idéia por aí com a 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 sem sentido é apenas sua convenção de que as coisas estão indo de um jeito ruim.

O que eu acho que você está realmente tentando reclamar é chamado de código clichê. Sim, usar composição em vez de herança exige código clichê. Nada fica exposto de graça, você tem que escrever código que o expõe. Com esse clichê vem a flexibilidade do estado, a capacidade de estreitar a interface exposta, para dar nomes diferentes que são apropriados para o nível de abstração, boa direção, e você está usando o que você é composto de fora, não o dentro, então você está enfrentando a interface normal.

Mas sim, é muito mais digitação do teclado. Contanto que eu possa evitar o problema do yo-yo de saltar para cima e para baixo em uma pilha de herança enquanto eu leio o código vale a pena.

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

public class MyLoginPageWasNull extends NullPointerException{}
    
por 12.02.2018 / 07:04
fonte
128

Um mal-entendido comum com o princípio DRY é que ele está de alguma forma relacionado a não repetir linhas de código. O princípio DRY é "Cada conhecimento deve ter uma representação única, não ambígua e autoritativa dentro de um sistema" . É sobre conhecimento, não código.

LoginPage sabe como desenhar a página para efetuar login. Se EditInfoPage souber como fazer isso, isso seria uma violação. Incluir LoginPage via composição é não de qualquer forma 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 DRY corretamente, você estará duplicando o código , e isso não é necessariamente uma coisa ruim.

    
por 12.02.2018 / 08:22
fonte
12

Resposta curta: sim, faz - com algum grau menor e aceitável.

À primeira vista, a herança às vezes 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 é 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 o restante disponível, não há como evitar isso.

Por que isso é tolerável? Veja os métodos que são replicados em um cenário de composição - esses métodos são exclusivamente delegando chamadas para a interface do componente, não contendo, portanto, nenhuma lógica real.

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

Mas, como a delegação de chamadas não contém lógica, elas normalmente não são sujeitas a essa alteração, portanto, não causam problemas reais quando "preferem composição sobre herança". E mesmo que a interface do componente mude, o que pode induzir mudanças formais em todas as classes usando o componente, em uma linguagem compilada, o compilador nos dirá quando nos esquecermos de alterar um dos chamadores.

Uma observação ao seu exemplo: não sei como seu HomePage e seu EditInfoPage se parecem, mas se eles têm funcionalidade de login e um HomePage (ou um EditInfoPage ) é um LoginPage , então a herança pode ser a ferramenta correta aqui. Um exemplo menos discutível, onde a composição será a melhor ferramenta de uma maneira mais óbvia, provavelmente tornaria as coisas mais claras.

Assumindo que nem HomePage nem EditInfoPage são LoginPage , e um deseja reutilizá-lo, como você escreveu, então é muito provável que apenas algumas partes do LoginPage sejam necessárias, não tudo. Se for esse o caso, uma abordagem melhor do que usar a composição da maneira mostrada pode ser

  • extraia a parte reutilizável de LoginPage em um componente próprio

  • reutilize esse componente em HomePage e EditInfoPage da mesma forma que é usado agora em LoginPage

Dessa forma, normalmente fica mais claro porque e quando a composição sobre herança é a abordagem correta.

    
por 12.02.2018 / 07:30
fonte