El año pasado di el salto y aprendí un lenguaje de programación funcional (F #) y una de las cosas más interesantes que he encontrado es cómo afecta la forma en que diseño el software OO. Las dos cosas que me faltan más en los idiomas OO son la coincidencia de patrones y los tipos de suma. En todas partes que miro veo situaciones que serían modeladas trivialmente con una unión discriminada, pero soy reacio a la palanca en alguna implementación de OO DU que se siente poco natural para el paradigma.
Esto generalmente me lleva a crear tipos intermedios para manejar las or
relaciones que un tipo de suma manejaría por mí. También parece conducir a una buena ramificación. Si leo a personas como Misko Hevery , sugiere que un buen diseño de OO puede minimizar la ramificación a través del polimorfismo.
Una de las cosas que evito tanto como sea posible en el código OO son los tipos con null
valores. Obviamente, la or
relación puede ser modelada por un tipo con un null
valor y otro sin null
valor, pero esto significa null
pruebas en todas partes. ¿Hay alguna forma de modelar polimórficamente los tipos heterogéneos pero lógicamente asociados? Las estrategias o patrones de diseño serían muy útiles, o simplemente formas de pensar acerca de los tipos heterogéneos y asociados generalmente en el paradigma OO.
fuente
Respuestas:
Al igual que usted, desearía que los sindicatos discriminados fueran más frecuentes; sin embargo, la razón por la que son útiles en la mayoría de los lenguajes funcionales es que brindan una concordancia exhaustiva de patrones, y sin esto, son solo una sintaxis bonita: no solo coincidencia de patrones: coincidencia exhaustiva de patrones, de modo que el código no se compila si no lo hace ' Cubra todas las posibilidades: esto es lo que le da poder.
La única forma de hacer algo útil con un tipo de suma es descomponerlo y ramificar según el tipo que sea (p. Ej., Por coincidencia de patrones). Lo mejor de las interfaces es que no te importa de qué tipo es algo, porque sabes que puedes tratarlo como un
iface
: no se necesita una lógica única para cada tipo: no hay ramificación.Este no es un "código funcional que tiene más ramificaciones, el código OO tiene menos", es un "'lenguajes funcionales' que se adaptan mejor a los dominios donde tiene uniones, que obligan a la ramificación, y los 'idiomas OO' son más adecuados para el código donde puede exponer el comportamiento común como una interfaz común, lo que podría parecer que se ramifica menos ". La ramificación es una función de su diseño y el dominio. En pocas palabras, si sus "tipos heterogéneos pero asociados lógicamente" no pueden exponer una interfaz común, entonces debe ramificar / combinar patrones sobre ellos. Este es un problema de dominio / diseño.
A lo que Misko puede estar refiriéndose es a la idea general de que si puede exponer sus tipos como una interfaz común, entonces el uso de características OO (interfaces / polimorfismo) mejorará su vida al poner un comportamiento específico de tipo en el tipo en lugar de en el consumidor código.
Es importante reconocer que las interfaces y las uniones son algo opuestas entre sí: una interfaz define algunas cosas que el tipo tiene que implementar, y la unión define algunas cosas que el consumidor debe tener en cuenta. Si agrega un método a una interfaz, ha cambiado ese contrato y ahora cada tipo que lo implementó anteriormente debe actualizarse. Si agrega un nuevo tipo a una unión, ha cambiado ese contrato, y ahora cada patrón exhaustivo que coincida con la unión debe actualizarse. Cumplen diferentes roles, y aunque a veces puede ser posible implementar un sistema 'de cualquier manera', lo que usted elige es una decisión de diseño: ninguno es inherentemente mejor.
Una ventaja de usar interfaces / polimorfismo es que el código de consumo es más extensible: puede pasar un tipo que no se definió en el momento del diseño, siempre que exponga la interfaz acordada. Por otro lado, con una unión estática, puede explotar comportamientos que no se consideraron en el momento del diseño escribiendo nuevas coincidencias de patrones exhaustivas, siempre y cuando se cumplan con el contrato de la unión.
Con respecto al 'Patrón de objeto nulo': esto no es una bala de plata y no reemplaza los
null
cheques. Todo lo que hace proporciona una manera de evitar algunas comprobaciones 'nulas' donde el comportamiento 'nulo' puede quedar expuesto detrás de una interfaz común. Si no puede exponer el comportamiento 'nulo' detrás de la interfaz del tipo, entonces estará pensando "Realmente desearía poder hacer coincidir exhaustivamente este patrón" y terminará realizando una verificación de "ramificación".fuente
Hay una forma bastante "estándar" de codificar tipos de suma en un lenguaje orientado a objetos.
Aquí hay dos ejemplos:
En C #, podríamos representar esto como:
F # nuevamente:
C # nuevamente:
La codificación es completamente mecánica. Esta codificación produce un resultado que tiene la mayoría de las mismas ventajas y desventajas de los tipos de datos algebraicos. También puede reconocer esto como una variación del Patrón de visitante. Podríamos recopilar los parámetros
Match
juntos en una interfaz que podríamos llamar un visitante.En el lado de las ventajas, esto le proporciona una codificación basada en principios de tipos de suma. (Es la codificación Scott ). Le brinda una exhaustiva "coincidencia de patrones" aunque solo una "capa" de coincidencia a la vez.
Match
es de alguna manera una interfaz "completa" para estos tipos y cualquier operación adicional que deseemos puede definirse en términos de la misma. Presenta una perspectiva diferente en muchos patrones OO, como el Patrón de objeto nulo y el Patrón de estado, como indiqué en la respuesta de Ryathal, así como el Patrón de visitante y el Patrón compuesto. El tipoOption
/Maybe
es como un patrón de objeto nulo genérico. El patrón compuesto es similar a la codificacióntype Tree<'a> = Leaf of 'a | Children of List<Tree<'a>>
. El patrón de estado es básicamente una codificación de una enumeración.En el lado de las desventajas, como lo escribí, el
Match
método impone algunas restricciones sobre qué subclases se pueden agregar significativamente, especialmente si queremos mantener la Propiedad de sustituibilidad de Liskov. Por ejemplo, aplicar esta codificación a un tipo de enumeración no le permitiría extender significativamente la enumeración. Si quisiera extender la enumeración, tendría que cambiar todas las personas que llaman e implementadores en todas partes como si estuviera usandoenum
yswitch
. Dicho esto, esta codificación es algo más flexible que la original. Por ejemplo, podemos agregar unAppend
implementadorList
que solo contiene dos listas, lo que nos da un apéndice de tiempo constante. Esto se comportaría como las listas adjuntas, pero se representaría de manera diferente.Por supuesto, muchos de estos problemas tienen que ver con el hecho de que
Match
está un tanto (conceptual pero intencionalmente) vinculado a las subclases. Si utilizamos métodos que no son tan específicos, obtenemos diseños OO más tradicionales y recuperamos la extensibilidad, pero perdemos la "integridad" de la interfaz y, por lo tanto, perdemos la capacidad de definir cualquier operación en este tipo en términos de interfaz. Como se mencionó en otra parte, esta es una manifestación del problema de expresión .Podría decirse que los diseños como los anteriores se pueden usar sistemáticamente para eliminar por completo la necesidad de ramificar para lograr un ideal OO. Smalltalk, por ejemplo, usa este patrón a menudo incluso para los propios booleanos. Pero como sugiere la discusión anterior, esta "eliminación de ramificación" es bastante ilusoria. Acabamos de implementar la ramificación de una manera diferente, y todavía tiene muchas de las mismas propiedades.
fuente
El manejo nulo se puede hacer con el patrón de objeto nulo . La idea es crear una instancia de sus objetos que devuelva valores predeterminados para cada miembro y que tenga métodos que no hagan nada, pero que tampoco generen errores. Esto no elimina las verificaciones nulas por completo, pero significa que solo necesita verificar nulos en la creación del objeto y devolver su objeto nulo.
El patrón de estado es una forma de minimizar la ramificación y brindar algunos de los beneficios de la coincidencia de patrones. Nuevamente, empuja la lógica de ramificación a la creación de objetos. Cada estado es una implementación separada de una interfaz base, por lo que todo el código consumidor solo necesita llamar a DoStuff () y se llama al método adecuado. Algunos lenguajes también están agregando coincidencia de patrones como característica, C # es un ejemplo.
fuente