Mixins vs composición en scala

78

En el mundo de Java (más precisamente si no tiene herencia múltiple / mixins) la regla general es bastante simple: "Favorecer la composición de objetos sobre la herencia de clases".

Me gustaría saber si / cómo se cambia si también considera los mixins, especialmente en scala.
¿Los mixins se consideran una forma de herencia múltiple o más composición de clases?
¿Existe también una pauta de "Favorecer la composición de objetos sobre la composición de clases" (o al revés)?

He visto bastantes ejemplos en los que las personas usan (o abusan) de mixins cuando la composición de objetos también podría hacer el trabajo y no siempre estoy seguro de cuál es mejor. Me parece que se pueden lograr cosas bastante similares con ellos, pero también hay algunas diferencias, algunos ejemplos:

  • Visibilidad: con mixins, todo pasa a formar parte de la API pública, lo que no ocurre con la composición.
  • verbosidad: en la mayoría de los casos, los mixins son menos detallados y un poco más fáciles de usar, pero no siempre es así (por ejemplo, si también usa tipos propios en jerarquías complejas)

Sé que la respuesta corta es "Depende", pero probablemente haya alguna situación típica en la que esto o aquello sea mejor.

Algunos ejemplos de pautas que se me han ocurrido hasta ahora (suponiendo que tenga dos rasgos A y B y que A quiera usar algunos métodos de B):

  • Si desea extender la API de A con los métodos de B, entonces mixins, de lo contrario composición. Pero no ayuda si la clase / instancia que estoy creando no es parte de una API pública.
  • Si desea utilizar algunos patrones que necesitan combinaciones (por ejemplo, patrón de rasgos apilables ), entonces es una decisión fácil.
  • Si tiene dependencias circulares, los mixins con tipos propios pueden ayudar. (Trato de evitar esta situación, pero no siempre es fácil)
  • Si desea algunas decisiones dinámicas en tiempo de ejecución sobre cómo hacer la composición, luego la composición del objeto.

En muchos casos, los mixins parecen ser más fáciles (y / o menos detallados), pero estoy bastante seguro de que también tienen algunos inconvenientes, como la "clase de Dios" y otros descritos en dos artículos de Artima: parte 1 , parte 2 (por cierto Me parece que la mayoría de los otros problemas no son relevantes / no son tan graves para scala).

¿Tienes más pistas como estas?

Sandor Murakozi
fuente

Respuestas:

41

Muchos de los problemas que las personas tienen con las mezclas pueden evitarse en Scala si solo mezcla rasgos abstractos en las definiciones de su clase y luego mezcla los rasgos concretos correspondientes en el momento de la instanciación del objeto. Por ejemplo

trait Locking{
   // abstract locking trait, many possible definitions
   protected def lock(body: =>A):A
}

class MyService{
   this:Locking =>
}

//For this time, we'll use a java.util.concurrent lock
val myService:MyService = new MyService with JDK15Locking 

Esta construcción tiene varias cosas que la recomiendan. Primero, evita que haya una explosión de clases ya que se necesitan diferentes combinaciones de funcionalidades de rasgos. En segundo lugar, permite realizar pruebas fácilmente, ya que se pueden crear y mezclar rasgos concretos de "no hacer nada", similares a los objetos simulados. Finalmente, hemos ocultado por completo el rasgo de bloqueo utilizado, e incluso ese bloqueo, a los consumidores de nuestro servicio.

Dado que hemos superado la mayoría de los inconvenientes de las mezclas, todavía nos queda una compensación entre la mezcla y la composición. Por mi parte, normalmente tomo la decisión en función de si un objeto delegado hipotético estaría completamente encapsulado por el objeto contenedor, o si podría potencialmente ser compartido y tener un ciclo de vida propio. El bloqueo proporciona un buen ejemplo de delegados completamente encapsulados. Si su clase usa un objeto de bloqueo para administrar el acceso concurrente a su estado interno, ese bloqueo está completamente controlado por el objeto que lo contiene, y ni él ni sus operaciones se anuncian como parte de la interfaz pública de la clase. Para una funcionalidad completamente encapsulada como esta, opto por mix-ins. Para algo compartido, como una fuente de datos, use composición.

Dave Griffith
fuente
¿Cuál es el significado del constructo this => Logging? No se compila.
HRJ
3
Es una anotación de tipo propio, de la que siempre olvido la sintaxis. Editado. En este caso, significa que cualquier objeto que extienda MyService también debe extender Locking
Dave Griffith
11

Otras diferencias que no has mencionado:

  • Las clases de rasgos no tienen existencia independiente:

( Programación de Scala )

Si encuentra que un rasgo en particular se usa con mayor frecuencia como padre de otras clases, de modo que las clases secundarias se comporten como el rasgo principal, considere definir el rasgo como una clase en su lugar, para que esta relación lógica sea más clara.
(Dijimos que se comporta como , en lugar de es , porque la primera es la definición más precisa de herencia, basada en el principio de sustitución de Liskov; ver [Martin2003], por ejemplo).

[Martin2003]: Robert C. Martin, Desarrollo de software ágil: principios, patrones y prácticas, Prentice-Hall, 2003

  • mixins ( trait) no tiene parámetros de constructor.

De ahí el consejo, todavía de Programming Scala :

Evite los campos concretos en los rasgos que no se pueden inicializar a los valores predeterminados adecuados.
En su lugar, use campos abstractos o convierta el rasgo en una clase con un constructor .
Por supuesto, los rasgos sin estado no tienen ningún problema con la inicialización.

Es un principio general de un buen diseño orientado a objetos que una instancia siempre debe estar en un estado válido conocido, comenzando desde el momento en que finaliza el proceso de construcción .

Esa última parte, con respecto al estado inicial de un objeto, a menudo ha ayudado a decidir entre clase (y composición de clase) y rasgo (y mixins) para un concepto dado.

VonC
fuente
Gracias por tu respuesta, pero creo que se trata más de rasgos que de clases. Me siento un poco más cómodo en ese tema :-) Quizás no fui lo suficientemente preciso cuando usé el término "composición de la clase". Lo que quise decir se describe en el artículo Patrón de rasgos apilables, "Este patrón es similar en estructura al patrón del decorador, excepto que involucra decoración con el propósito de composición de clases en lugar de composición de objetos", aunque mezcla clases y rasgos.
Sandor Murakozi
@Sandor: Entiendo. Estaba abordando la parte básica de su pregunta, ya que todavía no me siento completamente cómodo con la distinción, pero pronto debería obtener respuestas más precisas.
VonC
"Los mixins ( trait) no tienen parámetros de constructor " . Curiosamente, Dotty (que se convertirá en Scala 3) admite parámetros de rasgos . Como resultado, la regla general de Programming Scala que mencionaste podría evolucionar.
jub0bs
1
@Jubobs Gracias. 8 años después, no me sorprende que esta regla evolucione.
VonC