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.