Conformidade a padrões não é uma medida de virtude.[1]
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.
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.
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.
O diagrama de classes UML na Figura 1 retrata um arranjo de classes exemplificando o padrão 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
, eLargeOrderPromo
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.
Order
com estratégias de desconto intercambiáveislink: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.
Order
com a aplicação de diferentes promoçõeslink:code/10-dp-1class-func/classic_strategy.py[role=include]
-
Dois clientes:
joe
tem 0 pontos de fidelidade,ann
tem 1.100. -
Um carrinho de compras com três itens.
-
A promoção
FidelityPromo
não dá qualquer desconto parajoe
. -
ann
recebe um desconto de 5% porque tem pelo menos 1.000 pontos. -
O
banana_cart
contém 30 unidade do produto"banana"
e 10 maçãs. -
Graças à
BulkItemPromo
,joe
recebe um desconto de $1,50 no preço das bananas. -
O
long_cart
tem 10 itens diferentes, cada um custando $1,00. -
joe
recebe um desconto de 7% no pedido total, por causa daLargerOrderPromo
.
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.
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]
Order
com as estratégias de descontos implementadas como funçõeslink:code/10-dp-1class-func/strategy.py[role=include]
-
Essa dica de tipo diz:
promotion
pode serNone
, ou pode ser um invocável que recebe umaOrder
como argumento e devolve umDecimal
. -
Para calcular o desconto, chama o invocável
self.promotion
, passandoself
como um argumento. Veja a razão disso logo abaixo. -
Nenhuma classe abstrata.
-
Cada estratégia é uma função.
Tip
|
Por que self.promotion(self)?
Na classe A [methods_are_descriptors_sec] vai explicar o mecanismo que vincula automaticamente métodos a instâncias. Mas isso não se aplica a |
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.
Order
com as promoções como funçõeslink:code/10-dp-1class-func/strategy.py[role=include]
-
Mesmos dispositivos de teste do Exemplo 1.
-
Para aplicar uma estratégia de desconto a uma
Order
, basta passar a função de promoção como argumento. -
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.
Dados os mesmos clientes e carrinhos de compras dos testes no Exemplo 4, vamos agora acrescentar três testes adicionais ao Exemplo 5.
best_promo
aplica todos os descontos e devolve o maiorlink:code/10-dp-1class-func/strategy_best.py[role=include]
-
best_promo
selecionou alarger_order_promo
para o clientejoe
. -
Aqui
joe
recebeu o desconto debulk_item_promo
, por comprar muitas bananas. -
Encerrando a compra com um carrinho simples,
best_promo
deu à cliente fielann
o desconto dafidelity_promo
.
A implementação de best_promo
é muito simples. Veja o Exemplo 6.
best_promo
encontra o desconto máximo iterando sobre uma lista de funçõeslink:code/10-dp-1class-func/strategy_best.py[role=include]
-
promos
: lista de estratégias implementadas como funções. -
best_promo
recebe uma instância deOrder
como argumento, como as outras funções*_promo
. -
Usando uma expressão geradora, aplicamos cada uma das funções de
promos
aorder
, 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.
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.
promos
é construída a partir da introspecção do espaço de nomes global do módulolink:code/10-dp-1class-func/strategy_best2.py[role=include]
-
Importa as funções de promoções, para que fiquem disponíveis no espaço de nomes global.[6]
-
Itera sobre cada item no
dict
devolvido porglobals()
. -
Seleciona apenas aqueles valores onde o nome termina com o sufixo
_promo
e… -
…filtra e remove a própria
best_promo
, para evitar uma recursão infinita quandobest_promo
for invocada. -
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.
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.
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].
promos
é preenchida pelo decorador promotion
link:code/10-dp-1class-func/strategy_best4.py[role=include]
-
A lista
promos
é global no módulo, e começa vazia. -
promotion
é um decorador de registro: ele devolve a funçãopromo
inalterada, após inserí-la na listapromos
. -
Nenhuma mudança é necessária em
best_promo
, pois ela se baseia na listapromos
. -
Qualquer função decorada com
@promotion
será adicionada apromos
.
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.
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.
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.
MacroCommand
tem uma lista interna de comandosclass 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()
-
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 deMacroCommand
. -
Quando uma instância de
MacroCommand
é invocada, cada comando emself.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__
.
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.
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.
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'
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).