Im vergangenen Jahr habe ich den Sprung gewagt und eine funktionale Programmiersprache (F #) gelernt. Eines der interessantesten Dinge, die ich gefunden habe, ist, wie sich dies auf die Art und Weise auswirkt, wie ich OO-Software entwerfe. Die beiden Dinge, die mir in OO-Sprachen am meisten fehlen, sind Mustervergleich und Summentypen. Überall, wo ich hinschaue, sehe ich Situationen, die mit einer diskriminierten Gewerkschaft trivial modelliert würden, aber ich zögere es, bei einer OO DU-Implementierung, die sich für das Paradigma unnatürlich anfühlt, eine Brechstange zu bilden.
Dies führt mich im Allgemeinen dazu, Zwischentypen zu erstellen, um die or
Beziehungen zu behandeln, die ein Summentyp für mich behandeln würde. Es scheint auch zu einer starken Verzweigung zu führen. Wenn ich Leute wie Misko Hevery lese , schlägt er vor, dass ein gutes OO-Design die Verzweigung durch Polymorphismus minimieren kann.
Eines der Dinge, die ich im OO-Code so weit wie möglich vermeide, sind Typen mit null
Werten. Natürlich kann die or
Beziehung durch einen Typ mit einem null
Wert und einem Nichtwert modelliert werden null
, aber dies bedeutet null
Tests überall. Gibt es eine Möglichkeit, heterogene, aber logisch assoziierte Typen polymorph zu modellieren? Entwurfsstrategien oder -muster wären sehr hilfreich oder einfach Möglichkeiten, über heterogene und assoziierte Typen im Allgemeinen im OO-Paradigma nachzudenken.
quelle
Antworten:
Wie Sie wünsche ich mir, dass diskriminierte Gewerkschaften häufiger auftreten. Der Grund, warum sie in den meisten funktionalen Sprachen nützlich sind, ist, dass sie einen umfassenden Mustervergleich bieten. Ohne diesen sind sie nur eine hübsche Syntax: nicht nur ein Mustervergleich: ein erschöpfender Mustervergleich, sodass der Code nicht kompiliert wird, wenn Sie ihn nicht verwenden. Nicht alle Möglichkeiten abdecken: Dies gibt Ihnen Kraft.
Die einzige Möglichkeit, mit einem Summentyp etwas Nützliches zu tun, besteht darin, ihn zu zerlegen und je nach Typ zu verzweigen (z. B. durch Mustervergleich). Das Tolle an Schnittstellen ist, dass es Ihnen egal ist, um welchen Typ es sich handelt, denn Sie wissen, dass Sie es wie folgt behandeln können
iface
: Keine eindeutige Logik für jeden Typ erforderlich: keine Verzweigung.Dies ist kein "Funktionscode hat mehr Verzweigungen, OO-Code hat weniger", dies ist eine "Funktionssprachen", die besser für Domänen geeignet sind, in denen Sie Gewerkschaften haben - die eine Verzweigung vorschreiben - und "OO-Sprachen" sind besser für Code geeignet Hier können Sie allgemeines Verhalten als gemeinsame Schnittstelle verfügbar machen - was sich möglicherweise weniger verzweigt anfühlt. " Die Verzweigung ist eine Funktion Ihres Designs und der Domäne. Ganz einfach, wenn Ihre "heterogenen, aber logisch zugeordneten Typen" keine gemeinsame Schnittstelle verfügbar machen können, müssen Sie über sie verzweigen / Muster abgleichen. Dies ist ein Domain- / Designproblem.
Was Misko gemeint sein kann , ist die allgemeine Vorstellung , dass , wenn Sie können Ihre Typen als gemeinsame Schnittstelle aussetzen, dann mit OO - Funktionen (Schnittstellen / Polymorphismus) Ihr Leben besser typspezifische Verhalten machen , indem sie in der Art statt in der verzehr Code.
Es ist wichtig zu erkennen, dass Schnittstellen und Gewerkschaften das Gegenteil voneinander sind: Eine Schnittstelle definiert einige Dinge, die der Typ implementieren muss, und die Gewerkschaft definiert einige Dinge, die der Verbraucher berücksichtigen muss. Wenn Sie einer Schnittstelle eine Methode hinzufügen, haben Sie diesen Vertrag geändert, und jetzt muss jeder zuvor implementierte Typ aktualisiert werden. Wenn Sie einer Gewerkschaft einen neuen Typ hinzufügen, haben Sie diesen Vertrag geändert, und jetzt muss jedes umfassende Muster, das über die Gewerkschaft hinweg übereinstimmt , aktualisiert werden. Sie besetzen unterschiedliche Rollen, und obwohl es manchmal möglich ist, ein System in beide Richtungen zu implementieren, ist dies eine Entwurfsentscheidung: Beides ist von Natur aus nicht besser.
Ein Vorteil von Interfaces / Polymorphismus besteht darin, dass der konsumierende Code erweiterbarer ist: Sie können einen Typ übergeben, der zur Entwurfszeit nicht definiert wurde, solange die vereinbarte Schnittstelle verfügbar gemacht wird. Auf der anderen Seite können Sie mit einer statischen Vereinigung Verhaltensweisen ausnutzen, die zur Entwurfszeit nicht berücksichtigt wurden, indem Sie neue umfassende Musterübereinstimmungen schreiben, solange sie sich an den Vertrag der Vereinigung halten.
In Bezug auf die ‚Pattern Null - Objekt‘: dies ist nicht kein Allheilmittel, und hat nicht ersetzen
null
Schecks. Alles, was es tut, bietet eine Möglichkeit, einige "Null" -Prüfungen zu vermeiden, bei denen das "Null" -Verhalten hinter einer gemeinsamen Schnittstelle angezeigt werden kann. Wenn Sie das "Null" -Verhalten hinter der Schnittstelle des Typs nicht offenlegen können, werden Sie denken "Ich wünschte wirklich, ich könnte dieses Muster vollständig anpassen" und am Ende eine "Verzweigungs" -Prüfung durchführen.quelle
Es gibt eine ziemlich "Standard" -Methode zum Codieren von Summentypen in eine objektorientierte Sprache.
Hier sind zwei Beispiele:
In C # könnten wir dies wie folgt rendern:
F # nochmal:
Wieder C #:
Die Codierung ist vollständig mechanisch. Diese Codierung führt zu einem Ergebnis, das die meisten Vor- und Nachteile algebraischer Datentypen aufweist. Sie können dies auch als Variation des Besuchermusters erkennen. Wir könnten die Parameter
Match
zusammen in einer Schnittstelle sammeln, die wir als Besucher bezeichnen könnten.Auf der Vorteilsseite erhalten Sie eine prinzipielle Codierung von Summentypen. (Es ist die Scott-Codierung .) Sie erhalten einen umfassenden "Mustervergleich", obwohl jeweils nur eine "Ebene" des Abgleichs vorhanden ist.
Match
ist in gewisser Weise eine "vollständige" Schnittstelle für diese Typen, und alle zusätzlichen Operationen, die wir möglicherweise wünschen, können in Bezug darauf definiert werden. Es bietet eine andere Perspektive auf viele OO-Muster wie das Null-Objektmuster und das Zustandsmuster, wie ich in Ryathals Antwort angegeben habe, sowie das Besuchermuster und das zusammengesetzte Muster. Der TypOption
/Maybe
ähnelt einem generischen Nullobjektmuster. Das zusammengesetzte Muster ähnelt der Codierungtype Tree<'a> = Leaf of 'a | Children of List<Tree<'a>>
. Das Zustandsmuster ist im Grunde eine Kodierung einer Aufzählung.Auf der Nachteilsseite, wie ich es geschrieben habe
Match
, legt die Methode einige Einschränkungen fest, welche Unterklassen sinnvoll hinzugefügt werden können, insbesondere wenn wir die Liskov-Substituierbarkeitseigenschaft beibehalten möchten. Wenn Sie diese Codierung beispielsweise auf einen Aufzählungstyp anwenden, können Sie die Aufzählung nicht sinnvoll erweitern. Wenn Sie die Aufzählung erweitern möchten, müssen Sie alle Aufrufer und Implementierer überall so ändern, als würden Sieenum
und verwendenswitch
. Diese Codierung ist jedoch etwas flexibler als das Original. Zum Beispiel können wir einenAppend
Implementierer hinzufügen , derList
nur zwei Listen enthält, die uns einen zeitlich konstanten Anhang geben. Dies würde sich wie die zusammengehängten Listen verhalten, aber auf andere Weise dargestellt werden.Natürlich haben viele dieser Probleme damit zu tun, dass sie
Match
etwas (konzeptionell, aber absichtlich) an die Unterklassen gebunden sind. Wenn wir Methoden verwenden, die nicht so spezifisch sind, erhalten wir traditionellere OO-Designs und wir gewinnen die Erweiterbarkeit wieder, aber wir verlieren die "Vollständigkeit" der Schnittstelle und damit die Fähigkeit, Operationen für diesen Typ in Bezug auf die zu definieren Schnittstelle. Wie an anderer Stelle erwähnt, ist dies eine Manifestation des Ausdrucksproblems .Möglicherweise können Designs wie das oben genannte systematisch verwendet werden, um die Notwendigkeit einer Verzweigung, die jemals ein OO-Ideal erreicht, vollständig zu eliminieren. Smalltalk verwendet dieses Muster beispielsweise häufig, auch für Boolesche Werte. Aber wie die vorangegangene Diskussion nahe legt, ist diese "Beseitigung der Verzweigung" ziemlich illusorisch. Wir haben die Verzweigung gerade auf eine andere Art und Weise implementiert und sie hat immer noch viele der gleichen Eigenschaften.
quelle
Die Behandlung von Null kann mit dem Null-Objektmuster erfolgen . Die Idee ist, eine Instanz Ihrer Objekte zu erstellen, die Standardwerte für jedes Mitglied zurückgibt und Methoden enthält, die nichts tun, aber auch keine Fehler verursachen. Dadurch werden Nullprüfungen nicht vollständig eliminiert, sondern Sie müssen nur bei der Objekterstellung nach Nullen suchen und Ihr Nullobjekt zurückgeben.
Das Zustandsmuster ist eine Möglichkeit, die Verzweigung zu minimieren und einige der Vorteile des Mustervergleichs aufzuzeigen. Wiederum wird die Verzweigungslogik zur Objekterstellung verschoben. Jeder Status ist eine separate Implementierung einer Basisschnittstelle, sodass der gesamte verbrauchende Code nur DoStuff () aufrufen muss und die richtige Methode aufgerufen wird. Einige Sprachen fügen auch Pattern Matching als Feature hinzu, C # ist ein Beispiel.
quelle