Anwendungsarchitektur / Komposition in F #

Ich habe SOLID in C # in letzter Zeit auf ein ziemlich extremes Niveau gebracht und irgendwann wurde mir klar, dass ich heutzutage im Grunde nicht mehr viel anderes mache als Funktionen zu komponieren. Und nachdem ich mich kürzlich wieder mit F # befasst hatte, stellte ich mir vor, dass dies wahrscheinlich die besser geeignete Wahl für die Sprache sein würde, die ich gerade tue. Deshalb würde ich gerne versuchen, ein reales C # -Projekt auf F # zu portieren. als Proof of Concept. Ich glaube, ich könnte den eigentlichen Code abrufen (auf sehr unidiomatische Weise), aber ich kann mir nicht vorstellen, wie eine Architektur aussehen würde, die es mir ermöglicht, auf ähnlich flexible Weise wie in C # zu arbeiten.

Damit meine ich, dass ich viele kleine Klassen und Interfaces habe, die ich mithilfe eines IoC-Containers zusammenstelle, und außerdem häufig Muster wie Decorator und Composite. Dies führt (meiner Meinung nach) zu einer sehr flexiblen und entwicklungsfähigen Gesamtarchitektur, die es mir ermöglicht, die Funktionalität an jedem Punkt der Anwendung einfach zu ersetzen oder zu erweitern. Je nachdem, wie groß die erforderliche Änderung ist, muss ich möglicherweise nur eine neue Implementierung einer Schnittstelle schreiben, diese in der IoC-Registrierung ersetzen und fertig sein. Selbst wenn die Änderung größer ist, kann ich Teile des Objektgraphen ersetzen, während der Rest der Anwendung einfach so bleibt, wie er zuvor war.

Jetzt habe ich mit F # keine Klassen und Interfaces (ich weiß, dass ich das kann, aber ich denke, das ist nicht der Punkt, an dem ich die eigentliche funktionale Programmierung machen möchte), ich habe keine Konstruktorinjektion und ich habe keine IoC Behälter. Ich weiß, dass ich mit Funktionen höherer Ordnung so etwas wie ein Decorator-Muster erstellen kann, aber das scheint mir nicht die gleiche Flexibilität und Wartbarkeit zu bieten wie Klassen mit Konstruktorinjektion.

Betrachten Sie diese C # -Typen:

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

Ich werde meinen IoC-Container auflösenAsdFilteringGetStuff zumIGetStuff undGeneratingGetStuff für seine eigene Abhängigkeit mit dieser Schnittstelle. Wenn ich jetzt einen anderen Filter benötige oder den Filter ganz entferne, brauche ich möglicherweise die entsprechende Implementierung vonIGetStuff und dann einfach die IoC-Registrierung ändern. Solange das Interface gleich bleibt, muss ich nichts anfasseninnerhalb die Anwendung. OCP und LSP, aktiviert durch DIP.

Was mache ich jetzt in 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")

Ich mag, wie viel weniger Code das ist, aber ich verliere die Flexibilität. Ja, ich kann anrufenAsdFilteredDingse oderGenerateDingse am selben Ort, weil die Typen gleich sind - aber wie entscheide ich, welche ich anrufen soll, ohne sie an der Anrufstelle fest zu codieren? Auch wenn diese beiden Funktionen austauschbar sind, kann ich die Generatorfunktion im Inneren jetzt nicht ersetzenAsdFilteredDingse ohne auch diese Funktion zu ändern. Das ist nicht sehr schön.

Nächster Versuch:

let GenerateDingse id =
    // create list

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

Jetzt habe ich die Kompositionsfähigkeit, indem ich AsdFilteredDingse zu einer Funktion höherer Ordnung mache, aber die beiden Funktionen sind nicht mehr austauschbar. Auf den zweiten Blick sollten sie es wahrscheinlich sowieso nicht sein.

Was könnte ich noch tun? Ich könnte das "Composition Root" -Konzept aus meinem C # SOLID in der letzten Datei des F # -Projekts nachahmen. Die meisten Dateien sind nur Sammlungen von Funktionen, dann habe ich eine Art "Registrierung", die den IoC-Container ersetzt, und schließlich gibt es eine Funktion, die ich aufrufe, um die Anwendung tatsächlich auszuführen, und die Funktionen aus der "Registrierung" verwendet. Ich weiß, dass ich in der "Registrierung" eine Funktion vom Typ (Guid -> Dings-Liste) benötige, die ich aufrufen werdeGetDingseForId. Dies ist die, die ich nenne, niemals die einzelnen Funktionen, die zuvor definiert wurden.

Für den Dekorateur wäre die Definition

let GetDingseForId id = AsdFilteredDingse GenerateDingse

Um den Filter zu entfernen, ändere ich ihn auf

let GetDingseForId id = GenerateDingse

Der Nachteil (?) Davon ist, dass alle Funktionen, die andere Funktionen verwenden, sinnvollerweise Funktionen höherer Ordnung sein müssten und meine "Registrierung" zugeordnet werden müsstealle Funktionen, die ich benutze, weil die zuvor definierten Funktionen keine später definierten Funktionen aufrufen können, insbesondere nicht die aus der "Registry". Ich könnte auch auf zirkuläre Abhängigkeitsprobleme mit den "Registrierungs" -Zuordnungen stoßen.

Ist irgendetwas davon sinnvoll? Wie kann eine F # -Anwendung wirklich so erstellt werden, dass sie gewartet und weiterentwickelt werden kann (ganz zu schweigen von der Testbarkeit)?

Antworten auf die Frage(1)

Ihre Antwort auf die Frage