¿Por qué un modelo de dominio anémico se considera malo en C # / OOP, pero muy importante en F # / FP?

46

En una publicación de blog en F # por diversión y ganancias, dice:

En un diseño funcional, es muy importante separar el comportamiento de los datos. Los tipos de datos son simples y "tontos". Y luego, por separado, tiene una serie de funciones que actúan sobre esos tipos de datos.

Esto es exactamente lo contrario de un diseño orientado a objetos, donde el comportamiento y los datos están destinados a combinarse. Después de todo, eso es exactamente lo que es una clase. De hecho, en un diseño verdaderamente orientado a objetos, no debe tener nada más que comportamiento: los datos son privados y solo se puede acceder a ellos mediante métodos.

De hecho, en OOD, no tener un comportamiento suficiente en torno a un tipo de datos se considera algo malo e incluso tiene un nombre: el " modelo de dominio anémico ".

Dado que en C # parece que seguimos pidiendo prestado de F # y tratando de escribir más código de estilo funcional; ¿Cómo es que no tomamos prestada la idea de separar datos / comportamiento, e incluso lo consideramos malo? ¿Es simplemente que la definición no coincide con OOP, o hay una razón concreta de que sea mala en C # que por alguna razón no se aplica en F # (y de hecho, se invierte)?

(Nota: Estoy específicamente interesado en las diferencias en C # / F # que podrían cambiar la opinión de lo que es bueno / malo, en lugar de las personas que pueden estar en desacuerdo con cualquiera de las opiniones en la publicación del blog).

Danny Tuppeny
fuente
1
Hola dan Tu gratitud es inspiradora. Además de la plataforma .NET (y haskell), te animo a que veas scala. Debashish ghosh ha escrito un par de blogs sobre modelado de dominios con herramientas funcionales, fue perspicaz para mí, espero que también para ti, aquí tengas: debasishg.blogspot.com/2012/01/…
AndreasScheinert
2
Un colega me envió una publicación de blog interesante hoy: blog.inf.ed.ac.uk/sapm/2014/02/04/… Parece que la gente está empezando a cuestionar la idea de que los modelos de dominio anémico son absolutamente malos; ¡lo cual creo que podría ser algo bueno!
Danny Tuppeny
1
La publicación de blog a la que hace referencia se basa en una idea equivocada: "Normalmente está bien que los datos se expongan sin estar encapsulados. Los datos son inmutables, por lo que no pueden" dañarse "por una función que se porta mal". Incluso los tipos inmutables tienen invariantes que deben preservarse, y eso requiere ocultar datos y controlar cómo se pueden crear. Por ejemplo, no puede exponer la implementación de un árbol rojo-negro inmutable, porque entonces alguien podría crear un árbol que consta de solo nodos rojos.
Doval
44
@Doval, para ser justos, es como decir que no puedes exponer un escritor del sistema de archivos porque alguien podría llenar tu disco. Alguien que crea un árbol de solo nodos rojos no causa ningún daño al árbol rojo-negro del que fueron clonados, ni a ningún código en todo el sistema que esté usando esa instancia bien formada. Si escribe código que crea activamente nuevas instancias de basura o hace cosas peligrosas, la inmutabilidad no lo salvará, pero salvará a otros de usted . Ocultar la implementación no impedirá que las personas escriban código sin sentido que termine dividiéndose por cero.
Jimmy Hoffa
2
@JimmyHoffa Estoy de acuerdo, pero eso no tiene nada que ver con lo que critiqué. El autor afirma que normalmente está bien que se expongan los datos porque son inmutables, y digo que la inmutabilidad no elimina mágicamente la necesidad de ocultar los detalles de la implementación.
Doval

Respuestas:

37

La razón principal por la que FP apunta a esto y C # OOP no lo hace es que en FP el foco está en la transparencia referencial; es decir, los datos entran en una función y salen datos, pero los datos originales no cambian.

En C # OOP existe un concepto de delegación de responsabilidad en el que delega la administración de un objeto en él y, por lo tanto, desea que cambie sus propios elementos internos.

En FP nunca desea cambiar los valores en un objeto, por lo tanto, tener sus funciones incrustadas en su objeto no tiene sentido.

Además, en FP tiene un polimorfismo de tipo más alto que permite que sus funciones sean mucho más generalizadas de lo que permite C # OOP. De esta manera, puede escribir una función que funcione para cualquiera ay, por lo tanto, tenerla incrustada en un bloque de datos no tiene sentido; eso acoplaría estrechamente el método para que solo funcione con ese tipo particular de a. Un comportamiento como ese está muy bien y es común en C # OOP porque no tienes la capacidad de abstraer funciones tan generalmente de todos modos, pero en FP es una compensación.

El mayor problema que he visto en los modelos de dominio anémico en C # OOP es que terminas con un código duplicado porque tienes DTO x, y 4 funciones diferentes que comprometen la actividad f a DTO x porque 4 personas diferentes no vieron la otra implementación . Cuando coloca el método directamente en DTO x, esas 4 personas ven la implementación de f y la reutilizan.

Los modelos de datos anémicos en C # OOP dificultan la reutilización del código, pero este no es el caso en FP porque una sola función se generaliza en tantos tipos diferentes que obtienes una mayor reutilización del código, ya que esa función es utilizable en muchos más escenarios que una función escribiría para un solo DTO en C #.


Como se señaló en los comentarios , la inferencia de tipos es uno de los beneficios en los que se basa FP para permitir un polimorfismo tan significativo, y específicamente puede rastrear esto al sistema de tipos Hindley Milner con la inferencia de tipo Algorithm W; dicha inferencia de tipo en el sistema de tipo C # OOP se evitó porque el tiempo de compilación cuando se agrega inferencia basada en restricciones se vuelve extremadamente largo debido a la búsqueda exhaustiva necesaria, detalles aquí: https://stackoverflow.com/questions/3968834/generics-why -cant-the-compiler-infer-the-type-argumentos-en-este-caso

Jimmy Hoffa
fuente
¿Qué características en F # hacen que sea más fácil escribir este tipo de código reutilizable? ¿Por qué el código en C # no puede ser tan reutilizable? (Creo que vi la posibilidad de que los métodos tomen argumentos con propiedades específicas, sin necesidad de una interfaz; ¿cuál supongo que sería clave?)
Danny Tuppeny,
77
@DannyTuppeny honestamente F # es un mal ejemplo de comparación, es solo un C # ligeramente disfrazado; es un lenguaje imperativo mutable al igual que C #, tiene algunas facilidades de FP que C # no tiene pero no mucho. Mire a Haskell para ver dónde realmente se destaca FP y cosas como esta se vuelven mucho más posibles debido a las clases de tipos, así como a los ADT genéricos
Jimmy Hoffa
@MattFenwick Me refiero explícitamente a C # en eso porque eso es lo que el cartel preguntaba. Cuando me refiero a OOP a lo largo de mi respuesta aquí me refiero a C # OOP, editaré para aclarar.
Jimmy Hoffa
2
Una característica común entre los lenguajes funcionales que permite este tipo de reutilización es la escritura dinámica o inferida. Si bien el lenguaje en sí puede usar tipos de datos bien definidos, a la función típica no le importa qué datos son, siempre que las operaciones (otras funciones o aritmética) que se realicen en ellos sean válidas. Esto también está disponible en paradigmas OO (Go, por ejemplo, tiene una implementación de interfaz implícita que permite que un objeto sea un Pato, porque puede Volar, Nadar y Quack, sin que el objeto haya sido declarado explícitamente como un Pato) pero es más o menos un requisito de programación funcional.
KeithS
44
@KeithS Por sobrecarga basada en valores en Haskell, creo que te refieres a la coincidencia de patrones. La capacidad de Haskell de tener múltiples funciones de nivel superior del mismo nombre con diferentes patrones se desvanece inmediatamente a 1 función de nivel superior + una coincidencia de patrón.
jozefg
6

¿Por qué un modelo de dominio anémico se considera malo en C # / OOP, pero muy importante en F # / FP?

Su pregunta tiene un gran problema que limitará la utilidad de las respuestas que obtiene: implica / asume que F # y FP son similares. FP es una gran familia de lenguajes que incluye reescritura de términos simbólicos, dinámicos y estáticos. Incluso entre los lenguajes FP de tipo estático, existen muchas tecnologías diferentes para expresar modelos de dominio, como los módulos de orden superior en OCaml y SML (que no existen en F #). F # es uno de estos lenguajes funcionales, pero es particularmente notable por ser delgado y, en particular, no proporciona módulos de orden superior ni tipos de tipo superior.

De hecho, no podría comenzar a decirte cómo se expresan los modelos de dominio en FP. La otra respuesta aquí habla muy específicamente sobre cómo se hace en Haskell y no es aplicable en absoluto a Lisp (la madre de todos los idiomas FP), la familia de idiomas ML o cualquier otro idioma funcional.

¿Cómo es que no tomamos prestada la idea de separar datos / comportamiento, e incluso lo consideramos malo?

Los genéricos pueden considerarse una forma de separar datos y comportamiento. Los genéricos provienen de la familia ML de lenguajes de programación funcionales que no forman parte de OOP. C # tiene genéricos, por supuesto. Entonces, uno podría argumentar que C # está prestando lentamente la idea de separar datos y comportamiento.

¿Es simplemente que la definición no encaja con OOP,

Creo que OOP se basa en una premisa fundamentalmente diferente y, en consecuencia, no le brinda las herramientas que necesita para separar los datos y el comportamiento. Para todos los fines prácticos, necesita los tipos de datos de producto y suma y el envío a través de ellos. En ML esto significa unión y tipos de registros y coincidencia de patrones.

Mira el ejemplo que di aquí .

¿O hay una razón concreta de que es malo en C # que por alguna razón no se aplica en F # (y de hecho, se invierte)?

Tenga cuidado al saltar de OOP a C #. C # no es tan puritano sobre OOP como otros lenguajes. .NET Framework ahora está lleno de genéricos, métodos estáticos e incluso lambdas.

(Nota: Estoy específicamente interesado en las diferencias en C # / F # que podrían cambiar la opinión de lo que es bueno / malo, en lugar de las personas que pueden estar en desacuerdo con cualquiera de las opiniones en la publicación del blog).

La falta de tipos de unión y coincidencia de patrones en C # hace que sea casi imposible hacerlo. Cuando todo lo que tienes es un martillo, todo parece un clavo ...

Jon Harrop
fuente
2
... cuando todo lo que tiene es un martillo, la gente de OOP creará fábricas de martillos; P +1 para precisar realmente el quid de lo que falta C # para permitir que los datos y el comportamiento se abstraigan completamente entre sí: tipos de unión y coincidencia de patrones.
Jimmy Hoffa
-4

Creo que en una aplicación empresarial a menudo no desea ocultar datos porque la coincidencia de patrones en valores inmutables es excelente para garantizar que cubra todos los casos posibles. Pero si está implementando algoritmos complejos o estructuras de datos, es mejor que oculte los detalles de implementación convirtiendo los ADT (tipos de datos algebraicos) en ADT (tipos de datos abstractos).

Giacomo Citi
fuente
44
¿Podría explicar un poco más sobre cómo se aplica esto a la programación orientada a objetos en comparación con la programación funcional?