Herencia versus propiedad adicional con valor nulo

12

Para las clases con campos opcionales, ¿es mejor usar herencia o una propiedad anulable? Considere este ejemplo:

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

o

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

o

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

El código que depende de estas 3 opciones sería:

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

o

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

o

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

El primer enfoque me parece más natural, pero tal vez sea menos legible debido a la necesidad de hacer casting. ¿Hay alguna otra forma de lograr este comportamiento?

Bojan VukasovicTest
fuente
1
Me temo que la solución se reduce a su definición de reglas comerciales. Quiero decir, si todos tus libros pueden tener un color pero el color es opcional, entonces la herencia es exagerada y realmente innecesaria, porque quieres que todos tus libros sepan que existe la propiedad del color. Por otro lado, si puede tener libros que no sepan nada de color y libros que tengan color, crear clases especializadas no está mal. Incluso entonces, probablemente iría por la composición sobre la herencia.
Andy
Para hacerlo un poco más específico, hay 2 tipos de libros, uno con color y otro sin él. Entonces, no todos los libros pueden tener un color.
Bojan VukasovicTest
1
Si realmente hay un caso, donde el libro no tiene color en absoluto, en su caso, la herencia es la forma más simple de extender las propiedades de un libro colorido. En este caso, no parece que estaría rompiendo nada heredando la clase de libro base y agregando una propiedad de color. Puede leer sobre el principio de sustitución de liskov para obtener más información sobre casos en los que se permite extender una clase y cuándo no.
Andy
44
¿Puedes identificar el comportamiento que quieres de los libros con colores? ¿Puedes encontrar un comportamiento común entre los libros con y sin color, donde los libros con color deben comportarse de manera ligeramente diferente? Si es así, este es un buen caso para OOP con diferentes tipos, y la idea sería mover los comportamientos a las clases en lugar de interrogar la presencia y el valor de la propiedad externamente.
Erik Eidt
1
Si los posibles colores del libro se conocen con anticipación, ¿qué tal un campo Enum para BookColor con una opción para BookColor.NoColor?
Graham

Respuestas:

7

Depende de las circunstancias. El ejemplo específico no es realista, ya que no se llamaría a una subclase BookWithColoren un programa real.

Pero, en general, una propiedad que solo tiene sentido para ciertas subclases solo debería existir en esas subclases.

Por ejemplo, si Booktiene PhysicalBooky DigialBookcomo descendientes, entonces PhysicalBookpodría tener una weightpropiedad y DigitalBookuna sizeInKbpropiedad. Pero DigitalBookno tendrá weighty viceversa. Bookno tendrá ninguna propiedad, ya que una clase solo debe tener propiedades compartidas por todos los descendientes.

Un mejor ejemplo es mirar clases de software real. El JSlidercomponente tiene el campo majorTickSpacing. Como solo un control deslizante tiene "marcas", este campo solo tiene sentido para JSlidersus descendientes. Sería muy confuso si otros componentes hermanos como JButtontuvieran un majorTickSpacingcampo.

JacquesB
fuente
o podría reducir el peso para poder reflejar ambos, pero eso probablemente sea demasiado.
Walfrat
3
@Walfrat: Sería una muy mala idea que el mismo campo represente cosas completamente diferentes en diferentes subclases.
JacquesB
¿Qué pasa si un libro tiene ediciones físicas y digitales? Tendría que crear una clase diferente para cada permutación de tener o no tener cada campo opcional. Creo que la mejor solución sería permitir que algunos campos se omitan o no, dependiendo del libro. Usted ha citado una situación completamente diferente en la que las dos clases se comportarían de manera funcional diferente. Eso no es lo que implica esta pregunta. Solo necesitan modelar una estructura de datos.
4castle
@ 4castle: necesitaría clases separadas por edición, ya que cada edición también puede tener un precio diferente. No puede resolver esto con campos opcionales. Pero no sabemos por el ejemplo si el operador desea un comportamiento diferente para libros con color o "libros incoloros", por lo que es imposible saber cuál es el modelado correcto en este caso.
JacquesB
3
de acuerdo con @JacquesB. no puede dar una solución universalmente correcta para "libros de modelos", porque depende de qué problema está tratando de resolver y cuál es su negocio. Creo que es bastante seguro decir que la definición de jerarquías de clases donde se violan Liskov es categóricamente incorrecto (tm), aunque (sí sí, su caso es probablemente muy especial y lo justifica (aunque lo dudo))
Sara
5

Un punto importante que no parece haber sido mencionado: en la mayoría de los idiomas, las instancias de una clase no pueden cambiar de qué clase son instancias. Entonces, si tuviera un libro sin color y quisiera agregar un color, necesitaría crear un nuevo objeto si usa clases diferentes. Y luego probablemente deba reemplazar todas las referencias al objeto antiguo con referencias al nuevo objeto.

Si "libros sin color" y "libros con color" son instancias de la misma clase, entonces agregar el color o eliminar el color será un problema mucho menor. (Si su interfaz de usuario muestra una lista de "libros con color" y "libros sin color", entonces esa interfaz de usuario tendría que cambiar obviamente, pero supongo que eso es algo que debe manejar de todos modos, similar a una "lista de rojo libros "y" lista de libros verdes ").

gnasher729
fuente
Este es realmente un punto importante! Define si se debe considerar una colección de propiedades opcionales en lugar de atributos de clase.
cromanoide
1

Piense en un JavaBean (como su Book) como un registro en una base de datos. Las columnas opcionales son nullcuando no tienen valor, pero es perfectamente legal. Por lo tanto, su segunda opción:

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

Es lo mas razonable. 1

Sin embargo, ten cuidado con cómo usas la Optionalclase. Por ejemplo, no lo es Serializable, lo que suele ser una característica de un JavaBean. Aquí hay algunos consejos de Stephen Colebourne :

  1. No declare ninguna variable de instancia de tipo Optional.
  2. Se usa nullpara indicar datos opcionales dentro del ámbito privado de una clase.
  3. Se usa Optionalpara captadores que acceden al campo opcional.
  4. No utilizar Optionalen setters o constructores.
  5. Úselo Optionalcomo tipo de retorno para cualquier otro método de lógica de negocios que tenga un resultado opcional.

Por lo tanto, dentro de su clase debe usar nullpara representar que el campo no está presente, pero cuando color sale del Book(como tipo de retorno) debe estar envuelto con un Optional.

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

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

Esto proporciona un diseño claro y un código más legible.


1 Si tiene la intención de que un BookWithColorencapsule una "clase" completa de libros que tienen capacidades especializadas sobre otros libros, entonces tendría sentido usar la herencia.

4castle
fuente
1
¿Quién dijo algo sobre una base de datos? Si todos los patrones OO tuvieran que encajar en un paradigma de base de datos relacional, tendríamos que cambiar muchos patrones OO.
alexanderbird
Yo diría que la adición de propiedades no utilizadas "por si acaso" es la opción menos clara. Como otros dijeron, depende del caso de uso, pero la situación más deseable sería no llevar propiedades donde una aplicación externa necesita saber si una propiedad que existe realmente se está utilizando o no.
Volker Kueffel
@Volker Acordemos no estar de acuerdo, porque puedo citar a varias personas que jugaron una mano para agregar la Optionalclase a Java para este propósito exacto. Puede que no sea OO, pero sin duda es una opinión válida.
4castle
@Alexanderbird Estaba abordando específicamente un JavaBean, que debería ser serializable. Si estaba fuera de tema al abrir JavaBeans, entonces ese fue mi error.
4castle
@Alexanderbird No espero que todos los patrones coincidan con una base de datos relacional, pero sí espero que un Java Bean sea
4castle
0

Debería usar una interfaz (no herencia) o algún tipo de mecánica de propiedad (tal vez incluso algo que funcione como el marco de descripción de recursos).

Entonces tampoco

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

o

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 {
}

Aclaración (editar):

Simplemente agregando campos opcionales en la clase de entidad

Especialmente con el contenedor opcional (editar: ver la respuesta de 4castle ) Creo que esto (agregar campos en la entidad original) es una forma viable de agregar nuevas propiedades a pequeña escala. El mayor problema con este enfoque es que podría funcionar contra el bajo acoplamiento.

Imagine que su clase de libro se define en un proyecto dedicado para su modelo de dominio. Ahora agrega otro proyecto que utiliza el modelo de dominio para realizar una tarea especial. Esta tarea requiere una propiedad adicional en la clase de libro. O terminas con herencia (ver más abajo) o tienes que cambiar el modelo de dominio común para hacer posible la nueva tarea. En el último caso, puede terminar con un montón de proyectos que dependen de sus propias propiedades agregadas a la clase de libro, mientras que la clase de libro en sí depende de estos proyectos, porque no puede comprender la clase de libro sin el proyectos adicionales

¿Por qué la herencia es problemática cuando se trata de proporcionar propiedades adicionales?

Cuando veo su clase de ejemplo "Libro", pienso en un objeto de dominio que a menudo termina teniendo muchos campos y subtipos opcionales. Solo imagine que desea agregar una propiedad para libros que incluyen un CD. Ahora hay cuatro tipos de libros: libros, libros con color, libros con CD, libros con color y CD. No puede describir esta situación con herencia en Java.

Con las interfaces, evita este problema. Puede componer fácilmente las propiedades de una clase de libro específica con interfaces. La delegación y la composición harán que sea fácil obtener exactamente la clase que desea. Con la herencia, generalmente terminas con algunas propiedades opcionales en la clase que solo están allí porque una clase hermana las necesita.

Más información sobre por qué la herencia es a menudo una idea problemática:

¿Por qué los proponentes de la OOP generalmente ven la herencia como algo malo?

JavaWorld: ¿Por qué se extiende es malo?

El problema con la implementación de un conjunto de interfaces para componer un conjunto de propiedades

Cuando usa interfaces para extensión, todo está bien siempre que tenga solo un pequeño conjunto de ellas. Especialmente cuando su modelo de objetos es utilizado y extendido por otros desarrolladores, por ejemplo, en su empresa, la cantidad de interfaces crecerá. Y eventualmente terminas creando una nueva "interfaz de propiedad" oficial que agrega un método que tus colegas ya usaron en su proyecto para el cliente X para un caso de uso completamente no relacionado: Ugh.

editar: otro aspecto importante es mencionado por gnasher729 . A menudo desea agregar dinámicamente propiedades opcionales a un objeto existente. Con herencia o interfaces, tendría que recrear todo el objeto con otra clase que está lejos de ser opcional.

Cuando espere extensiones a su modelo de objetos hasta tal punto, estará mejor modelando explícitamente la posibilidad de una extensión dinámica . Propongo algo como arriba donde cada "extensión" (en este caso propiedad) tiene su propia clase. La clase funciona como espacio de nombres e identificador para la extensión. Cuando configura las convenciones de nomenclatura del paquete de esta manera, permite una cantidad infinita de extensiones sin contaminar el espacio de nombres para los métodos en la clase de entidad original.

En el desarrollo del juego, a menudo te encuentras con situaciones en las que deseas componer el comportamiento y los datos en muchas variaciones. Esta es la razón por la cual el patrón de arquitectura entidad-componente-sistema se hizo bastante popular en los círculos de desarrollo de juegos. Este es también un enfoque interesante que puede considerar, cuando espera muchas extensiones para su modelo de objetos.

cromanoide
fuente
Y no ha abordado la pregunta "cómo decidir entre estas opciones", es solo otra opción. ¿Por qué es esto siempre mejor que todas las opciones que presentó el OP?
alexanderbird
Tienes razón, mejoraré la respuesta.
cromanoide
@ 4castle ¡En Java las interfaces no son herencia! Implementar interfaces es una forma de cumplir con un contrato, mientras que heredar una clase es básicamente extender su comportamiento. Puede implementar múltiples interfaces mientras no puede extender múltiples clases en una clase. En mi ejemplo, usted extiende con interfaces mientras usa la herencia para heredar el comportamiento de la entidad original.
cromanoide
Tiene sentido. En su ejemplo, PropertiesHolderprobablemente sería una clase abstracta. ¿Pero para qué sugerirías una implementación getPropertyo getPropertyValue? Los datos aún deben almacenarse en algún tipo de campo de instancia en algún lugar. ¿O usaría a Collection<Property>para almacenar las propiedades en lugar de usar campos?
4castle
1
Hice Bookextensible PropertiesHolderpor razones de claridad. Por lo PropertiesHoldergeneral, debe ser miembro de todas las clases que necesitan esta funcionalidad. La implementación tendría una Map<Class<? extends Property<?>>,Property<?>>o algo como esto. Esta es la parte fea de la implementación, pero proporciona un marco realmente agradable que incluso funciona con JAXB y JPA.
cromanoide
-1

Si la Bookclase es derivable sin propiedad de color como a continuación

class E_Book extends Book{
   private String type;
}

entonces la herencia es razonable.

Adem Caglin
fuente
¿No estoy seguro de entender tu ejemplo? Si colores privado, cualquier clase derivada no debería preocuparse de colortodos modos. Simplemente agregue algunos constructores Bookque ignoren por colorcompleto y se configurará de manera predeterminada null.
4castle