Herança vs propriedade adicional com valor nulo

12

Para classes com campos opcionais, é melhor usar herança ou uma propriedade anulável? Considere este exemplo:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

ou

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

ou

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

O código, dependendo dessas três opções, seria:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

ou

if (book.getColor() != null) { book.getColor(); }

ou

if (book.getColor().isPresent()) { book.getColor(); }

A primeira abordagem me parece mais natural, mas talvez seja menos legível por causa da necessidade de fazer o casting. Existe alguma outra maneira de alcançar esse comportamento?

Bojan VukasovicTest
fonte
1
Receio que a solução se refira à sua definição de regras de negócios. Quero dizer, se todos os seus livros tiverem uma cor, mas a cor for opcional, a herança será um exagero e, na verdade, desnecessária, porque você deseja que todos os seus livros saibam que a propriedade da cor existe. Por outro lado, se você pode ter livros que não sabem nada sobre cores do que nunca e livros que têm cores, criar aulas especializadas não é ruim. Mesmo assim, eu provavelmente preferiria a composição à herança.
Andy
Para torná-lo um pouco mais específico - existem 2 tipos de livros - um com cores e outro sem. Portanto, nem todos os livros podem ter uma cor.
Bojan VukasovicTest
1
Se realmente houver um caso em que o livro não tenha cores, a herança é a maneira mais simples de estender as propriedades de um livro colorido. Nesse caso, não parece que você quebraria algo herdando a classe de livro base e adicionando uma propriedade de cor a ela. Você pode ler sobre o princípio de substituição liskov para saber mais sobre casos em que a extensão de uma classe é permitida e quando não é.
Andy
4
Você consegue identificar um comportamento que deseja nos livros com cores? Você consegue encontrar um comportamento comum entre os livros com e sem coloração, onde os livros com coloração devem se comportar de maneira um pouco diferente? Nesse caso, esse é um bom caso para OOP com tipos diferentes e, a idéia seria mover os comportamentos para as classes em vez de interrogar a presença e o valor da propriedade externamente.
Erik Eidt
1
Se as cores possíveis do livro forem conhecidas com antecedência, que tal um campo Enum para BookColor com uma opção para BookColor.NoColor?
Graham

Respostas:

7

Isso depende das circunstâncias. O exemplo específico não é realista, pois você não teria uma subclasse chamada BookWithColorem um programa real.

Mas, em geral, uma propriedade que só faz sentido para determinadas subclasses deve existir apenas nessas subclasses.

Por exemplo, se Booktem PhysicalBooke DigialBookcomo descendentes, PhysicalBookpode ter uma weightpropriedade e DigitalBookuma sizeInKbpropriedade. Mas DigitalBooknão terá weighte vice-versa. Booknão terá nenhuma propriedade, pois uma classe deve ter apenas propriedades compartilhadas por todos os descendentes.

Um exemplo melhor é olhar para as classes de software real. O JSlidercomponente tem o campo majorTickSpacing. Como apenas um controle deslizante tem "ticks", esse campo só faz sentido para JSlidere seus descendentes. Seria muito confuso se outros componentes irmãos JButtontivessem um majorTickSpacingcampo.

JacquesB
fonte
ou você pode reduzir o peso para refletir os dois, mas isso provavelmente é demais.
Walfrat
3
@ Walfrat: Seria uma péssima idéia ter o mesmo campo representando coisas completamente diferentes em diferentes subclasses.
JacquesB
E se um livro tiver edições físicas e digitais? Você precisaria criar uma classe diferente para cada permutação de ter ou não ter cada campo opcional. Eu acho que a melhor solução seria permitir que alguns campos fossem omitidos ou não, dependendo do livro. Você citou uma situação completamente diferente, na qual as duas classes se comportariam de forma funcional diferente. Não é isso que esta pergunta implica. Eles só precisam modelar uma estrutura de dados.
4castle
@ 4castle: Você precisaria de aulas separadas por edição, pois cada edição também pode ter preços diferentes. Você não pode resolver isso com campos opcionais. Mas não sabemos a partir do exemplo se a operação deseja um comportamento diferente para livros com cores ou "livros sem cores", portanto, é impossível saber qual é a modelagem correta nesse caso.
JacquesB
3
concordo com @JacquesB. você não pode fornecer uma solução universalmente correta para "modelar livros", porque depende de qual problema você está tentando resolver e qual é o seu negócio. Eu acho que é bastante seguro dizer que a definição de hierarquias de classe onde violam Liskov é categoricamente errado (tm) embora (sim, sim, o seu caso é provavelmente muito especial e garante (embora eu duvido))
sara
5

Um ponto importante que parece não ter sido mencionado: Na maioria dos idiomas, as instâncias de uma classe não podem alterar de qual classe elas são. Portanto, se você tivesse um livro sem uma cor e desejasse adicionar uma cor, seria necessário criar um novo objeto se estiver usando classes diferentes. E então você provavelmente precisará substituir todas as referências ao objeto antigo por referências ao novo objeto.

Se "livros sem cor" e "livros com cor" são instâncias da mesma classe, adicionar ou remover a cor será muito menos problemático. (Se a interface do usuário mostrar uma lista de "livros com cores" e "livros sem cores", a interface do usuário precisará ser alterada obviamente, mas seria de esperar que você precise lidar com isso de qualquer maneira, semelhante a uma "lista de cores vermelhas" livros "e" lista de livros verdes ").

gnasher729
fonte
Este é realmente um ponto importante! Ele define se deve-se considerar uma coleção de propriedades opcionais em vez de atributos de classe.
chromanoid
1

Pense em um JavaBean (como o seu Book) como um registro em um banco de dados. As colunas opcionais são nullquando não têm valor, mas é perfeitamente legal. Portanto, sua segunda opção:

class Book {
    private String name;
    private String color; // null when not applicable
}

É o mais razoável. 1

Tenha cuidado com o modo como você usa a Optionalclasse. Por exemplo, não é Serializable, o que geralmente é uma característica de um JavaBean. Aqui estão algumas dicas de Stephen Colebourne :

  1. Não declarar qualquer variável de instância do tipo Optional.
  2. Use nullpara indicar dados opcionais no escopo privado de uma classe.
  3. Use Optionalpara getters que acessam o campo opcional.
  4. Não use Optionalem setters ou construtores.
  5. Use Optionalcomo um tipo de retorno para qualquer outro método de lógica de negócios que tenha um resultado opcional.

Portanto, dentro da sua classe, você deve usar nullpara representar que o campo não está presente, mas quando color deixar o Book(como um tipo de retorno), ele deve ser envolvido com um Optional.

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

Isso fornece um design claro e um código mais legível.


1 Se você pretende para um BookWithColorpara encapsular uma "classe" inteira de livros que se especializaram capacidades sobre outros livros, então não faria sentido a herança uso.

4castle
fonte
1
Quem disse algo sobre um banco de dados? Se todos os padrões de OO precisassem se encaixar em um paradigma de banco de dados relacional, teríamos que alterar muitos padrões de OO.
Alexanderbird
Eu argumentaria que a adição de propriedades não utilizadas "apenas por precaução" é a opção menos clara. Como outros disseram, isso depende do caso de uso, mas a situação mais desejável seria não transportar propriedades em que um aplicativo externo precise saber se uma propriedade que existe está ou não sendo realmente usada.
Volker Kueffel
@ Volker Vamos concordar em discordar, porque posso citar várias pessoas que jogaram uma mão ao adicionar a Optionalclasse ao Java para esse fim exato. Pode não ser OO, mas certamente é uma opinião válida.
4castle
@ Alexanderbird Eu estava abordando especificamente um JavaBean, que deveria ser serializável. Se eu estava fora de tópico ao abrir o JavaBeans, esse foi meu erro.
4castle
@Alexanderbird Eu não espero que todos os padrões para corresponder a um banco de dados relacional, mas eu esperava um Java Bean para
4castle
0

Você deve usar uma interface (não herança) ou algum tipo de mecânico de propriedade (talvez até algo que funcione como a estrutura de descrição de recursos).

Então também

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

ou

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

Esclarecimento (editar):

Simplesmente adicionando campos opcionais na classe de entidade

Especialmente com o Wrapper opcional (editar: veja a resposta do 4castle ) Eu acho que isso (adicionar campos na entidade original) é uma maneira viável de adicionar novas propriedades em pequena escala. O maior problema com essa abordagem é que ela pode funcionar contra baixo acoplamento.

Imagine que sua classe de livros é definida em um projeto dedicado ao seu modelo de domínio. Agora você adiciona outro projeto que usa o modelo de domínio para executar uma tarefa especial. Esta tarefa requer uma propriedade adicional na classe book. Você acaba com herança (veja abaixo) ou precisa alterar o modelo de domínio comum para tornar possível a nova tarefa. Nesse último caso, você pode acabar com vários projetos que dependem de suas próprias propriedades adicionadas à classe book, enquanto a própria classe book de certa forma depende desses projetos, porque você não é capaz de entender a classe book sem o projetos adicionais.

Por que a herança é problemática quando se trata de uma maneira de fornecer propriedades adicionais?

Quando vejo sua classe de exemplo "Livro", penso em um objeto de domínio que muitas vezes acaba tendo muitos campos e subtipos opcionais. Imagine que você deseja adicionar uma propriedade para livros que incluem um CD. Existem agora quatro tipos de livros: livros, livros com cores, livros com CD, livros com cores e CD. Você não pode descrever essa situação com herança em Java.

Com interfaces, você contorna esse problema. Você pode facilmente compor as propriedades de uma classe de livro específica com interfaces. A delegação e a composição facilitarão a obtenção da turma desejada. Com a herança, você geralmente acaba com algumas propriedades opcionais na classe que estão lá apenas porque uma classe irmã precisa delas.

Leitura adicional por que a herança geralmente é uma ideia problemática:

Por que a herança geralmente é vista como algo ruim pelos proponentes da OOP

JavaWorld: Por que estender é mau

O problema com a implementação de um conjunto de interfaces para compor um conjunto de propriedades

Quando você usa interfaces para extensão, tudo fica bem, desde que você tenha apenas um pequeno conjunto delas. Especialmente quando seu modelo de objeto é usado e estendido por outros desenvolvedores, por exemplo, em sua empresa, a quantidade de interfaces aumentará. E, finalmente, você acaba criando uma nova "interface de propriedade" oficial que adiciona um método que seus colegas já usaram em seu projeto para o cliente X para um caso de uso completamente não relacionado - Ugh.

editar: Outro aspecto importante é mencionado por gnasher729 . Você geralmente deseja adicionar dinamicamente propriedades opcionais a um objeto existente. Com herança ou interfaces, você teria que recriar o objeto inteiro com outra classe que está longe de ser opcional.

Quando você espera extensões para o seu modelo de objeto a tal ponto, será melhor modelar explicitamente a possibilidade de extensão dinâmica . Eu proponho algo como acima, onde cada "extensão" (neste caso, propriedade) tem sua própria classe. A classe funciona como espaço para nome e identificador para a extensão. Quando você define as convenções de nomenclatura de pacotes dessa maneira, permite uma quantidade infinita de extensões sem poluir o espaço para nome dos métodos na classe de entidade original.

No desenvolvimento de jogos, você geralmente encontra situações em que deseja compor comportamentos e dados em diversas variações. É por isso que o padrão de arquitetura entidade-componente-sistema ficou bastante popular nos círculos de desenvolvimento de jogos. Essa também é uma abordagem interessante que você pode querer observar quando espera muitas extensões para o seu modelo de objeto.

cromanóide
fonte
E não abordou a questão "como decidir entre essas opções", é apenas outra opção. Por que isso é sempre melhor do que todas as opções apresentadas pelo OP?
Alexanderbird
Você está certo, eu vou melhorar a resposta.
Chromanoid #
@ 4castle Em interfaces Java não são herança! A implementação de interfaces é uma maneira de cumprir um contrato enquanto a herança de uma classe está basicamente estendendo seu comportamento. Você pode implementar várias interfaces enquanto não pode estender várias classes em uma classe. No meu exemplo, você estende as interfaces enquanto usa a herança para herdar o comportamento da entidade original.
chromanoid
Faz sentido. No seu exemplo, PropertiesHolderprovavelmente seria uma classe abstrata. Mas o que você sugere ser uma implementação getPropertyou getPropertyValue? Os dados ainda precisam ser armazenados em algum tipo de campo de instância em algum lugar. Ou você usaria um Collection<Property>para armazenar as propriedades em vez de usar campos?
4castle
1
Fiz a Bookextensão PropertiesHolderpor razões de clareza. A PropertiesHolderregra geral deve ser um membro em todas as classes que precisam dessa funcionalidade. A implementação conteria Map<Class<? extends Property<?>>,Property<?>>algo assim. Essa é a parte mais feia da implementação, mas fornece uma estrutura muito boa que até funciona com JAXB e JPA.
Chromanoid #
-1

Se Bookclasse é derivável sem propriedade de cor como abaixo

class E_Book extends Book{
   private String type;
}

então a herança é razoável.

adem caglin
fonte
Não sei se entendi o seu exemplo? Se colorfor privado, qualquer classe derivada não deverá se preocupar com isso color. Basta adicionar alguns construtores para Bookque ignorem colorcompletamente e o padrão será null.
4castle