similitud proximidad principios principio ley gestalt experiencia destino continuidad comun cerramiento buena design-patterns f# discriminated-union

design patterns - proximidad - ¿Los sindicatos discriminados entran en conflicto con el principio de cierre abierto?



principio de proximidad (4)

Los objetos y las uniones discriminadas tienen limitaciones que son duales entre sí:

  • Cuando se usa una interfaz, es fácil agregar nuevas clases que implementan la interfaz sin afectar otras implementaciones, pero es difícil agregar nuevos métodos (es decir, si agrega un nuevo método, necesita agregar implementaciones del método a cada clase que implemente la interfaz).
  • Al diseñar un tipo de DU, es fácil agregar nuevos métodos usando el tipo sin afectar otros métodos, pero es difícil agregar nuevos casos (es decir, si agrega un nuevo caso, entonces cada método existente debe actualizarse para manejarlo).

Así que los DU definitivamente no son apropiados para modelar todos los problemas; pero tampoco lo son los diseños OO tradicionales. A menudo, sabes en qué "dirección" necesitarás hacer modificaciones en el futuro, por lo que es fácil elegir (p. Ej., Las listas están vacías o tienen cabeza y cola, por lo que tiene sentido modelizarlas a través de un DU).

A veces se desea poder ampliar las cosas en ambas direcciones (agregar nuevos "tipos" de objetos y también agregar nuevas "operaciones"): esto está relacionado con el problema de expresión , y no hay soluciones particularmente claras en ninguno de los programas clásicos de OO. o la programación FP clásica (aunque son posibles soluciones algo barrocas, ver por ejemplo el comentario de Vesa Karvonen aquí , que he transcrito al F # aquí ).

Una razón por la que los DU pueden verse más favorablemente que cambiar sentencias es que el compilador de F # admite la exhaustividad y la verificación de redundancia puede ser más exhaustiva que, por ejemplo, la comprobación de las sentencias switch del compilador C # (por ejemplo, si tengo match x with | A -> ''a'' | B -> ''b'' y agrego un nuevo caso de DU C entonces obtendré una advertencia / error, pero cuando uso una enum en C # necesito tener un caso default todos modos para que las comprobaciones en tiempo de compilación puedan '' t ser tan fuerte).

No puedo evitar preguntar si el uso de las Uniones Discriminadas dentro de un sistema grande viola el principio de Abrir / Cerrar.

Entiendo que el Principio de Abrir / Cerrar está Orientado a Objetos y NO Funcional. Sin embargo, tengo razones para creer que existe el mismo olor a código.

A menudo evito las declaraciones de cambio porque normalmente me veo obligado a manejar casos que no fueron contabilizados inicialmente. Por lo tanto, me encuentro teniendo que actualizar cada referencia con un nuevo caso y un comportamiento relativo.

Por lo tanto, sigo creyendo que las Uniones Discriminadas tienen el mismo olor a código que las declaraciones de cambio.

¿Son mis pensamientos exactos?

¿Por qué se desaprueban las declaraciones de cambio, pero se aceptan las Uniones Discriminadas?

¿No nos encontramos con las mismas inquietudes de mantenimiento al usar Sindicatos discriminados a medida que cambiamos las declaraciones a medida que evoluciona la base de código o las digresiones?


En mi opinión, el principio de Open / Closed es un poco confuso, ¿qué significa "abrir para extensión" en realidad?

¿Significa extenderse con nuevos datos, o extenderse con un nuevo comportamiento, o ambos?

Aquí hay una cita de Betrand Meyer (tomada de Wikipedia ):

Una clase está cerrada, ya que puede ser compilada, almacenada en una biblioteca, puesta a punto y utilizada por las clases de los clientes. Pero también está abierto, ya que cualquier clase nueva puede usarlo como padre, agregando nuevas características. Cuando se define una clase descendiente, no hay necesidad de cambiar el original o molestar a sus clientes.

Y aquí hay una cita del artículo de Robert Martin:

El principio abierto-cerrado ataca esto de una manera muy directa. Dice que debes diseñar módulos que nunca cambien. Cuando cambian los requisitos, extiende el comportamiento de dichos módulos agregando un nuevo código, no cambiando el código anterior que ya funciona .

Lo que le quito a estas citas es el énfasis en nunca romper clientes que dependen de usted.

En el paradigma orientado a objetos (basado en el comportamiento), interpretaría eso como una recomendación para usar interfaces (o clases base abstractas). Luego, si los requisitos cambian, puede crear una nueva implementación de una interfaz existente o, si se necesita un nuevo comportamiento, crear una nueva interfaz que amplíe la original. (Y por cierto, las declaraciones de cambio no son OO, ¡deberías estar usando polimorfismo !)

En el paradigma funcional, el equivalente de una interfaz desde el punto de vista del diseño es una función. Del mismo modo que pasaría una interfaz a un objeto en un diseño OO, pasaría una función como parámetro a otra función en un diseño FP. Además, en FP, cada firma de función es automáticamente una "interfaz". La implementación de la función se puede cambiar más adelante siempre que su firma de función no cambie.

Si necesita un nuevo comportamiento, simplemente defina una nueva función: los clientes existentes de la función anterior no se verán afectados, mientras que los clientes que necesiten esta nueva funcionalidad deberán modificarse para aceptar un nuevo parámetro.

Extender un DU

Ahora, en el caso específico de los requisitos cambiantes para un DU en F #, puede ampliarlo sin afectar a los clientes de dos maneras.

  • Utilice la composición para crear un nuevo tipo de datos a partir del anterior, o
  • Oculte los casos de los clientes y use patrones activos.

Digamos que tiene un DU simple como este:

type NumberCategory = | IsBig of int | IsSmall of int

Y desea agregar un nuevo caso IsMedium .

En el enfoque de composición, crearía un tipo nuevo sin tocar el tipo antiguo, por ejemplo, de esta manera:

type NumberCategoryV2 = | IsBigOrSmall of NumberCategory | IsMedium of int

Para los clientes que solo necesitan el componente NumberCategory original, puede convertir el nuevo tipo al antiguo de esta manera:

// convert from NumberCategoryV2 to NumberCategory let toOriginal (catV2:NumberCategoryV2) = match catV2 with | IsBigOrSmall original -> original | IsMedium i -> IsSmall i

Puedes pensar en esto como una especie de upcasting explícito :)

Alternativamente, puede ocultar los casos y solo exponer patrones activos:

type NumberCategory = private // now private! | IsBig of int | IsSmall of int let createNumberCategory i = if i > 100 then IsBig i else IsSmall i // active pattern used to extract data since type is private let (|IsBig|IsSmall|) numberCat = match numberCat with | IsBig i -> IsBig i | IsSmall i -> IsSmall i

Más adelante, cuando el tipo cambie, puede modificar los patrones activos para mantener la compatibilidad:

type NumberCategory = private | IsBig of int | IsSmall of int | IsMedium of int // new case added let createNumberCategory i = if i > 100 then IsBig i elif i > 10 then IsMedium i else IsSmall i // active pattern used to extract data since type is private let (|IsBig|IsSmall|) numberCat = match numberCat with | IsBig i -> IsBig i | IsSmall i -> IsSmall i | IsMedium i -> IsSmall i // compatible with old definition

¿Qué enfoque es el mejor?

Bueno, para el código que controlo completamente, no usaría ninguno, ¡solo haría el cambio al DU y repararía los errores del compilador!

Para el código que está expuesto como API a los clientes que no controlo, usaría el enfoque de patrón activo.


Las declaraciones de cambio no son antitéticas al principio Abierto / Cerrado. Todo depende de dónde los pongas.

OCP le dice que agregar nuevas implementaciones de dependencias no debería forzarlo a modificar el código que las está consumiendo.

Pero cuando agrega una nueva implementación, la lógica que decide elegir esa implementación sobre otra debe estar en algún lugar del código. La nueva clase no se tendrá en cuenta por arte de magia. Dicha decisión puede tener lugar en el código de configuración del contenedor IoC, o en un lugar condicional en algún lugar durante la ejecución del programa. Este condicional puede ser perfectamente una declaración de cambio .

Lo mismo ocurre con la coincidencia de patrones. Puede usarlo para decidir qué función pasar a una función de orden superior F (que sería el equivalente a inyectar una dependencia en OO). No significa que F haga la elección o sepa qué función concreta se le asigna. La abstracción se conserva.


No estoy seguro de cuál es su enfoque para el principio Open-Close con OO, pero a menudo termino implementando dicho código de principios recurriendo a funciones de orden superior , el otro enfoque que empleo es el uso de interfaces. Tiendo a evitar las clases base.

Puede usar el mismo enfoque con DU al tener un caso de extensión abierto que tiene un functor como parámetro además de otros casos útiles que están más codificados como:

type Cases<T> = | Case1 of string | Case2 of int | Case3 of IFoo | OpenCase of (unit -> T)

Al usar OpenCase, puede pasar una función que es específica del sitio donde crea una instancia de este valor de la unión discriminada.

¿Por qué se desaprueban las declaraciones de cambio, pero se aceptan las Uniones Discriminadas?

Puede cotejar DU con la coincidencia de patrones, por lo que intentaré aclarar:

La coincidencia de patrones es una construcción de código (como un switch ), mientras que DU es una construcción de tipo (como una jerarquía cerrada de clases o estructuras, o una enumeración).

La coincidencia de patrones con el match en F # tiene mayores habilidades que el switch en C #.

¿No nos encontramos con las mismas inquietudes de mantenimiento al usar Sindicatos discriminados a medida que cambiamos las declaraciones a medida que evoluciona la base de código o las digresiones?

Las uniones discriminatorias usadas con la coincidencia de patrones tienen más propiedades de seguridad / exhaustividad de tipo que una declaración de cambio regular, el compilador es más útil ya que emitirá advertencias para coincidencias incompletas, que no se obtienen con declaraciones de cambio de C #.

Puede tener problemas de mantenimiento con el código OO, que es de principio abierto y no creo que el DU esté relacionado con esto.