Muitas classes pequenas versus herança lógica (mas) complexa

24

Gostaria de saber o que é melhor em termos de bom design de OOP, código limpo, flexibilidade e evitar odores de código no futuro. Situação da imagem, na qual você tem muitos objetos muito semelhantes que precisa representar como classes. Essas classes não possuem nenhuma funcionalidade específica, apenas classes de dados e são diferentes apenas pelo nome (e contexto) Exemplo:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

Seria melhor mantê-los em classes separadas, para obter legibilidade "melhor", mas com muitas repetições de código, ou seria melhor usar a herança para ser mais SECA?

Ao usar a herança, você terá algo parecido com isto, mas os nomes de classe perderão o significado contextual (por causa do is-a, não has-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

Sei que a herança não é e não deve ser usada para a reutilização de código, mas não vejo outra maneira possível de reduzir a repetição de código nesse caso.

user969153
fonte

Respostas:

58

A regra geral diz "Preferir delegação sobre herança", não "evitar toda herança". Se os objetos tiverem um relacionamento lógico e um B puder ser usado sempre que um A for esperado, é uma boa prática usar a herança. No entanto, se os objetos tiverem campos com o mesmo nome e não tiverem relação de domínio, não use herança.

Com frequência, vale a pena fazer a pergunta "razão da mudança" no processo de design: se a classe A obtiver um campo extra, você esperaria que a classe B também obtivesse? Se isso for verdade, os objetos compartilham um relacionamento, e herança é uma boa idéia. Caso contrário, sofra a pequena repetição para manter conceitos distintos em código distinto.

thiton
fonte
Claro, a razão da mudança é um bom argumento, mas em situações em que essas classes são e sempre serão constantes?
user969153
20
@ user969153: Quando as aulas serão realmente constantes, isso não importa. Toda boa engenharia de software é para o futuro mantenedor, que precisa fazer alterações. Se não houver mudanças futuras, tudo correrá bem, mas confie em mim, você sempre terá alterações, a menos que programe para a lixeira.
thiton
@ user969153: Dito isto, a "razão da mudança" é uma heurística. Ajuda você a decidir, nada mais.
thiton
2
Boa resposta. Razões para mudar é S no acrônimo SOLID do tio bob, que ajudará na criação de um bom software.
Klee
4
@ user969153, Na minha experiência, "onde essas classes estão e sempre serão constantes?" não é um caso de uso válido :) Ainda estou trabalhando em um aplicativo de tamanho significativo que não sofreu alterações totalmente inesperadas.
cdkMoose
18

Ao usar a herança, você terá algo parecido com isto, mas os nomes de classe perderão o significado contextual (por causa do is-a, não has-a):

Exatamente. Veja isso, fora de contexto:

Class D : C
{
    String age;
}

Quais propriedades um D tem? Você consegue se lembrar? E se você complicar ainda mais a hierarquia:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

E se, horror após horrores, você deseja adicionar um campo imageUrl a F, mas não a E? Você não pode derivar de C e E.

Eu vi uma situação real em que muitos tipos diferentes de componentes tinham nome, ID e descrição. Mas isso não era da natureza de serem componentes, era apenas uma coincidência. Cada um tinha um ID porque eles foram armazenados em um banco de dados. O nome e a descrição foram exibidos.

Os produtos também tinham um ID, Nome e Descrição, por motivos semelhantes. Mas eles não eram componentes, nem eram produtos de componentes. Você nunca veria as duas coisas juntas.

Mas, alguns desenvolvedores leram sobre o DRY e decidiram que ele removeria toda sugestão de duplicação do código. Então, ele chamou tudo de produto e acabou com a necessidade de definir esses campos. Eles foram definidos em Produto e tudo que precisava desses campos poderia derivar de Produto.

Não posso dizer isso com força suficiente: essa não é uma boa ideia.

DRY não se trata de remover a necessidade de propriedades comuns. Se um objeto não for um Produto, não o chame de Produto. Se um Produto não EXIGIR uma Descrição, pela própria natureza de ser um Produto, não defina Descrição como uma Propriedade do Produto.

Por fim, aconteceu que tínhamos Componentes sem descrições. Então começamos a ter descrições nulas nesses objetos. Não anulável. Nulo. Sempre. Para qualquer produto do tipo X ... ou posterior Y ... e N.

Esta é uma violação flagrante do princípio da substituição de Liskov.

DRY é sobre nunca se colocar em uma posição em que você pode corrigir um bug em um lugar e esquecer de fazê-lo em outro lugar. Isso não vai acontecer porque você tem dois objetos com a mesma propriedade. Nunca. Então não se preocupe.

Se o contexto das classes torna óbvio que você deveria, o que será muito raro, então e somente então você deve considerar uma classe pai comum. Mas, se for propriedades, considere por que você não está usando uma interface.

pdr
fonte
4

A herança é uma maneira de obter um comportamento polimórfico. Se suas classes não têm nenhum comportamento, a herança é inútil. Nesse caso, eu nem os consideraria objetos / classes, mas estruturas.

Se você quiser colocar estruturas diferentes em diferentes partes do seu comportamento, considere interfaces com métodos ou propriedades get / set, como IHasName, IHasAge, etc. Isso tornará o design muito mais limpo e permitirá uma melhor composição destes. tipo de hiearchies.

Eufórico
fonte
3

Não é para isso que servem as interfaces? Classes não relacionadas com funções diferentes, mas com uma propriedade semelhante.

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;
Pieter B
fonte
1

Sua pergunta parece estar enquadrada no contexto de um idioma que não suporta herança múltipla, mas não especifica isso especificamente. Se você estiver trabalhando com um idioma que suporta herança múltipla, isso se tornará trivialmente fácil de resolver com apenas um único nível de herança.

Aqui está um exemplo de como isso pode ser resolvido usando herança múltipla.

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
pgraham
fonte