Wie verändert sich das Denken über Entwurfsmuster und OOP-Praktiken in dynamischen und schwach typisierten Sprachen?

11

In diesem Sinne gibt es bereits eine ziemlich hilfreiche Frage (" Nicht-OOP-Entwurfsmuster? "), Aber ich bin neugieriger auf eine Übergangsperspektive für jemanden, der gerade erst mit dynamischen und schwach typisierten Sprachen anfängt.

Das heißt: Nehmen wir an, ich programmiere seit vielen Jahren in C ++, C # oder Java und habe viel Weisheit in Anlehnung an die GoF-Entwurfsmuster, Fowlers Muster der Unternehmensanwendungsarchitektur , SOLID-Prinzipien usw. aufgenommen. Ich beschäftige mich mit Ruby, Python, JavaScript usw. und frage mich, wie mein Wissen zutrifft. Vermutlich könnte ich in vielen Fällen direkte Übersetzungen machen, aber mit ziemlicher Sicherheit würde das meine neue Einstellung nicht voll ausnutzen. Allein das Tippen von Enten stellt einen Großteil meines schnittstellenbasierten Denkens auf den Kopf.

Was bleibt gleich? Was ändert sich? Gibt es Leitprinzipien wie SOLID oder kanonische Muster (vielleicht völlig neue), die ein Neuling in einer dynamischen Sprache kennen sollte?

Domenic
quelle

Antworten:

7

Was bleibt gleich? Was ändert sich?

Die Muster sind die gleichen. Die Sprachtechniken ändern sich.

Gibt es Leitprinzipien wie SOLID,

Ja. In der Tat bleiben sie die Leitprinzipien. Nichts verändert sich.

oder kanonische Muster (vielleicht völlig neue), die ein Neuling in einer dynamischen Sprache kennen sollte?

Einige Dinge sind einzigartig. Die Auswirkung besteht hauptsächlich darin, dass sich die Implementierungstechniken ändern.

Ein Muster ist - na ja - ein Muster . Kein Gesetz. Kein Unterprogramm. Kein Makro. Es ist nur eine gute Idee, die wiederholt wird, weil es eine gute Idee ist.

Gute Ideen kommen nicht aus der Mode oder ändern sich dramatisch.

Weitere Hinweise. Python ist nicht "schwach typisiert". Es ist stärker typisiert als Java oder C ++, da es keine Cast-Operation gibt. [Ja, es gibt eine Möglichkeit, die mit einem Objekt verknüpfte Klasse zu verfälschen, aber es ist nichts, was getan wird, außer um einen pingeligen, legalistischen Punkt zu beweisen.]

Ebenfalls. Die meisten Entwurfsmuster basieren auf verschiedenen Möglichkeiten, den Polymorphismus auszunutzen.

Schauen Sie sich als Beispiele State oder Command oder Memento an. Sie haben Klassenhierarchien, um polymorphe Zustände, Befehle oder Andenken an Zustandsänderungen zu erstellen. Wenn Sie dies in Python tun, ändert sich nichts wesentlich. Zu den geringfügigen Änderungen gehört die Lockerung der genauen Klassenhierarchie, da der Polymorphismus in Python von gängigen Methoden abhängt, nicht von gemeinsamen Vorfahren.

Einige Muster sind auch nur ein Versuch, eine späte Bindung zu erreichen. Die meisten Factory- bezogenen Muster sind ein Versuch, eine einfache Änderung einer Klassenhierarchie zu ermöglichen, ohne jedes C ++ - Modul in der Anwendung neu zu kompilieren. Dies ist keine so interessante Optimierung in einer dynamischen Sprache. Eine Factory als Möglichkeit, Implementierungsdetails zu verbergen, hat jedoch immer noch einen enormen Wert.

Einige Muster sind ein Versuch, den Compiler und den Linker anzutreiben. Singleton existiert zum Beispiel, um verwirrende Globals zu erstellen, diese aber zumindest zu kapseln. Python-Singleton-Klassen sind keine angenehme Aussicht. Aber Python-Module sind bereits Singletons, daher verwenden viele von uns nur ein Modul und vermeiden es, sich mit einer Singleton- Klasse herumzuschlagen .

S.Lott
quelle
Ich würde nicht sagen, dass sich mit SOLID "nichts ändert". Abhängig von der Sprache und ihrem Objektmodell können sowohl das Open-Closed-Prinzip als auch das Liskov-Substitutionsprinzip bedeutungslos sein. (JavaScript und Go kommen beide in den Sinn.)
Mason Wheeler
@ Mason Wheeler. Open-Closed ist meiner Erfahrung nach sprachunabhängig. Sie müssen einige konkretere Beispiele dafür liefern, wie Open-Closed-Design mit JavaScript oder Go "bedeutungslos" ist. Die Liskov-Substitution gilt möglicherweise nicht für JavaScript, aber das wesentliche Muster - Polymorphismus - scheint immer noch zuzutreffen.
S.Lott
@ S.Lott: Schöne Updates in der Bearbeitung; Sie waren viel interessanter als die ursprüngliche Antwort: P. Vielen Dank, dass Sie meinen Python-Fehler korrigiert haben. Im Allgemeinen sind die spezifischen Musterbeispiele und ihre Verknüpfung mit dynamischen Sprachen, Polymorphismus, Spätbindung usw. perfekt.
Domenic
@ S.Lott: Weil es bei Open / Closed um Vererbung geht, die diese Sprachen nicht haben. (Auch die Idee, dass ein Objekt "wegen Modifikation geschlossen" wird, passt nicht gut zu vielen Ruby-Codierern ...)
Mason Wheeler
@ Mason Wheeler: Danke für die Klarstellung zu Open / Closed. Ich denke, die JavaScript-Ausnahme ist wichtig, aber da die Frage so offen ist (Auflistung von JavaScript, Python und Ruby sowie einer Sprache namens ETC), bin ich mir nicht sicher, wie ich den Sonderfall angehen soll.
S.Lott
8

Peter Norvig hat sich 1998 genau dieser Frage gestellt. Lesen Sie http://norvig.com/design-patterns/ppframe.htm, um eine Reihe detaillierter Informationen zu erhalten, und http://c2.com/cgi/wiki?AreDesignPatternsMissingLanguageFeatures for weitere Diskussion um den Punkt.

Die kurze Version besagt, dass sich wiederholende Entwurfsmuster einfacher werden, wenn Ihre Sprache mehr Funktionen enthält - oft bis sie unsichtbar sind. Er stellte fest, dass dies für die meisten von der GoF identifizierten Entwurfsmuster zutraf.

Übrigens
quelle
8

Das Programmieren in einer dynamischen objektorientierten Sprache verwendet viele der gleichen Muster und Prinzipien, aber es gibt bestimmte Verbesserungen und Unterschiede aufgrund der Umgebung:

Ersetzen Sie Schnittstellen durch Ententypisierung - Wenn die Viererbande Sie auffordert, eine abstrakte Basisklasse mit rein virtuellen Funktionen zu verwenden, und Sie eine Schnittstelle in Java in einer dynamischen Sprache verwenden, benötigen Sie nur ein Verständnis. Da Sie jedes Objekt überall verwenden können und es einwandfrei funktioniert, wenn es die tatsächlich aufgerufenen Methoden implementiert, müssen Sie keine formale Schnittstelle definieren. Es kann sich lohnen , eine zu dokumentieren , damit klar ist, was tatsächlich erforderlich ist.

Funktionen sind auch Objekte - Es gibt viele Muster, bei denen es darum geht, Entscheidung von Aktion zu trennen. Befehl, Strategie, Verantwortungskette usw. In einer Sprache mit erstklassigen Funktionen ist es oft sinnvoll, eine Funktion einfach weiterzugeben, anstatt Objekte mit .doIt()Methoden zu erstellen. Diese Muster wandeln sich in "eine Funktion höherer Ordnung verwenden" um.

VERKAUFT - Das Prinzip der Schnittstellentrennung ist hier am stärksten betroffen, da keine Schnittstellen vorhanden sind. Sie sollten das Prinzip weiterhin berücksichtigen, es jedoch nicht in Ihren Code umwandeln. Nur persönliche Wachsamkeit schützt Sie hier. Auf der anderen Seite ist der Schmerz, der durch die Verletzung dieses Prinzips verursacht wird, in üblichen dynamischen Umgebungen stark reduziert.

"... in meiner eigenen besonderen ... Redewendung!" - Jede Sprache hat gute und schlechte Praktiken, und Sie müssen sie lernen und befolgen, wenn Sie den besten Code in diesen Sprachen wünschen. Ein perfekt geschriebenes Iteratormuster kann beispielsweise in einer Sprache mit integriertem Listenverständnis ausgelacht werden.

Sean McMillan
quelle
3

Nach meiner Erfahrung sind einige Muster in Python immer noch nützlich und noch einfacher einzurichten als in statischeren Sprachen. Einige Patterns OTOH werden einfach nicht benötigt oder sind sogar verpönt, wie das Singleton-Pattern. Verwenden Sie stattdessen eine Variable oder Funktion auf Modulebene. Oder verwenden Sie das Borg-Muster.

Anstatt ein Erstellungsmuster einzurichten, reicht es oft aus, ein aufrufbares Muster zu übergeben, das Objekte erstellt. Das kann eine Funktion, ein Objekt mit einer __call__Methode oder sogar eine Klasse sein, da es new()in Python keine gibt , nur einen Aufruf der Klasse selbst:

def make_da_thing(maker, other, stuff):
    da_thing = maker(other + 1, stuff + 2)
    # ... do sth
    return da_thing

def maker_func(x, y):
     return x * y

class MakerClass(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
...
a = make_da_thing(maker_func, 5, 8)
b = make_da_thing(MakerClass, 6, 7)

Status- und Strategiemuster haben in Sprachen wie C ++ und Java eine sehr ähnliche Struktur. Weniger in Python. Das Strategiemuster bleibt mehr oder weniger gleich, aber das Zustandsmuster wird größtenteils unnötig. Das Zustandsmuster in statischen Sprachen simuliert den Klassenwechsel zur Laufzeit. In Python können Sie genau das tun: Ändern Sie die Klasse eines Objekts zur Laufzeit. Solange Sie dies kontrolliert und gekapselt tun, sollte es Ihnen gut gehen:

class On(object):
    is_on = True
    def switch(self):
        self.__class__ = Off

class Off(object):
    is_on = False
    def switch(self):
        self.__class__ = On
...

my_switch = On()
assert my_switch.is_on
my_switch.switch()
assert not my_switch.is_on

Muster, die auf statischem Typversand basieren, funktionieren nicht oder ganz anders. Sie müssen nicht so viel Code für die Kesselplatte schreiben, z. B. Besuchermuster: In Java und C ++ müssen Sie in jeder besuchbaren Klasse eine Akzeptanzmethode schreiben, während Sie in Python diese Funktionalität über eine Mixin-Klasse wie Visitable erben können:

class Visitable(object):
    def accept(self, visitor):
        visit = getattr(visitor, 'visit' + self.__class__.__name__)
        return visit(self)
...

class On(Visitable):
    ''' exactly like above '''

class Off(Visitable):
    ''' exactly like above '''

class SwitchStatePrinter(object): # Visitor
    def visitOn(self, switch):
         print 'the switch is on'
    def visitOff(self, switch):
         print 'the switch is off'

class SwitchAllOff(object): # Visitor
    def visitOn(self, switch):
         switch.switch()
    def visitOff(self, switch):
         pass
...
print_state = SwitchStatePrinter()
turn_em_off = SwitchAllOff()
for each in my_switches:
    each.accept(print_state)
    each.accept(turn_em_off)

Viele Situationen, in denen ein Muster in einer statischen Sprache angewendet werden muss, tun dies in Python weniger. Viele Dinge können mit anderen Techniken gelöst werden, wie Funktionen höherer Ordnung (Dekoratoren, Funktionsfabriken) oder Metaklassen.

Pillmuncher
quelle
Mir ist jetzt klar, dass Ihre Antwort hauptsächlich die Frage abdeckt, die ich gerade gestellt habe: Ist das Überschreiben __class__zur Implementierung einer Factory in Python eine gute Idee?
1.