Arquitetura / composição de aplicativos em F #

Eu tenho feito o SOLID em C # a um nível bastante extremo nos últimos tempos e, em algum momento, percebi que não estou fazendo muito mais do que compor funções hoje em dia. E depois que recentemente comecei a olhar para o F # novamente, imaginei que provavelmente seria a escolha de idioma muito mais apropriada para muito do que estou fazendo agora, então gostaria de tentar portar um projeto C # do mundo real para o F # como prova de conceito. Eu acho que poderia obter o código real (de uma maneira muito não-idiomática), mas não consigo imaginar como seria uma arquitetura que me permita trabalhar de uma maneira igualmente flexível como em C #.

O que quero dizer com isso é que tenho muitas classes e interfaces pequenas que componho usando um contêiner de IoC e também uso muito padrões como Decorator e Composite. Isso resulta em uma arquitetura geral (na minha opinião) muito flexível e evolutiva que me permite substituir ou estender facilmente a funcionalidade em qualquer ponto do aplicativo. Dependendo do tamanho da alteração necessária, talvez seja necessário escrever uma nova implementação de uma interface, substituí-la no registro de IoC e concluir. Mesmo que a mudança seja maior, eu posso substituir partes do gráfico de objetos enquanto o restante do aplicativo simplesmente permanece como antes.

Agora, com F #, não tenho classes e interfaces (sei que posso, mas acho que isso não vem ao caso em que eu quero fazer programação funcional real), não tenho injeção de construtor e não tenho IoC recipientes. Eu sei que posso fazer algo como um padrão Decorator usando funções de ordem superior, mas isso não parece me dar o mesmo tipo de flexibilidade e manutenção das classes com injeção de construtor.

Considere estes 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;
    }
}

Vou dizer ao meu contêiner de IoC para resolverAsdFilteringGetStuff paraIGetStuff eGeneratingGetStuff por sua própria dependência com essa interface. Agora, se eu precisar de um filtro diferente ou removê-lo completamente, talvez seja necessário a respectiva implementação deIGetStuff e simplesmente altere o registro de IoC. Contanto que a interface permaneça a mesma, não preciso tocar nas coisasdentro a aplicação. OCP e LSP, ativado pelo DIP.

Agora, o que eu faço em 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")

Eu amo quanto menos código isso é, mas perco a flexibilidade. Posso ligar simAsdFilteredDingse ouGenerateDingse no mesmo local, porque os tipos são os mesmos - mas como decido qual deles chamar sem codificá-lo no local da chamada? Além disso, embora essas duas funções sejam intercambiáveis, agora não posso substituir a função do gerador dentroAsdFilteredDingse sem alterar esta função também. Isso não é muito legal.

Próxima tentativa:

let GenerateDingse id =
    // create list

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

Agora eu tenho composibilidade, tornando o AsdFilteredDingse uma função de ordem superior, mas as duas funções não são mais intercambiáveis. Pensando bem, eles provavelmente não deveriam ser assim.

O que mais eu poderia fazer? Eu poderia imitar o conceito "raiz da composição" do meu C # SOLID no último arquivo do projeto F #. A maioria dos arquivos são apenas coleções de funções, então eu tenho algum tipo de "registro", que substitui o contêiner de IoC e, finalmente, há uma função que eu chamo para realmente executar o aplicativo e que usa funções do "registro". No "registro", eu sei que preciso de uma função do tipo (Guid -> lista Dings), que chamareiGetDingseForId. É o que eu chamo, nunca as funções individuais definidas anteriormente.

Para o decorador, a definição seria

let GetDingseForId id = AsdFilteredDingse GenerateDingse

Para remover o filtro, eu mudaria isso para

let GetDingseForId id = GenerateDingse

A desvantagem (?) Disso é que todas as funções que usam outras funções teriam sensivelmente funções de ordem superior e meu "registro" teria que mapeartudo funções que eu uso, porque as funções reais definidas anteriormente não podem chamar nenhuma função definida posteriormente, em particular as do "registro". Também posso ter problemas de dependência circular com os mapeamentos "registro".

Isso faz sentido? Como você realmente cria um aplicativo F # para ser sustentável e evolutível (para não mencionar testável)?

questionAnswers(1)

yourAnswerToTheQuestion