Varias veces se les preguntó a los colegas que no estaba claro por qué se necesitaban mixins (impurezas) en el idioma Dart. Decidí ver qué hay en Internet sobre este tema. Para disgusto de los artículos que se pueden encontrar, hablan principalmente sobre cómo usar impurezas, pero no explican por qué son necesarios, en cuyo caso su uso es más preferible que la herencia ordinaria o la implementación de interfaces. Este artículo es un intento de llenar este vacío.
A pesar del hecho de que hay suficientes artículos en Internet sobre el tema de las impurezas en Dart and Flutter, no aportan claridad en mi opinión porque los ejemplos que se muestran muestran la mecánica pura de construir clases con impurezas, lo que está lejos de ser razonable y, por lo tanto, no demuestra el alcance real de su aplicación. . En particular, conocí ese ejemplo . Tenemos:
class Animal {} class Dog {} class Cat {}
Y por alguna razón, queríamos obtener un animal que tuviera las propiedades de gatos y perros al mismo tiempo. En este caso, podemos hacer esto:
class CatDog extends Animal with Cat, Dog {}
Hay al menos dos preguntas para este ejemplo:
- ¿Por qué necesitamos un cruce entre un gato y un perro?
- ¿Por qué un gato y un perro no heredan de
Animal
? ¿No son animales?
Al mismo tiempo, ¿por qué son necesarias las impurezas? Sigue siendo un misterio.
En mi humilde opinión, para comprender el significado de las impurezas, es necesario comenzar a considerar el tema con la relación de la herencia. El principal punto de herencia en OOP es que una entidad es una variación de otra entidad. Por ejemplo, el
es una variación de la
o el
es una variación del
. Y esto es precisamente lo que debería ser el factor determinante en la construcción de una jerarquía de clases.
Si observamos la herencia desde un punto de vista diferente, veremos que el
hereda las propiedades de la
, y el
hereda las propiedades del
. Si no presta atención a la lógica, entonces, puramente técnico, es posible que desee heredar las propiedades de varias entidades diferentes. Para hacer esto, algunos lenguajes de programación admiten herencia múltiple .
La herencia múltiple es criticada por una serie de deficiencias (ver Wikipedia ), por lo que muchos lenguajes de programación no utilizan herencia múltiple en absoluto, sino que utilizan un mecanismo para implementar interfaces y / o impurezas. Y, desde el punto de vista de la lógica, las construcciones resultantes de la herencia múltiple no son fáciles de comprender.
Para comprender el siguiente material, es necesario recordar algunos conceptos de la lógica elemental. En particular, los conceptos de propiedades esenciales y no esenciales . Las propiedades esenciales de los objetos son aquellas debido a la presencia de las cuales se refiere a una clase particular de objetos. Las propiedades no esenciales de un objeto son aquellas cuya presencia, ausencia o valores específicos no afectan el objeto que pertenece a una determinada clase de objetos. Por ejemplo, la forma de un rectángulo es una propiedad esencial de esta forma, porque si cambiamos esta forma (eliminar o agregar un lado o cambiar los ángulos), el rectángulo dejará de ser un rectángulo. Pero si cambia el tamaño del rectángulo, seguirá siendo un rectángulo. Por lo tanto, las dimensiones son una propiedad insignificante.
La construcción de una jerarquía de clases generalmente se basa en agregar propiedades esenciales a la clase primaria. Por ejemplo
abstract class Shape { void draw(); } class Rectangle extends Shape { @override void draw() { print('Draw rectangle'); } } class Circle extends Shape { @override void draw() { print('Draw circle'); } }
La base de esta jerarquía es la propiedad esencial de la forma de la figura.
Otro ejemplo:
abstract class Widget { void render(); } class Container extends Widget { @override void render() { print('Renders container'); } } class Text extends Widget { @override void render('Render text'); }
Una propiedad esencial aquí es el propósito del widget.
Ahora supongamos que necesitamos agregar algunas propiedades no esenciales a nuestras entidades. Tal propiedad, por ejemplo, es el color. Ahora queramos colorear algunas formas y widgets.
Para hacer esto, puede, por supuesto, usar la herencia e implementar primero las PaintableWidget
PaintableShape
y PaintableWidget
. Pero esto no es conveniente, porque, en primer lugar, tendremos que duplicar la implementación de la funcionalidad de coloración en ambas jerarquías y, en segundo lugar, para cada figura y widget que queremos colorear, tendremos que implementar nuevas clases, por ejemplo, PaintableRect
y PaintableContainer
.
Puede usar el mecanismo para implementar interfaces. Entonces obtenemos algo como esto:
enum Color {red, yellow, green} abstract class Paintable { void paint(Color color); Color get color; } class PaintableRect extends Rectangle implements Paintable { Color _color; @override void paint(Color color) {_color = color;} @override Color get color => _color; } class PaintableContainer extends Container implements Paintable { Color _color; @override void paint(Color color) {_color = color;} @override Color get color => _color; }
Como puede ver, esta tampoco es la mejor solución, ya que tenemos que duplicar el mismo código para cada entidad resoluble.
Pero todos estos problemas pueden resolverse si la función funcional asociada con una propiedad insignificante se elimina como una mezcla separada (mixin):
enum Color {red, yellow, green} mixin PaintableMixin { Color _color; void paint(Color color) {_color = color;} Color get color => _color; } class PaintableRect extends Rectangle with PaintableMixin { @override void draw() { print('Draw rectangle with color $color'); } } class PaintableContainer extends Container with PaintableMixin { @override void render() { print('Render container with color $color'); } }
Ahora puedes usarlo:
main() { PaintableRect() ..paint(Color.red) ..draw(); PaintableContainer() ..paint(Color.yellow) ..render(); }
Para resumir lo anterior, se puede determinar de la siguiente manera cuándo es conveniente usar impurezas: si hay varias jerarquías diferentes que necesitan agregar la misma función que define alguna propiedad no esencial para las entidades de estas jerarquías. O puede ser una jerarquía, pero estamos tratando con sus diferentes ramas. Como ejemplo, considere los widgets del marco Flutter.
Supongamos que necesitamos agregar funcionalidad relacionada con la misma propiedad a algunos widgets. Los widgets en Flutter se construyen de la siguiente manera:
class MyStatelessWidget extends StatelessWidget {}
o
class MyStatefulWidget extends StatefulWidget {}
Para agregar una propiedad a través de la herencia, deberá implementar al menos dos clases:
class StatelessWidgetWithProperty extends StatelessWidget {} class StatefulWidgetWithPropery extends StatefulWidget {}
al mismo tiempo, como puede ver nuevamente, debe duplicar la funcionalidad asociada con la propiedad agregada.
Cuando se usan impurezas, el problema se resuelve:
mixin Property {} class MyStatelessWidget extends StatelessWidget with Propery {} class MyStatefulWidget extends StatefulWidget with Property {}
Para aquellos que están familiarizados con los patrones de diseño, el uso de impurezas en algunos casos puede reemplazar el uso del patrón Bridge .
En conclusión, debe notarse que de esta manera uno puede mezclar funcionalmente varias propiedades diferentes a la vez en combinaciones arbitrarias.
Este artículo no pretende definir exhaustivamente el uso de impurezas. Probablemente la mente inquisitiva del desarrollador podrá encontrar muchos más usos hermosos para ellos. Me alegraría si estas opciones para usar impurezas aparecen en los comentarios de este artículo.