pro planos para nuevas moviles for construccion arquitectura arquitectos architects apps app aplicaciones f# inversion-of-control composition solid-principles

planos - Arquitectura/composición de aplicaciones en F#



ipad app arquitectura (1)

He estado haciendo SOLID en C # a un nivel bastante extremo en los últimos tiempos y en algún momento me di cuenta de que esencialmente no estoy haciendo mucho más que componer funciones hoy en día. Y después de que comencé a buscar F # nuevamente, pensé que probablemente sería la elección de lenguaje mucho más apropiada para gran parte de lo que estoy haciendo ahora, así que me gustaría probar y portar un proyecto de C # del mundo real a F # como una prueba de concepto. Creo que podría sacar el código real (de una manera muy poco idiomática), pero no puedo imaginar cómo se vería una arquitectura que me permita trabajar de manera similar flexible como en C #.

Lo que quiero decir con esto es que tengo muchas clases pequeñas e interfaces que compongo usando un contenedor IoC, y también uso patrones como Decorador y Composite. Esto da como resultado una arquitectura general (en mi opinión) muy flexible y evolutiva que me permite reemplazar o ampliar fácilmente la funcionalidad en cualquier punto de la aplicación. Dependiendo de cuán grande sea el cambio requerido, tal vez solo necesite escribir una nueva implementación de una interfaz, reemplazarla en el registro de IoC y terminar. Incluso si el cambio es más grande, puedo reemplazar partes del gráfico del objeto, mientras que el resto de la aplicación simplemente se mantiene como lo hizo antes.

Ahora, con F #, no tengo clases e interfaces (sé que puedo, pero creo que eso no viene al caso cuando quiero hacer una programación funcional real), no tengo inyección de constructor, y no tengo IoC contenedores. Sé que puedo hacer algo como un patrón Decorator usando funciones de orden superior, pero eso no parece darme el mismo tipo de flexibilidad y mantenibilidad que las clases con inyección de constructor.

Considere estos tipos de C #:

public class Dings { public string Lol { get; set; } public string Rofl { get; set; } } public interface IGetStuff { IEnumerable<Dings> For(Guid id); } public class AsdFilteringGetStuff : IGetStuff { private readonly IGetStuff _innerGetStuff; public AsdFilteringGetStuff(IGetStuff innerGetStuff) { this._innerGetStuff = innerGetStuff; } public IEnumerable<Dings> For(Guid id) { return this._innerGetStuff.For(id).Where(d => d.Lol == "asd"); } } public class GeneratingGetStuff : IGetStuff { public IEnumerable<Dings> For(Guid id) { IEnumerable<Dings> dingse; // somehow knows how to create correct dingse for the ID return dingse; } }

Le diré a mi contenedor de IoC que resuelva AsdFilteringGetStuff para IGetStuff y GeneratingGetStuff para su propia dependencia con esa interfaz. Ahora, si necesito un filtro diferente o elimino el filtro por completo, puedo necesitar la implementación respectiva de IGetStuff y luego simplemente cambiar el registro de IoC. Siempre que la interfaz se mantenga igual, no es necesario que toque cosas dentro de la aplicación. OCP y LSP, habilitado por DIP.

Ahora que hago en F #?

type Dings (lol, rofl) = member x.Lol = lol member x.Rofl = rofl let GenerateDingse id = // create list let AsdFilteredDingse id = GenerateDingse id |> List.filter (fun x -> x.Lol = "asd")

Me encanta cuánto menos código es esto, pero pierdo flexibilidad. Sí, puedo llamar a AsdFilteredDingse o GenerateDingse en el mismo lugar, porque los tipos son los mismos, pero ¿cómo puedo decidir a cuál llamar sin tener que codificarlo en el sitio de llamadas? Además, aunque estas dos funciones son intercambiables, ahora no puedo reemplazar la función del generador dentro de AsdFilteredDingse sin cambiar también esta función. Esto no es muy bueno

Siguiente intento:

let GenerateDingse id = // create list let AsdFilteredDingse (generator : System.Guid -> Dings list) id = generator id |> List.filter (fun x -> x.Lol = "asd")

Ahora tengo capacidad de compilación haciendo AsdFilteredDingse una función de orden superior, pero las dos funciones ya no son intercambiables. Pensándolo bien, probablemente no deberían serlo de todos modos.

¿Qué más podría hacer? Podría imitar el concepto de "raíz de composición" de mi C # SOLID en el último archivo del proyecto F #. La mayoría de los archivos son simplemente colecciones de funciones, luego tengo algún tipo de "registro", que reemplaza el contenedor IoC, y finalmente hay una función a la que llamo para ejecutar realmente la aplicación y que usa funciones del "registro". En el "registro", sé que necesito una función de tipo (Guid -> lista de Dings), que llamaré GetDingseForId . Este es el que yo llamo, nunca las funciones individuales definidas anteriormente.

Para el decorador, la definición sería

let GetDingseForId id = AsdFilteredDingse GenerateDingse

Para eliminar el filtro, cambiaría eso a

let GetDingseForId id = GenerateDingse

La desventaja (?) De esto es que todas las funciones que usan otras funciones deberían tener sensiblemente funciones de orden superior, y mi "registro" debería mapear todas las funciones que uso, porque las funciones actuales definidas anteriormente no pueden llamar a ninguna función. funciones definidas más adelante, en particular no las del "registro". También podría encontrar problemas de dependencia circular con las asignaciones de "registro".

¿Algo de esto tiene sentido? ¿Cómo se puede construir realmente una aplicación F # para que se pueda mantener y evolucionar (sin mencionar que se puede probar)?


Esto es fácil una vez que se da cuenta de que la Inyección de Constructor Orientada a Objetos corresponde muy de cerca a la Aplicación Funcional de Función Parcial .

Primero, escribiría Dings como un tipo de registro:

type Dings = { Lol : string; Rofl : string }

En F #, la interfaz IGetStuff se puede reducir a una sola función con la firma

Guid -> seq<Dings>

Un cliente que usa esta función lo tomaría como un parámetro:

let Client getStuff = getStuff(Guid("055E7FF1-2919-4246-876E-1DA71980BE9C")) |> Seq.toList

La firma para la función Client es:

(Guid -> #seq<''b>) -> ''b list

Como puede ver, toma una función de la firma de destino como entrada y devuelve una lista.

Generador

La función del generador es fácil de escribir:

let GenerateDingse id = seq { yield { Lol = "Ha!"; Rofl = "Ha ha ha!" } yield { Lol = "Ho!"; Rofl = "Ho ho ho!" } yield { Lol = "asd"; Rofl = "ASD" } }

La función GenerateDingse tiene esta firma:

''a -> seq<Dings>

Esto es en realidad más genérico que Guid -> seq<Dings> , pero eso no es un problema. Si solo quiere componer el Client con GenerateDingse , simplemente puede usarlo así:

let result = Client GenerateDingse

Que devolvería los tres valores Ding de GenerateDingse .

Decorador

El decorador original es un poco más difícil, pero no mucho. En general, en lugar de agregar el tipo Decorado (interno) como un argumento constructor, simplemente lo agrega como valor de parámetro a una función:

let AdsFilteredDingse id s = s |> Seq.filter (fun d -> d.Lol = "asd")

Esta función tiene esta firma:

''a -> seq<Dings> -> seq<Dings>

No es exactamente lo que queremos, pero es fácil componerlo con GenerateDingse :

let composed id = GenerateDingse id |> AdsFilteredDingse id

La función composed tiene la firma

''a -> seq<Dings>

Justo lo que estamos buscando!

Ahora puede usar Client con composed como este:

let result = Client composed

que devolverá solo [{Lol = "asd"; Rofl = "ASD";}] [{Lol = "asd"; Rofl = "ASD";}] .

No es necesario definir primero la función composed ; también puedes componerlo en el acto:

let result = Client (fun id -> GenerateDingse id |> AdsFilteredDingse id)

Esto también devuelve [{Lol = "asd"; Rofl = "ASD";}] [{Lol = "asd"; Rofl = "ASD";}] .

Decorador alternativo

El ejemplo anterior funciona bien, pero realmente no Decora una función similar. Aquí hay una alternativa:

let AdsFilteredDingse id f = f id |> Seq.filter (fun d -> d.Lol = "asd")

Esta función tiene la firma:

''a -> (''a -> #seq<Dings>) -> seq<Dings>

Como puede ver, el argumento f es otra función con la misma firma, por lo que se asemeja más al patrón Decorator. Puedes componerlo así:

let composed id = GenerateDingse |> AdsFilteredDingse id

De nuevo, puede usar Client con composed como este:

let result = Client composed

o en línea así:

let result = Client (fun id -> GenerateDingse |> AdsFilteredDingse id)

Para obtener más ejemplos y principios para componer aplicaciones completas con F #, consulte mi curso en línea sobre Arquitectura funcional con F # .

Para obtener más información sobre los principios orientados a objetos y cómo se asignan a la programación funcional, consulte la publicación de mi blog sobre los principios SOLID y cómo se aplican a FP .