Comment la réflexion sur les modèles de conception et les pratiques de POO change-t-elle dans les langues dynamiques et faiblement typées?

11

Il y a déjà une question assez utile dans ce sens (" Motifs de conception non-POO? "), Mais je suis plus curieux de savoir un point de vue transitionnel pour quelqu'un qui vient de commencer avec des langages dynamiques et faiblement typés.

C'est-à-dire: disons que je programme en C ++, C # ou Java depuis de nombreuses années et j'ai absorbé beaucoup de sagesse selon les modèles de conception du GoF, les modèles de Fowler de l' architecture d'application d'entreprise , les principes SOLID , etc. Je m'essaye à Ruby, Python, JavaScript, etc., et je me demande comment mes connaissances s'appliquent. Vraisemblablement, je pourrais faire des traductions directes dans de nombreux cas, mais presque certainement cela ne profiterait pas pleinement de mon nouveau cadre. Duck tapant à lui seul bouleverse ma pensée basée sur l'interface.

Qu'est-ce qui reste le même? Quels changements? Existe-t-il des principes directeurs comme SOLIDE ou des modèles canoniques (peut-être entièrement nouveaux) qu'un débutant en langage dynamique devrait connaître?

Domenic
la source

Réponses:

7

Qu'est-ce qui reste le même? Quels changements?

Les motifs sont les mêmes. Les techniques langagières changent.

Existe-t-il des principes directeurs comme SOLID,

Oui. En effet, ils restent les principes directeurs. Rien ne change.

ou des modèles canoniques (peut-être entièrement nouveaux) qu'un débutant en langage dynamique devrait connaître?

Certaines choses sont uniques. La plupart du temps, l'impact est que les techniques de mise en œuvre changent.

Un modèle est - enfin - un modèle . Pas une loi. Pas un sous-programme. Pas une macro. C'est juste une bonne idée qui se répète parce que c'est une bonne idée.

Les bonnes idées ne se démodent pas et ne changent pas de façon spectaculaire.

Autres notes. Python n'est pas "faiblement typé". Il est plus typé que Java ou C ++ car il n'y a pas d'opération de conversion. [Oui, il existe un moyen de truquer la classe associée à un objet, mais ce n'est pas le genre de chose qui est faite, sauf pour prouver un point difficile et légaliste.]

Également. La plupart des modèles de conception sont basés sur différentes manières d'exploiter le polymorphisme.

Regardez État ou Commandement ou Memento comme exemples. Ils ont des hiérarchies de classes pour créer un état polymorphe, des commandes ou des souvenirs de changements d'état. Rien ne change de manière significative lorsque vous effectuez cette opération en Python. Les changements mineurs incluent l'assouplissement de la hiérarchie de classe précise car le polymorphisme en Python dépend de méthodes communes et non d'ancêtres communs.

De plus, certains modèles sont simplement une tentative pour obtenir une liaison tardive. La plupart des modèles liés à Factory sont une tentative pour permettre une modification facile d'une hiérarchie de classes sans recompiler tous les modules C ++ de l'application. Ce n'est pas une optimisation aussi intéressante dans un langage dynamique. Cependant, une usine comme moyen de masquer les détails de l'implémentation a toujours une valeur énorme.

Certains modèles tentent de piloter le compilateur et l'éditeur de liens. Singleton , par exemple, existe pour créer des globaux déroutants mais au moins les encapsuler. Les cours de singleton en Python ne sont pas une perspective agréable. Mais les modules Python sont déjà des singletons, donc beaucoup d'entre nous utilisent simplement un module et évitent d'essayer de jouer avec une classe Singleton .

S.Lott
la source
Je ne dirais pas que "rien ne change" avec SOLID. Selon le langage et son modèle d'objet, le principe ouvert-fermé et le principe de substitution de Liskov peuvent tous deux être dénués de sens. (JavaScript et Go me viennent à l'esprit.)
Mason Wheeler
@Mason Wheeler. Open-Closed est une langue indépendante de mon expérience. Vous devrez fournir des exemples plus concrets de la façon dont la conception ouverte-fermée est «dénuée de sens» avec JavaScript ou Go. La substitution de Liskov, peut-être, ne s'applique pas à JavaScript, mais le motif essentiel - le polymorphisme - semble toujours s'appliquer.
S.Lott
@ S.Lott: Belles mises à jour dans l'édition; ils étaient beaucoup plus intéressants que la réponse originale: P. Merci d'avoir corrigé mon erreur Python. En général, les exemples de motifs spécifiques et leur lien avec les langages dynamiques, le polymorphisme, la liaison tardive, etc. sont parfaits.
Domenic
@ S.Lott: Parce que Open / Closed concerne l'héritage, que ces langues n'ont pas. (De plus, l'idée qu'un objet soit "fermé pour modification" ne conviendrait pas à beaucoup de codeurs Ruby ...)
Mason Wheeler
@Mason Wheeler: Merci pour la clarification sur Open / Closed. Je pense que l'exception JavaScript est importante, mais comme la question est tellement ouverte (listant JavaScript, Python et Ruby, ainsi qu'un langage appelé ETC), je ne sais pas comment traiter le cas spécial.
S.Lott
8

Peter Norvig s'est attaqué à cette question en 1998, lisez http://norvig.com/design-patterns/ppframe.htm pour un ensemble de choses détaillées qu'il a remarquées, et http://c2.com/cgi/wiki?AreDesignPatternsMissingLanguageFeatures pour poursuite de la discussion sur ce point.

La version courte est que lorsque votre langue a plus de fonctionnalités, les modèles de conception répétitifs ont tendance à devenir plus simples - souvent au point d'être invisibles. Il a constaté que cela était vrai pour la plupart des modèles de conception identifiés par le GoF.

btilly
la source
8

La programmation dans un langage orienté objet dynamique utilise plusieurs des mêmes modèles et principes, mais il existe certains ajustements et différences dus à l'environnement:

Remplacer les interfaces par Duck Typing - où le Gang of Four vous dirait d'utiliser une classe de base abstraite avec des fonctions virtuelles pures, et vous utiliseriez une interface en Java, dans un langage dynamique, vous n'avez besoin que d'une compréhension. Comme vous pouvez utiliser n'importe quel objet n'importe où et qu'il fonctionnera très bien s'il implémente les méthodes qui sont réellement appelées, vous n'avez pas besoin de définir une interface formelle. Il peut être utile d'en documenter un, afin de savoir clairement ce qui est réellement requis.

Les fonctions sont aussi des objets - Il existe de nombreux modèles qui séparent la décision de l'action; Commande, stratégie, chaîne de responsabilité, etc. Dans un langage avec des fonctions de première classe, il est souvent raisonnable de simplement passer une fonction au lieu de créer des objets avec des .doIt()méthodes. Ces modèles se transforment en «utiliser une fonction d'ordre supérieur».

VENDU - Le principe de ségrégation des interfaces est le plus touché ici, car il n'y a pas d'interfaces. Vous devriez toujours considérer le principe, mais vous ne pouvez pas le réifier dans votre code. Seule une vigilance personnelle vous protégera ici. Du côté positif, la douleur causée par la violation de ce principe est très réduite dans les environnements dynamiques courants.

"... dans mon propre ... idiome!" - Chaque langue a de bonnes pratiques et de mauvaises pratiques, et vous devrez les apprendre et les suivre si vous voulez le meilleur code dans ces langues. Un modèle d'itérateur parfaitement écrit peut être ri dans une langue avec des compréhensions de liste intégrées, par exemple.

Sean McMillan
la source
3

D'après mon expérience, certains Patterns sont toujours utiles en Python, et encore plus faciles à configurer que dans des langages plus statiques. Certains modèles OTOH ne sont tout simplement pas nécessaires, ou même désapprouvés, comme le modèle Singleton. Utilisez plutôt une variable ou une fonction de niveau module. Ou utilisez le modèle Borg.

Au lieu de configurer un modèle de création, il suffit souvent de passer un appelable qui crée des objets. Cela pourrait être une fonction, un objet avec une __call__méthode ou même une classe, car il n'y new()en a pas en Python, juste une invocation de la classe elle-même:

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)

State and Strategy Pattern partagent une structure très similaire dans des langages comme C ++ et Java. Moins en Python. Le modèle de stratégie reste plus ou moins le même, mais le modèle d'état devient généralement inutile. Le modèle d'état dans les langages statiques simule le changement de classe lors de l'exécution. En Python, vous pouvez faire exactement cela: changer la classe d'un objet au moment de l'exécution. Tant que vous le faites de manière contrôlée et encapsulée, tout devrait bien se passer:

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

Les modèles qui reposent sur la répartition de type statique ne fonctionneront pas ou fonctionneront de manière très différente. Vous n'avez pas à écrire autant de code de plaque de chaudière, par exemple Visitor Pattern: en Java et C ++, vous devez écrire une méthode accept dans chaque classe visitable, alors qu'en Python vous pouvez hériter de cette fonctionnalité via une classe mixin, comme Visitable:

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)

De nombreuses situations qui nécessitent l'application d'un modèle dans un langage statique ne le font pas autant en Python. Beaucoup de choses peuvent être résolues avec d'autres techniques, comme des fonctions d'ordre supérieur (décorateurs, usines de fonctions) ou des méta-classes.

pillmuncher
la source
Je me rends compte maintenant que votre réponse couvre la question que je viens de poser: est-ce que l' écrasement __class__pour implémenter une usine en Python est une bonne idée?
rds