Como o pensamento sobre padrões de design e práticas de POO muda em linguagens dinâmicas e com tipos fracos?

11

Já existe uma pergunta bastante útil nesse sentido (" Non-OOP Design Patterns? "), Mas estou mais curioso sobre um ponto de vista transitório para alguém que está apenas começando com linguagens dinâmicas e de tipo fraco.

Ou seja: digamos que eu programa em C ++, C # ou Java há muitos anos, e absorvi muita sabedoria ao longo dos padrões de design do GoF, Padrões de arquitetura de aplicativos empresariais de Fowler , princípios SOLID , etc. estou mexendo em Ruby, Python, JavaScript etc. e imaginando como meu conhecimento se aplica. Presumivelmente, eu poderia fazer traduções diretas em muitos casos, mas quase certamente isso não tiraria o máximo proveito da minha nova configuração. A digitação com pato, por si só, transforma muito do meu pensamento baseado em interface.

O que permanece o mesmo? O que muda? Existem princípios orientadores, como o SOLID, ou padrões canônicos (talvez inteiramente novos) que um novato em linguagem dinâmica deve conhecer?

Domenic
fonte

Respostas:

7

O que permanece o mesmo? O que muda?

Os padrões são os mesmos. As técnicas de linguagem mudam.

Existem princípios orientadores como o SOLID,

Sim. Na verdade, eles continuam sendo os princípios orientadores. Nada muda.

ou padrões canônicos (talvez inteiramente novos) que um novato em linguagem dinâmica deveria conhecer?

Algumas coisas são únicas. Principalmente o impacto é que as técnicas de implementação mudam.

Um padrão é - bem - um padrão . Não é uma lei. Não é uma sub-rotina. Não é uma macro. É apenas uma boa ideia que se repete porque é uma boa ideia.

Boas idéias não saem de moda nem mudam drasticamente.

Outras notas. Python não é "fracamente digitado". É mais tipificado do que Java ou C ++ porque não há operação de conversão. [Sim, existe uma maneira de falsificar a classe associada a um objeto, mas não é o tipo de coisa que é feita, exceto para provar um ponto exigente e legalista.]

Além disso. A maioria dos padrões de design é baseada em diferentes maneiras de explorar o polimorfismo.

Olhe para Estado ou de comando ou Memento como exemplos. Eles têm hierarquias de classe para criar estados polimórficos, comandos ou lembranças de mudanças de estado. Nada muda significativamente quando você faz isso no Python. Pequenas mudanças incluem o relaxamento da hierarquia precisa de classes, porque o polimorfismo no Python depende de métodos comuns e não de ancestrais comuns.

Além disso, alguns padrões são simplesmente uma tentativa de obter ligação tardia. A maioria dos padrões relacionados à fábrica é uma tentativa de permitir alterações fáceis em uma hierarquia de classes sem recompilar todos os módulos C ++ no aplicativo. Essa otimização não é tão interessante em uma linguagem dinâmica. No entanto, uma fábrica como forma de ocultar detalhes de implementação ainda tem um valor enorme.

Alguns padrões são uma tentativa de conduzir o compilador e o vinculador. Singleton , por exemplo, existe para criar globais confusos, mas pelo menos os encapsula. As classes singleton em Python não são uma perspectiva agradável. Mas os módulos Python já são singletons, muitos de nós apenas usamos um módulo e evitamos tentar mexer com uma classe Singleton .

S.Lott
fonte
Eu não diria que "nada muda" com o SOLID. Dependendo da linguagem e seu modelo de objeto, o Princípio Aberto-Fechado e o Princípio de Substituição de Liskov podem não ter sentido. (JavaScript and Go ambos vêm à mente.)
Mason Wheeler
@Mason Wheeler. Aberto-fechado é independente de idioma na minha experiência. Você precisará fornecer alguns exemplos mais concretos de como o design aberto-fechado é "sem sentido" com JavaScript ou Go. A substituição de Liskov, talvez, não se aplica ao JavaScript, mas o padrão essencial - polimorfismo - ainda parece se aplicar.
precisa saber é o seguinte
@ S.Lott: Boas atualizações na edição; eles eram muito mais interessantes que a resposta original: p. Obrigado por corrigir meu erro de Python. Em geral, os exemplos de padrões específicos e como eles se ligam a linguagens dinâmicas, polimorfismo, ligação tardia etc. são perfeitos.
Domenic
@ S.Lott: Porque Aberto / Fechado é sobre herança, que esses idiomas não têm. (Além disso, a idéia de um objeto a ser "fechado para modificação" não se coaduna com um monte de programadores de Ruby ...)
Mason Wheeler
@ Mason Wheeler: Obrigado pelo esclarecimento sobre Aberto / Fechado. Acho que a exceção do JavaScript é importante, mas como a pergunta é muito aberta (listando JavaScript, Python e Ruby, além de uma linguagem chamada ETC), não sei como resolver o caso especial.
S.Lott
8

Peter Norvig abordou essa questão em 1998, leia http://norvig.com/design-patterns/ppframe.htm para obter um conjunto de coisas detalhadas que ele notou e http://c2.com/cgi/wiki?AreDesignPatternsMissingLanguageFeatures para discussão adicional sobre o assunto.

A versão curta é que, quando seu idioma possui mais recursos, os padrões de design repetitivo tendem a se tornar mais simples - geralmente a ponto de ficarem invisíveis. Ele descobriu que isso era verdade para a maioria dos padrões de design que o GoF havia identificado.

btilly
fonte
8

A programação em uma linguagem dinâmica orientada a objetos usa muitos dos mesmos padrões e princípios, mas existem alguns ajustes e diferenças devido ao ambiente:

Substituir interfaces por Duck Typing - Onde o Gang of Four diria para você usar uma classe base abstrata com funções virtuais puras e você usaria uma interface em Java, em uma linguagem dinâmica, você só precisa entender. Como você pode usar qualquer objeto em qualquer lugar e funcionará bem se implementar os métodos realmente chamados, não será necessário definir uma interface formal. Pode valer a pena documentar um, para que fique claro o que é realmente necessário.

As funções também são objetos - Existem muitos padrões relacionados à separação entre decisão e ação; Comando, Estratégia, Cadeia de Responsabilidade, etc. Em uma linguagem com funções de primeira classe, geralmente é razoável simplesmente passar uma função ao invés de criar objetos com .doIt()métodos. Esses padrões se transformam em "use uma função de ordem superior".

VENDIDO - O Princípio de Segregação de Interface leva o maior sucesso aqui, por não haver interfaces. Você ainda deve considerar o princípio, mas não pode reificá-lo em seu código. Somente a vigilância pessoal o protegerá aqui. No lado positivo, a dor causada pela violação desse princípio é muito reduzida em ambientes dinâmicos comuns.

"... no meu particular ... idioma!" - Cada idioma tem boas práticas e más práticas, e você precisará aprendê-las e segui-las, se quiser o melhor código nessas línguas. Um padrão de iterador perfeitamente escrito pode ser considerado uma linguagem com compreensões de lista incorporadas, por exemplo.

Sean McMillan
fonte
3

Na minha experiência, alguns padrões ainda são úteis em Python e ainda mais fáceis de configurar do que em linguagens mais estáticas. Alguns padrões OTOH simplesmente não são necessários, ou até desaprovados, como o padrão Singleton. Use uma variável ou função no nível do módulo. Ou use o Padrão Borg.

Em vez de configurar um Padrão de Criação, geralmente é suficiente transmitir uma chamada que cria objetos. Pode ser uma função, um objeto com um __call__método ou mesmo uma classe, já que não existe new()no Python, apenas uma invocação da própria classe:

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)

O padrão de estado e estratégia compartilha uma estrutura muito semelhante em linguagens como C ++ e Java. Menos no Python. O Padrão de Estratégia permanece mais ou menos o mesmo, mas o Padrão de Estado se torna praticamente desnecessário. O padrão de estado em idiomas estáticos simula a mudança de classe no tempo de execução. No Python, você pode fazer exatamente isso: alterar a classe de um objeto em tempo de execução. Contanto que você o faça de maneira encapsulada e controlada, você deve ficar bem:

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

Os padrões que dependem do Static Type Dispatch não funcionarão ou funcionarão de maneira bastante diferente. Você não precisa escrever tanto código da placa da caldeira, por exemplo, Visitor Pattern: em Java e C ++, é necessário escrever um método de aceitação em todas as classes visitáveis, enquanto que no Python você pode herdar essa funcionalidade por meio de uma classe mixin, como 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)

Muitas situações que exigem a aplicação de um padrão em uma linguagem estática não o fazem tanto em Python. Muitas coisas podem ser resolvidas com outras técnicas, como funções de ordem superior (decoradores, fábricas de funções) ou meta-classes.

pillmuncher
fonte
Agora percebo que sua resposta abrange quase a pergunta que acabei de fazer: É uma boa idéia substituir __class__a implementação de uma fábrica em Python ?
Rds