Skip to content

Latest commit

 

History

History
437 lines (317 loc) · 37.4 KB

cap10.adoc

File metadata and controls

437 lines (317 loc) · 37.4 KB

Padrões de projetos com funções de primeira classe

Conformidade a padrões não é uma medida de virtude.[1]

— Ralph Johnson
co-autor do clássico "Padrões de Projetos"

Em engenharia de software, um padrão de projeto é uma receita genérica para solucionar um problema de design frequente. Não é preciso conhecer padrões de projeto para acompanhar esse capítulo, vou explicar os padrões usados nos exemplos.

O uso de padrões de projeto em programação foi popularizado pelo livro seminal Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos (Addison-Wesley), de Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides—também conhecidos como "the Gang of Four" (A Gangue dos Quatro). O livro é um catálogo de 23 padrões, cada um deles composto por arranjos de classes e exemplificados com código em C++, mas assumidos como úteis também em outras linguagens orientadas a objetos.

Apesar dos padrões de projeto serem independentes da linguagem, isso não significa que todo padrão se aplica a todas as linguagens. Por exemplo, o [iterables2generators] vai mostrar que não faz sentido emular a receita do padrão Iterator (Iterador) (EN) no Python, pois esse padrão está embutido na linguagem e pronto para ser usado, na forma de geradores—que não precisam de classes para funcionar, e exigem menos código que a receita clássica.

Os autores de Padrões de Projetos reconhecem, na introdução, que a linguagem usada na implementação determina quais padrões são relevantes:

A escolha da linguagem de programação é importante, pois ela influencia nosso ponto de vista. Nossos padrões supõe uma linguagem com recursos equivalentes aos do Smalltalk e do C++—e essa escolha determina o que pode e o que não pode ser facilmente implementado. Se tivéssemos presumido uma linguagem procedural, poderíamos ter incluído padrões de projetos chamados "Herança", "Encapsulamento" e "Polimorfismo". Da mesma forma, alguns de nossos padrões são suportados diretamente por linguagens orientadas a objetos menos conhecidas. CLOS, por exemplo, tem multi-métodos, reduzindo a necessidade de um padrão como o Visitante.[2]

Em sua apresentação de 1996, "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas) (EN), Peter Norvig afirma que 16 dos 23 padrões no Padrões de Projeto original se tornam "invisíveis ou mais simples" em uma linguagem dinâmica (slide 9). Ele está falando das linguagens Lisp e Dylan, mas muitos dos recursos dinâmicos relevantes também estão presentes no Python. Em especial, no contexto de linguagens com funções de primeira classe, Norvig sugere repensar os padrões clássicos conhecidos como Estratégia (Strategy), Comando (Command), Método Template (Template Method) e Visitante (Visitor).

O objetivo desse capítulo é mostrar como—em alguns casos—as funções podem realizar o mesmo trabalho das classes, com um código mais legível e mais conciso. Vamos refatorar uma implementaçao de Estratégia usando funções como objetos, removendo muito código redundante. Vamos também discutir uma abordagem similar para simplificar o padrão Comando.

Novidades nesse capítulo

Movi este capítulo para o final da Parte II, para poder então aplicar o decorador de registro na Padrão Estratégia aperfeiçoado com um decorador, e também usar dicas de tipo nos exemplos. A maior parte das dicas de tipo usadas nesse capítulo não são complicadas, e ajudam na legibilidade.

Estudo de caso: refatorando Estratégia

Estratégia é um bom exemplo de um padrão de projeto que pode ser mais simples em Python, usando funções como objetos de primeira classe. Na próxima seção vamos descrever e implementar Estratégia usando a estrutura "clássica" descrita em Padrões de Projetos. Se você estiver familiarizado com o padrão clássico, pode pular direto para Estratégia baseada em funções, onde refatoramos o código usando funções, reduzindo significativamente o número de linhas.

Estratégia clássica

O diagrama de classes UML na Figura 1 retrata um arranjo de classes exemplificando o padrão Estratégia.

Cálculos de desconto de um pedido como estratégias
Figura 1. Diagrama de classes UML para o processamento de descontos em um pedido, implementado com o padrão de projeto Estratégia.

O padrão Estratégia é resumido assim em Padrões de Projetos:

Define uma família de algoritmos, encapsula cada um deles, e os torna intercambiáveis. Estratégia permite que o algoritmo varie de forma independente dos clientes que o usam.

Um exemplo claro de Estratégia, aplicado ao domínio do ecommerce, é o cálculo de descontos em pedidos de acordo com os atributos do cliente ou pela inspeção dos itens do pedido.

Considere uma loja online com as seguintes regras para descontos:

  • Clientes com 1.000 ou mais pontos de fidelidade recebem um desconto global de 5% por pedido.

  • Um desconto de 10% é aplicado a cada item com 20 ou mais unidades no mesmo pedido.

  • Pedidos com pelo menos 10 itens diferentes recebem um desconto global de 7%.

Para simplificar, vamos assumir que apenas um desconto pode ser aplicado a cada pedido.

O diagrama de classes UML para o padrão Estratégia aparece na Figura 1. Seus participantes são:

Contexto (Context)

Oferece um serviço delegando parte do processamento para componentes intercambiáveis, que implementam algoritmos alternativos. No exemplo de ecommerce, o contexto é uma classe Order, configurada para aplicar um desconto promocional de acordo com um de vários algoritmos.

Estratégia (Strategy)

A interface comum dos componentes que implementam diferentes algoritmos. No nosso exemplo, esse papel cabe a uma classe abstrata chamada Promotion.

Estratégia concreta (Concrete strategy)

Cada uma das subclasses concretas de Estratégia. FidelityPromo, BulkPromo, e LargeOrderPromo são as três estratégias concretas implementadas.

O código no Exemplo 1 segue o modelo da Figura 1. Como descrito em Padrões de Projetos, a estratégia concreta é escolhida pelo cliente da classe de contexto. No nosso exemplo, antes de instanciar um pedido, o sistema deveria, de alguma forma, selecionar o estratégia de desconto promocional e passá-la para o construtor de Order. A seleção da estratégia está fora do escopo do padrão.

Exemplo 1. Implementação da classe Order com estratégias de desconto intercambiáveis
link:code/10-dp-1class-func/classic_strategy.py[role=include]

Observe que no Exemplo 1, programei Promotion como uma classe base abstrata (ABC), para usar o decorador @abstractmethod e deixar o padrão mais explícito.

O Exemplo 2 apresenta os doctests usados para demonstrar e verificar a operação de um módulo implementando as regras descritas anteriormente.

Exemplo 2. Amostra de uso da classe Order com a aplicação de diferentes promoções
link:code/10-dp-1class-func/classic_strategy.py[role=include]
  1. Dois clientes: joe tem 0 pontos de fidelidade, ann tem 1.100.

  2. Um carrinho de compras com três itens.

  3. A promoção FidelityPromo não dá qualquer desconto para joe.

  4. ann recebe um desconto de 5% porque tem pelo menos 1.000 pontos.

  5. O banana_cart contém 30 unidade do produto "banana" e 10 maçãs.

  6. Graças à BulkItemPromo, joe recebe um desconto de $1,50 no preço das bananas.

  7. O long_cart tem 10 itens diferentes, cada um custando $1,00.

  8. joe recebe um desconto de 7% no pedido total, por causa da LargerOrderPromo.

O Exemplo 1 funciona perfeitamente bem, mas a mesma funcionalidade pode ser implementada com menos linhas de código em Python, se usarmos funções como objetos. Veremos como fazer isso na próxima seção.

Estratégia baseada em funções

Cada estratégia concreta no Exemplo 1 é uma classe com um único método, discount. Além disso, as instâncias de estratégia não tem nenhum estado (nenhum atributo de instância). Você poderia dizer que elas se parecem muito com funções simples, e estaria certa. O Exemplo 3 é uma refatoração do Exemplo 1, substituindo as estratégias concretas por funções simples e removendo a classe abstrata Promo. São necessários apenas alguns pequenos ajustes na classe Order.[3]

Exemplo 3. A classe Order com as estratégias de descontos implementadas como funções
link:code/10-dp-1class-func/strategy.py[role=include]
  1. Essa dica de tipo diz: promotion pode ser None, ou pode ser um invocável que recebe uma Order como argumento e devolve um Decimal.

  2. Para calcular o desconto, chama o invocável self.promotion, passando self como um argumento. Veja a razão disso logo abaixo.

  3. Nenhuma classe abstrata.

  4. Cada estratégia é uma função.

Tip
Por que self.promotion(self)?

Na classe Order, promotion não é um método. É um atributo de instância que por acaso é invocável. Então a primeira parte da expressão, self.promotion, busca aquele invocável. Mas, ao invocá-lo, precisamos fornecer uma instância de Order, que neste caso é self. Por isso self aparece duas vezes na expressão.

A [methods_are_descriptors_sec] vai explicar o mecanismo que vincula automaticamente métodos a instâncias. Mas isso não se aplica a promotion, pois ela não é um método.

O código no Exemplo 3 é mais curto que o do Exemplo 1. Usar a nova Order é também um pouco mais simples, como mostram os doctests no Exemplo 4.

Exemplo 4. Amostra do uso da classe Order com as promoções como funções
link:code/10-dp-1class-func/strategy.py[role=include]
  1. Mesmos dispositivos de teste do Exemplo 1.

  2. Para aplicar uma estratégia de desconto a uma Order, basta passar a função de promoção como argumento.

  3. Uma função de promoção diferente é usada aqui e no teste seguinte.

Observe os textos explicativos do Exemplo 4—não há necessidade de instanciar um novo objeto promotion com cada novo pedido: as funções já estão disponíveis para serem usadas.

É interessante notar que no Padrões de Projetos, os autores sugerem que: "Objetos Estratégia muitas vezes são bons "peso mosca" (flyweight)".[4] Uma definição do padrão Peso Mosca em outra parte daquele texto afirma: "Um peso mosca é um objeto compartilhado que pode ser usado em múltiplos contextos simultaneamente."[5] O compartilhamento é recomendado para reduzir o custo da criação de um novo objeto concreto de estratégia, quando a mesma estratégia é aplicada repetidamente a cada novo contexto—no nosso exemplo, a cada nova instância de Order. Então, para contornar uma desvantagem do padrão Estratégia—seu custo durante a execução—os autores recomendam a aplicação de mais outro padrão. Enquanto isso, o número de linhas e custo de manutenção de seu código vão se acumulando.

Um caso de uso mais espinhoso, com estratégias concretas complexas mantendo estados internos, pode exigir a combinação de todas as partes dos padrões de projeto Estratégia e Peso Mosca. Muitas vezes, porém, estratégias concretas não tem estado interno; elas lidam apenas com dados vindos do contexto. Neste caso, não tenha dúvida, use as boas e velhas funções ao invés de escrever classes de um só metodo implementando uma interface de um só método declarada em outra classe diferente. Uma função pesa menos que uma instância de uma classe definida pelo usuário, e não há necessidade do Peso Mosca, pois cada função da estratégia é criada apenas uma vez por processo Python, quando o módulo é carregado. Uma função simples também é um "objeto compartilhado que pode ser usado em múltiplos contextos simultaneamente".

Uma vez implementado o padrão Estratégia com funções, outras possibilidades nos ocorrem. Suponha que você queira criar uma "meta-estratégia", que seleciona o melhor desconto disponível para uma dada Order. Nas próximas seções vamos estudar as refatorações adicionais para implementar esse requisito, usando abordagens que se valem de funções e módulos vistos como objetos.

Escolhendo a melhor estratégia: uma abordagem simples

Dados os mesmos clientes e carrinhos de compras dos testes no Exemplo 4, vamos agora acrescentar três testes adicionais ao Exemplo 5.

Exemplo 5. A funcão best_promo aplica todos os descontos e devolve o maior
link:code/10-dp-1class-func/strategy_best.py[role=include]
  1. best_promo selecionou a larger_order_promo para o cliente joe.

  2. Aqui joe recebeu o desconto de bulk_item_promo, por comprar muitas bananas.

  3. Encerrando a compra com um carrinho simples, best_promo deu à cliente fiel ann o desconto da fidelity_promo.

A implementação de best_promo é muito simples. Veja o Exemplo 6.

Exemplo 6. best_promo encontra o desconto máximo iterando sobre uma lista de funções
link:code/10-dp-1class-func/strategy_best.py[role=include]
  1. promos: lista de estratégias implementadas como funções.

  2. best_promo recebe uma instância de Order como argumento, como as outras funções *_promo.

  3. Usando uma expressão geradora, aplicamos cada uma das funções de promos a order, e devolvemos o maior desconto encontrado.

O Exemplo 6 é bem direto: promos é uma list de funções. Depois que você se acostuma à ideia de funções como objetos de primeira classe, o próximo passo é notar que construir estruturas de dados contendo funções muitas vezes faz todo sentido.

Apesar do Exemplo 6 funcionar e ser fácil de ler, há alguma duplicação que poderia levar a um bug sutil: para adicionar uma nova estratégia, precisamos escrever a função e lembrar de incluí-la na lista promos. De outra forma a nova promoção só funcionará quando passada explicitamente como argumento para Order, e não será considerada por best_promotion.

Vamos examinar algumas soluções para essa questão.

Encontrando estratégias em um módulo

Módulos também são objetos de primeira classe no Python, e a biblioteca padrão oferece várias funções para lidar com eles. A função embutida globals é descrita assim na documentação do Python:

globals()

Devolve um dicionário representando a tabela de símbolos globais atual. Isso é sempre o dicionário do módulo atual (dentro de uma função ou método, esse é o módulo onde a função ou método foram definidos, não o módulo de onde são chamados).

O Exemplo 7 é uma forma um tanto hacker de usar globals para ajudar best_promo a encontrar automaticamente outras funções *_promo disponíveis.

Exemplo 7. A lista promos é construída a partir da introspecção do espaço de nomes global do módulo
link:code/10-dp-1class-func/strategy_best2.py[role=include]
  1. Importa as funções de promoções, para que fiquem disponíveis no espaço de nomes global.[6]

  2. Itera sobre cada item no dict devolvido por globals().

  3. Seleciona apenas aqueles valores onde o nome termina com o sufixo _promo e…​

  4. …​filtra e remove a própria best_promo, para evitar uma recursão infinita quando best_promo for invocada.

  5. Nenhuma mudança em best_promo.

Outra forma de coletar as promoções disponíveis seria criar um módulo e colocar nele todas as funções de estratégia, exceto best_promo.

No Exemplo 8, a única mudança significativa é que a lista de funções de estratégia é criada pela introspecção de um módulo separado chamado promotions. Veja que o Exemplo 8 depende da importação do módulo promotions bem como de inspect, que fornece funções de introspecção de alto nível.

Exemplo 8. A lista promos é construída a partir da introspecção de um novo módulo, promotions
link:code/10-dp-1class-func/strategy_best3.py[role=include]

A função inspect.getmembers devolve os atributos de um objeto—neste caso, o módulo promotions—opcionalmente filtrados por um predicado (uma função booleana). Usamos inspect.isfunction para obter apenas as funções do módulo.

O Exemplo 8 funciona independente dos nomes dados às funções; tudo o que importa é que o módulo promotions contém apenas funções que, dado um pedido, calculam os descontos. Claro, isso é uma suposição implícita do código. Se alguém criasse uma função com uma assinatura diferente no módulo promotions, best_promo geraria um erro ao tentar aplicá-la a um pedido.

Poderíamos acrescentar testes mais estritos para filtrar as funções, por exemplo inspecionando seus argumentos. O ponto principal do Exemplo 8 não é oferecer uma solução completa, mas enfatizar um uso possível da introspecção de módulo.

Uma alternativa mais explícita para coletar dinamicamente as funções de desconto promocional seria usar um decorador simples. É nosso próximo tópico.

Padrão Estratégia aperfeiçoado com um decorador

Lembre-se que nossa principal objeção ao Exemplo 6 foi a repetição dos nomes das funções em suas definições e na lista promos, usada pela função best_promo para determinar o maior desconto aplicável. A repetição é problemática porque alguém pode acrescentar uma nova função de estratégia promocional e esquecer de adicioná-la manualmente à lista promos—caso em que best_promo vai silenciosamente ignorar a nova estratégia, introduzindo no sistema um bug sutil. O Exemplo 9 resolve esse problema com a técnica vista na [registration_deco_sec].

Exemplo 9. A lista promos é preenchida pelo decorador promotion
link:code/10-dp-1class-func/strategy_best4.py[role=include]
  1. A lista promos é global no módulo, e começa vazia.

  2. promotion é um decorador de registro: ele devolve a função promo inalterada, após inserí-la na lista promos.

  3. Nenhuma mudança é necessária em best_promo, pois ela se baseia na lista promos.

  4. Qualquer função decorada com @promotion será adicionada a promos.

Essa solução tem várias vantagens sobre aquelas apresentadas anteriormente:

  • As funções de estratégia de promoção não precisam usar nomes especiais—não há necessidade do sufixo _promo.

  • O decorador @promotion realça o propósito da função decorada, e também torna mais fácil desabilitar temporariamente uma promoção: basta transformar a linha do decorador em comentário.

  • Estratégias de desconto promocional podem ser definidas em outros módulos, em qualquer lugar do sistema, desde que o decorador @promotion seja aplicado a elas.

Na próxima seção vamos discutir Comando (Command)—outro padrão de projeto que é algumas vezes implementado via classes de um só metodo, quando funções simples seriam suficientes.

O padrão Comando

Comando é outro padrão de projeto que pode ser simplificado com o uso de funções passadas como argumentos. A Figura 2 mostra o arranjo das classes nesse padrão.

Aplicação do padrão Comando a um editor de texto
Figura 2. Diagrama de classes UML para um editor de texto controlado por menus, implementado com o padrão de projeto Comando. Cada comando pode ter um receptor (receiver) diferente: o objeto que implementa a ação. Para PasteCommand, o receptor é Document. Para OpenCommand, o receptor á a aplicação.

O objetivo de Comando é desacoplar um objeto que invoca uma operação (o invoker ou remetente) do objeto fornecedor que implementa aquela operação (o receiver ou receptor). No exemplo em Padrões de Projetos, cada remetente é um item de menu em uma aplicação gráfica, e os receptors são o documento sendo editado ou a própria aplicação.

A ideia é colocar um objeto Command entre os dois, implementando uma interface com um único método, execute, que chama algum método no receptor para executar a operação desejada. Assim, o remetente não precisa conhecer a interface do receptor, e receptors diferentes podem ser adaptados com diferentes subclasses de Command. O remetente é configurado com um comando concreto, e o opera chamando seu método execute. Observe na Figura 2 que MacroCommand pode armazenar um sequência de comandos; seu método execute() chama o mesmo método em cada comando armazenado.

Citando Padrões de Projetos, "Comandos são um substituto orientado a objetos para callbacks." A pergunta é: precisamos de um substituto orientado a objetos para callbacks? Algumas vezes sim, mas nem sempre.

Em vez de dar ao remetente uma instância de Command, podemos simplesmente dar a ele uma função. Em vez de chamar command.execute(), o remetente pode apenas chamar command(). O MacroCommand pode ser programado como uma classe que implementa __call__. Instâncias de MacroCommand seriam invocáveis, cada uma mantendo uma lista de funções para invocação futura, como implementado no Exemplo 10.

Exemplo 10. Cada instância de MacroCommand tem uma lista interna de comandos
class MacroCommand:
    """A command that executes a list of commands"""

    def __init__(self, commands):
        self.commands = list(commands)  # (1)

    def __call__(self):
        for command in self.commands:  # (2)
            command()
  1. Criar uma nova lista com os itens do argumento commands garante que ela seja iterável e mantém uma cópia local de referências a comandos em cada instância de MacroCommand.

  2. Quando uma instância de MacroCommand é invocada, cada comando em self.commands é chamado em sequência.

Usos mais avançados do padrão Comando—para implementar "desfazer", por exemplo—podem exigir mais que uma simples função de callback. Mesmo assim, o Python oferece algumas alternativas que merecem ser consideradas:

  • Uma instância invocável como MacroCommand no Exemplo 10 pode manter qualquer estado que seja necessário, e oferecer outros métodos além de __call__.

  • Uma clausura pode ser usada para manter o estado interno de uma função entre invocações.

Isso encerra nossa revisão do padrão Comando usando funções de primeira classe. Por alto, a abordagem aqui foi similar à que aplicamos a Estratégia: substituir as instâncias de uma classe participante que implementava uma interface de método único por invocáveis. Afinal, todo invocável do Python implementa uma interface de método único, e esse método se chama __call__.

Resumo do Capítulo

Como apontou Peter Norvig alguns anos após o surgimento do clássico Padrões de Projetos, "16 dos 23 padrões tem implementações qualitativamente mais simples em Lisp ou Dylan que em C++, pelo menos para alguns usos de cada padrão" (slide 9 da apresentação de Norvig, "Design Patterns in Dynamic Languages" presentation (Padrões de Projetos em Linguagens Dinâmicas)). O Python compartilha alguns dos recursos dinâmicos das linguagens Lisp e Dylan, especialmente funções de primeira classe, nosso foco nesse capítulo.

Na mesma palestra citada no início deste capítulo, refletindo sobre o 20º aniversário de Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos, Ralph Johnson afirmou que um dos defeitos do livro é: "Excesso de ênfase nos padrões como linhas de chegada, em vez de como etapas em um processo de design".[7] Neste capítulo usamos o padrão Estratégia como ponto de partida: uma solução que funcionava, mas que simplificamos usando funções de primeira classe.

Em muitos casos, funções ou objetos invocáveis oferecem um caminho mais natural para implementar callbacks em Python que a imitação dos padrões Estratégia ou Comando como descritos por Gamma, Helm, Johnson, e Vlissides em Padrões de Projetos. A refatoração de Estratégia e a discussão de Comando nesse capítulo são exemplos de uma ideia mais geral: algumas vezes você pode encontrar uma padrão de projeto ou uma API que exigem que seus componentes implementem uma interface com um único método, e aquele método tem um nome que soa muito genérico, como "executar", "rodar" ou "fazer". Tais padrões ou APIs podem frequentemente ser implementados em Python com menos código repetitivo, usando funções como objetos de primeira classe.

Leitura complementar

A "Receita 8.21. Implementando o Padrão Visitante" (Receipt 8.21. Implementing the Visitor Pattern) no Python Cookbook, 3ª ed. (EN), mostra uma implementação elegante do padrão Visitante, na qual uma classe NodeVisitor trata métodos como objetos de primeira classe.

Sobre o tópico mais geral de padrões de projetos, a oferta de leituras para o programador Python não é tão numerosa quando aquela disponível para as comunidades de outras linguagens.

Learning Python Design Patterns ("Aprendendo os Padrões de Projeto do Python"), de Gennadiy Zlobin (Packt), é o único livro inteiramente dedicado a padrões em Python que encontrei. Mas o trabalho de Zlobin é muito breve (100 páginas) e trata de apenas 8 dos 23 padrões de projeto originais.

Expert Python Programming ("Programação Avançada em Python"), de Tarek Ziadé (Packt), é um dos melhores livros de Python de nível intermediário, e seu capítulo final, "Useful Design Patterns" (Padrões de Projetos Úteis), apresenta vários dos padrões clássicos de uma perspectiva pythônica.

Alex Martelli já apresentou várias palestras sobre padrões de projetos em Python. Há um vídeo de sua apresentação na EuroPython (EN) e um conjunto de slides em seu site pessoal (EN). Ao longo dos anos, encontrei diferentes jogos de slides e vídeos de diferentes tamanhos, então vale a pena tentar uma busca mais ampla com o nome dele e as palavras "Python Design Patterns". Um editor me contou que Martelli está trabalhando em um livro sobre esse assunto. Eu certamente comprarei meu exemplar assim que estiver disponível.

Há muitos livros sobre padrões de projetos no contexto do Java mas, dentre todos eles, meu preferido é Head First Design Patterns ("Mergulhando de Cabeça nos Padrões de Projetos"), 2ª ed., de Eric Freeman e Elisabeth Robson (O’Reilly). Esse volume explica 16 dos 23 padrões clássicos. Se você gosta do estilo amalucado da série Head First e precisa de uma introdução a esse tópico, vai adorar esse livro. Ele é centrado no Java, mas a segunda edição foi atualizada para refletir a introdução de funções de primeira classe naquela linguagem, tornando alguns dos exemplos mais próximos de código que escreveríamos em Python.

Para um olhar moderno sobre padrões, do ponto de vista de uma linguagem dinâmica com duck typing e funções de primeira classe, Design Patterns in Ruby ("Padrões de Projetos em Ruby") de Russ Olsen (Addison-Wesley) traz muitas ideias aplicáveis também ao Python. A despeito de suas muitas diferenças sintáticas, no nível semântico o Python e o Ruby estão mais próximos entre si que do Java ou do C++.

Em "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas) (slides), Peter Norvig mostra como funções de primeira classe (e outros recursos dinâmicos) tornam vários dos padrões de projeto originais mais simples ou mesmo desnecessários.

A "Introdução" do Padrões de Projetos original, de Gamma et al. já vale o preço do livro—mais até que o catálogo de 23 padrões, que inclui desde receitas muito importantes até algumas raramente úteis. Alguns princípios de projetos de software muito citados, como "Programe para uma interface, não para uma implementação" e "Prefira a composição de objetos à herança de classe", vem ambos daquela introdução.

A aplicação de padrões a projetos se originou com o arquiteto Christopher Alexander et al., e foi apresentada no livro A Pattern Language ("Uma Linguagem de Padrões") (Oxford University Press). A ideia de Alexander é criar um vocabulário padronizado, permitindo que equipes compartilhem decisões comuns em projetos de edificações. M. J. Dominus wrote “‘Design Patterns’ Aren’t” (Padrões de Projetos Não São), uma curiosa apresentação de slides acompanhada de um texto, argumentando que a visão original de Alexander sobre os padrões é mais profunda e mais humanista e também aplicável à engenharia de software.

Ponto de vista

O Python tem funções de primeira classe e tipos de primeira classe, e Norvig afima que esses recursos afetam 10 dos 23 padrões (no slide 10 de "Design Patterns in Dynamic Languages" (Padrões de Projetos em Linguagens Dinâmicas)). No [closures_and_decorators], vimos que o Python também tem funções genéricas (na [generic_functions]), uma forma limitada dos multi-métodos do CLOS, que Gamma et al. sugerem como uma maneira mais simples de implementar o padrão clássico Visitante (Visitor). Norvig, por outro lado, diz (no slide 10) que os multi-métodos simplificam o padrão Construtor (Builder). Ligar padrões de projetos a recursos de linguagens não é uma ciência exata.

Em cursos a redor do mundo todo, padrões de projetos são frequentemente ensinados usando exemplos em Java. Ouvi mais de um estudante dizer que eles foram levados a crer que os padrões de projeto originais são úteis qualquer que seja a linguagem usada na implementação. A verdade é que os 23 padrões "clássicos" de Padrões de Projetos se aplicam muito bem ao Java, apesar de terem sido apresentados principalmente no contexto do C++—no livro, alguns deles tem exemplos em Smalltalk. Mas isso não significa que todos aqueles padrões podem ser aplicados de forma igualmente satisfatória a qualquer linguagem. Os autores dizem explicitamente, logo no início de seu livro, que "alguns de nossos padrões são suportados diretamente por linguagens orientadas a objetos menos conhecidas" (a citação completa apareceu na primeira página deste capítulo).

A bibliografia do Python sobre padrões de projetos é muito pequena, se comparada à existente para Java, C++ ou Ruby. Na Leitura complementar, mencionei Learning Python Design Patterns ("Aprendendo Padrões de Projeto do Python"), de Gennadiy Zlobin, que foi publicado apenas em novembro de 2013. Para se ter uma ideia, Design Patterns in Ruby ("Padrões de Projetos em Ruby"), de Russ Olsen, foi publicado em 2007 e tem 384 páginas—284 a mais que a obra de Zlobin.

Agora que o Python está se tornando cada vez mais popular no ambiente acadêmico, podemos esperar que novos livros sobre padrões de projetos sejam escritos no contexto de nossa linguagem. Além disso, o Java 8 introduziu referências a métodos e funções anônimas, e esses recursos muito esperados devem incentivar o surgimento de novas abordagens aos padrões em Java—reconhecendo que, à medida que as linguagens evoluem, nosso entendimento sobre a forma de aplicação dos padrões de projetos clássicos deve também evoluir.

O call selvagem

Enquanto trabalhávamos juntos para dar os toques finais a este livro, o revisor técnico Leonardo Rochael pensou:

Se funções tem um método __call__, e métodos também são invocáveis, será que os métodos __call__ também tem um método __call__?

Não sei se a descoberta dele tem alguma utilidade, mas eis um fato engraçado:

>>> def turtle():
...     return 'eggs'
...
>>> turtle()
'eggs'
>>> turtle.__call__()
'eggs'
>>> turtle.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__.__call__.__call__()
'eggs'

1. De um slide na palestra "Root Cause Analysis of Some Faults in Design Patterns," (Análise das Causas Básicas de Alguns Defeitos em Padrões de Projetos), apresentada por Ralph Johnson no IME/CCSL da Universidade de São Paulo, em 15 de novembro de 2014.
2. Visitor, Citado da página 4 da edição em inglês de Padrões de Projeto.
3. Precisei reimplementar Order com @dataclass devido a um bug no Mypy. Você pode ignorar esse detalhe, pois essa classe funciona também com NamedTuple, exatamente como no Exemplo 1. Quando Order é uma NamedTuple, o Mypy 0.910 encerra com erro ao verificar a dica de tipo para promotion. Tentei acrescentar # type ignore àquela linha específica, mas o erro persistia. Entretanto, se Order for criada com @dataclass, o Mypy trata corretamente a mesma dica de tipo. O Issue #9397 não havia sido resolvido em 19 de julho de 2021, quando essa nota foi escrita. Espero que o problema tenha sido solucionado quando você estiver lendo isso. NT: Aparentemente foi resolvido. O Issue #9397 gerou o Issue #12629, fechado com indicação de solucionado em agosto de 2022, o último comentário indicando que a opção de linha de comando --enable-recursive-aliases do Mypy evita os erros relatados).
4. veja a página 323 da edição em inglês de Padrões de Projetos.
5. Ibid., p. 196.
6. Tanto o flake8 quanto o VS Code reclamam que esses nomes são importados mas não são usados. Por definição, ferramentas de análise estática não conseguem entender a natureza dinâmica do Python. Se seguirmos todos os conselhos dessas ferramentas, logo estaremos escrevendo programas austeros e prolixos similares aos do Java, mas com a sintaxe do Python.
7. "Root Cause Analysis of Some Faults in Design Patterns" (Análise das Causas Básicas de Alguns Defeitos em Padrões de Projetos), palestra apresentada por Johnson no IME/CCSL da Universidade de São Paulo, em 15 de novembro de 2014.
8. NT: Literalmente "Tartarugas até embaixo" ou algo como "Tartarugas até onde a vista alcança" ou "Uma torre infinita de tartarugas". Curiosamente, um livro com esse nome foi publicado no Brasil com o título "Mil vezes adeus", na tradição brasileira (especialmente para filmes) de traduzir nomes de obras de forma preguiçosa ou aleatória.