Einige sagen, wenn Sie SOLID-Prinzipien auf die Spitze treiben, landen Sie in der funktionalen Programmierung . Ich bin mit diesem Artikel einverstanden, aber ich denke, dass einige Semantiken beim Übergang von der Schnittstelle / dem Objekt zur Funktion / zum Abschluss verloren gehen, und ich möchte wissen, wie funktionale Programmierung den Verlust mindern kann.
Aus dem Artikel:
Wenn Sie das Prinzip der Schnittstellentrennung (ISP - Interface Segregation Principle) konsequent anwenden, sollten Sie Rollenschnittstellen gegenüber Header-Schnittstellen bevorzugen.
Wenn Sie Ihr Design in Richtung kleinerer und kleinerer Schnittstellen weiterentwickeln, erhalten Sie schließlich die ultimative Rollenschnittstelle: eine Schnittstelle mit einer einzigen Methode. Das passiert mir sehr oft. Hier ist ein Beispiel:
public interface IMessageQuery
{
string Read(int id);
}
Wenn ich eine Abhängigkeit von einem annehme, besteht ein IMessageQuery
Teil des impliziten Vertrages darin, dass ein Anruf Read(id)
nach einer Nachricht mit der angegebenen ID sucht und diese zurückgibt.
Vergleichen Sie dies damit, dass Sie eine Abhängigkeit von der entsprechenden funktionalen Signatur annehmen int -> string
. Ohne zusätzliche Hinweise könnte diese Funktion eine einfache sein ToString()
. Wenn Sie IMessageQuery.Read(int id)
mit einem implementiert haben, ToString()
kann ich Sie beschuldigen, absichtlich subversiv zu sein!
Was können funktionierende Programmierer also tun, um die Semantik einer benannten Schnittstelle beizubehalten? Ist es üblich, beispielsweise einen Datensatztyp mit einem einzelnen Element zu erstellen?
type MessageQuery = {
Read: int -> string
}
quelle
Without any additional clues
... vielleicht ist die Dokumentation deshalb Bestandteil des Vertrages ?Antworten:
Wie Telastyn sagt, vergleichen Sie die statischen Definitionen von Funktionen:
zu
Sie haben von OOP bis FP nichts wirklich verloren.
Dies ist jedoch nur ein Teil der Geschichte, da Funktionen und Schnittstellen nicht nur in ihren statischen Definitionen erwähnt werden. Sie werden auch herumgereicht . Angenommen, unser
MessageQuery
Code wurde von einem anderen Code gelesen: aMessageProcessor
. Dann haben wir:Jetzt können wir den Methodennamen oder dessen Parameter nicht direkt sehen , aber wir können es sehr einfach über unsere IDE erreichen. Im Allgemeinen bedeutet die Tatsache, dass wir nicht nur eine Schnittstelle mit einer Methode, sondern eine Funktion von int an string übergeben, dass wir die dieser Funktion zugeordneten Metadaten des Parameternamens beibehalten.
IMessageQuery.Read
int id
IMessageQuery
id
Andererseits haben wir für unsere funktionale Version:
Was haben wir also behalten und verloren? Nun, wir haben immer noch den Parameternamen
messageReader
, was wahrscheinlich den Typnamen (das Äquivalent zuIMessageQuery
) überflüssig macht. Aber jetzt haben wir den Parameternamenid
in unserer Funktion verloren.Es gibt zwei Hauptwege, um dies zu umgehen:
Erstens können Sie beim Lesen dieser Unterschrift bereits ziemlich gut erraten, was passieren wird. Indem Sie die Funktionen kurz, einfach und zusammenhängend halten und eine gute Benennung verwenden, können Sie diese Informationen viel einfacher erkennen oder finden. Sobald wir die eigentliche Funktion gelesen haben, ist sie noch einfacher.
Zweitens wird es in vielen funktionalen Sprachen als idiomatisches Design angesehen , kleine Typen zum Umschließen von Primitiven zu erstellen. In diesem Fall ist das Gegenteil der Fall - anstatt einen Typnamen durch einen Parameternamen (
IMessageQuery
bismessageReader
) zu ersetzen, können wir einen Parameternamen durch einen Typnamen ersetzen. Könnte zum Beispielint
in einen Typ mit dem Namen gewickelt werdenId
:Jetzt wird unsere
read
Unterschrift:Welches ist genauso informativ wie das, was wir zuvor hatten.
Als Randnotiz bietet uns dies auch einen Teil des Compilerschutzes, den wir in OOP hatten. Während die OOP-Version sicherstellte, dass wir
IMessageQuery
nicht nur irgendeine alteint -> string
Funktion übernommen haben, haben wir hier einen ähnlichen (aber unterschiedlichen) Schutz, den wirId -> string
anstelle irgendeiner alten verwendenint -> string
.Ich würde nur ungern mit hundertprozentiger Sicherheit sagen, dass diese Techniken immer genauso gut und informativ sind wie die vollständigen Informationen, die auf einer Benutzeroberfläche verfügbar sind, aber ich denke, anhand der obigen Beispiele können Sie sagen, dass wir dies die meiste Zeit tun kann wahrscheinlich genauso gut arbeiten.
quelle
Wenn ich FP mache, neige ich dazu, spezifischere semantische Typen zu verwenden.
Zum Beispiel würde Ihre Methode für mich ungefähr so aussehen:
Dies kommuniziert viel mehr als der OO (/ java)
ThingDoer.doThing()
-Stilquelle
read: MessageId -> Message
sagt dir dasstring MessageReader.GetMessage(int messageId)
nicht?Verwenden Sie gut benannte Funktionen.
IMessageQuery::Read: int -> string
wird einfachReadMessageQuery: int -> string
oder so ähnlich.Das Wichtigste ist, dass Namen nur Verträge im weitesten Sinne des Wortes sind. Sie funktionieren nur, wenn Sie und ein anderer Programmierer dieselben Implikationen aus dem Namen ableiten und ihnen gehorchen. Aus diesem Grund können Sie wirklich jeden Namen verwenden, der dieses implizite Verhalten kommuniziert. OO und funktionale Programmierung haben ihre Namen an leicht unterschiedlichen Stellen und in leicht unterschiedlichen Formen, aber ihre Funktion ist dieselbe.
Nicht in diesem Beispiel. Wie ich oben erklärt habe, ist eine einzelne Klasse / Schnittstelle mit einer einzelnen Funktion nicht aussagekräftiger als eine ähnlich gut benannte Standalone-Funktion.
Sobald Sie mehr als eine Funktion / ein Feld / eine Eigenschaft in einer Klasse erhalten, können Sie weitere Informationen über diese ableiten, da Sie deren Beziehung sehen können. Es ist fraglich, ob dies informativer ist als eigenständige Funktionen, die dieselben / ähnliche Parameter oder eigenständige Funktionen verwenden, die nach Namespace oder Modul organisiert sind.
Persönlich glaube ich nicht, dass OO selbst in komplexeren Beispielen wesentlich informativer ist.
quelle
ReadMessageQuery id = <code to go fetch based on id>
let consumer (messageQuery : int -> string) = messageQuery 5
ist derid
Parameter beispielsweise nur einint
. Ich denke, ein Argument ist, dass Sie eineId
, nicht eine übergeben solltenint
. In der Tat wäre das eine anständige Antwort für sich.Ich stimme nicht zu, dass eine einzelne Funktion keinen "semantischen Vertrag" haben kann. Betrachten Sie diese Gesetze für
foldr
:Inwiefern ist das keine Semantik oder kein Vertrag? Sie müssen keinen Typ für einen "Foldrer" definieren, insbesondere, weil er
foldr
durch diese Gesetze eindeutig bestimmt wird. Sie wissen genau, was es tun wird.Wenn Sie eine Funktion eines Typs aufnehmen möchten, können Sie dasselbe tun:
Sie müssen diesen Typ nur benennen und erfassen, wenn Sie denselben Vertrag mehrmals benötigen:
Die Typprüfung erzwingt nicht die Semantik, die Sie einem Typ zuweisen. Die Erstellung eines neuen Typs für jeden Vertrag ist also nur ein Boilerplate.
quelle
foldr
. Hinweis: Die Definition vonfoldr
hat zwei Gleichungen (warum?), Während die oben angegebene Spezifikation drei hat.Nahezu alle statisch typisierten funktionalen Sprachen haben eine Möglichkeit, Basistypen so zu aliasisieren, dass Sie Ihre semantische Absicht explizit deklarieren müssen. Einige der anderen Antworten haben Beispiele gegeben. In der Praxis benötigen erfahrene Funktionsprogrammierer sehr gute Gründe für die Verwendung dieser Wrapper-Typen, da sie die Kompositionsfähigkeit und Wiederverwendbarkeit beeinträchtigen.
Angenommen, ein Client wollte eine Implementierung einer Nachrichtenabfrage, die von einer Liste unterstützt wurde. In Haskell könnte die Implementierung so einfach sein:
Mit
newtype Message = Message String
dieser wäre viel weniger einfach sein, so dass diese Implementierung sucht so etwas wie:Das mag nicht so schlimm erscheinen, aber entweder müssen Sie diese Art der Konvertierung überall hin und her machen , oder Sie richten eine Grenzschicht in Ihrem Code ein, in die alles, was darüber liegt
Int -> String
, konvertiert wirdId -> Message
, um zur darunter liegenden Schicht überzugehen. Angenommen, ich wollte Internationalisierung hinzufügen oder in Großbuchstaben formatieren oder den Protokollierungskontext hinzufügen, oder was auch immer. Diese Operationen sind alle kinderleicht mit einem zu komponierenInt -> String
und nerven mit einemId -> Message
. Es ist nicht so, dass es niemals Fälle gibt, in denen die erhöhten Typeinschränkungen wünschenswert sind, aber die Belästigung sollte den Kompromiss wert sein.Sie können ein Typ- Synonym anstelle eines Wrappers (in Haskell
type
anstelle vonnewtype
) verwenden. Dies ist weitaus häufiger der Fall und erfordert keine allumfassenden Konvertierungen, bietet jedoch keine statischen Typgarantien wie Ihre OOP-Version. nur ein bisschen Verkapselung. Typ - Wrapper wird meist verwendet , wenn der Client nicht erwartet , dass der Wert überhaupt zu manipulieren, es ist einfach zu speichern und weitergeben zurück. Zum Beispiel ein Datei-Handle.Nichts kann einen Kunden davon abhalten, "subversiv" zu sein. Sie erstellen nur Rahmen, durch die Sie springen können, für jeden einzelnen Kunden. Ein einfacher Schein für einen allgemeinen Komponententest erfordert oft seltsames Verhalten, das in der Produktion keinen Sinn ergibt. Deine Interfaces sollten so geschrieben sein, dass es ihnen egal ist, wenn es überhaupt möglich ist.
quelle
Id
undMessage
einfache Wrapper fürInt
und sindString
, ist es trivial , zwischen ihnen zu konvertieren.