Ich habe in letzter Zeit SOLID in C # auf ein ziemlich extremes Niveau gebracht und irgendwann festgestellt, dass ich heutzutage im Wesentlichen nicht viel anderes mache, als Funktionen zu komponieren. Und nachdem ich vor kurzem wieder angefangen hatte, mir F # anzuschauen, dachte ich mir, dass dies wahrscheinlich die viel passendere Wahl der Sprache für vieles ist, was ich jetzt mache, also möchte ich versuchen, ein reales C # -Projekt auf F # zu portieren als Proof of Concept. Ich denke, ich könnte den eigentlichen Code abrufen (auf eine sehr un-idiomatische Weise), aber ich kann mir nicht vorstellen, wie eine Architektur aussehen würde, die es mir ermöglicht, auf eine ähnlich flexible Art und Weise wie in C # zu arbeiten.
Damit meine ich, dass ich viele kleine Klassen und Schnittstellen habe, die ich mit einem IoC-Container zusammenstelle, und ich verwende auch häufig Muster wie Decorator und Composite. Dies führt zu einer (meiner Meinung nach) sehr flexiblen und weiterentwickelbaren 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 Objektdiagramms ersetzen, während der Rest der Anwendung einfach so bleibt wie zuvor.
Jetzt mit F # habe ich keine Klassen und Schnittstellen (ich weiß, dass ich das kann, aber ich denke, das ist nicht der Punkt, an dem ich die eigentliche funktionale Programmierung durchführen 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 geben 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 anweisen, AsdFilteringGetStuff
für IGetStuff
und GeneratingGetStuff
für seine eigene Abhängigkeit von dieser Schnittstelle aufzulösen . Wenn ich nun einen anderen Filter benötige oder den Filter insgesamt entferne, muss ich möglicherweise die entsprechende Implementierung von IGetStuff
und dann einfach die IoC-Registrierung ändern. Solange die Benutzeroberfläche gleich bleibt, muss ich keine Inhalte in der Anwendung berühren . 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 anrufen AsdFilteredDingse
oder GenerateDingse
am selben Ort, da die Typen gleich sind - aber wie entscheide ich, welchen ich anrufen soll, ohne ihn an der Anrufstelle fest zu codieren? Auch wenn diese beiden Funktionen austauschbar sind, kann ich die Generatorfunktion im Inneren nicht ersetzen, AsdFilteredDingse
ohne diese Funktion ebenfalls 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 kann ich AsdFilteredDingse zu einer Funktion höherer Ordnung machen, aber die beiden Funktionen sind nicht mehr austauschbar. Beim zweiten Gedanken 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. In der "Registrierung" weiß ich, dass ich eine Funktion vom Typ (Guid -> Dings-Liste) benötige, die ich aufrufen werde GetDingseForId
. 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, würde ich das in ändern
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" alle Funktionen zuordnen müsste , die ich verwende, da die zuvor definierten tatsächlichen Funktionen keine aufrufen können später definierte Funktionen, insbesondere nicht die aus der "Registrierung". Ich könnte auch auf zirkuläre Abhängigkeitsprobleme mit den "Registrierungs" -Zuordnungen stoßen.
Ist irgendetwas davon sinnvoll? Wie erstellen Sie wirklich eine F # -Anwendung, die wartbar und entwicklungsfähig ist (ganz zu schweigen von testbar)?
Composition
Modul implementiert , im Wesentlichen als "Poor Man's DI" -Kompositionswurzel. Ist das ein gangbarer Weg?