Vamos escrever um ensaio infinitamente longo ! Mas essa é uma tarefa impossível. Porém, podemos criar um processo que, se executado para sempre, geraria um ensaio infinitamente longo. Perto o suficiente .
Agora, obviamente, você pode gerar um ensaio longo e repetitivo com uma única linha de código Python:
>>> "This is water. " * 20 'This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. This is water. '
Booawn … vaias! Em vez disso, geraremos um ensaio muito mais interessante usando o padrão de projeto State neste artigo.
Primeiro, entenderemos o que são máquinas de estado e como elas estão relacionadas ao padrão de design State. A seguir, criaremos uma máquina de estados que pode gerar um ensaio (razoavelmente) interessante e infinito. Em seguida, examinaremos brevemente o padrão de design do estado. Finalmente, traduziremos essa máquina de estados em código orientado a objetos usando o padrão de design de estados.
Os padrões de projeto de software são formas eficazes de resolver problemas comuns. Quando aplicados adequadamente, os padrões de design de software, como o padrão de estado, podem ajudá-lo a escrever software melhor escalável, sustentável e testável.
Em essência, o padrão de design State traduz uma máquina de estados em código orientado a objetos.
Se você não está familiarizado com máquinas de estado, é um conceito bastante simples. Uma máquina de estados possui estados e transições . Os estados são certas propriedades do nosso sistema de interesse, e as transições de estado são ações que alteram essas propriedades e, portanto, também causam uma mudança de estado.
Como tenho experiência em robótica (entre outras coisas) e como as máquinas de estado são amplamente utilizadas em robótica, usarei um exemplo simples de aspirador de pó robô para ilustrar como as máquinas de estado funcionam.
O diagrama da máquina de estados mostra uma imagem intuitiva de como o robô opera, mesmo que você nunca tenha encontrado máquinas de estados. Vamos repassar esta operação passo a passo.
Assim, nosso aspirador robô possui três estados — Ancorado , Limpeza e Carregamento — e possui transições baseadas na detecção sensorial do chão e de sua bateria.
Agora que entendemos as máquinas de estado em um nível básico, vamos criar uma máquina de estado capaz de escrever um ensaio infinito.
Acima está uma máquina de estado que usa a gramática inglesa para gerar uma redação composta por frases curtas e simples. Prometo que chegaremos a uma versão mais interessante muito em breve, mas isto deve servir como um bom ponto de partida para a compreensão. Vamos ver como funciona.
Esta máquina de estado pode gerar um ensaio (absurdo) parecido com este.
O mundo late em vermelho! Primo Harry chove muito? Os tigres brilham divertidos. …
Embora “não determinístico” pareça complicado, para nossos propósitos, significa apenas adicionar alguma aleatoriedade. Essencialmente, estamos adicionando uma espécie de sorteio antes de fazer a transição para alguns dos estados. Você verá o que quero dizer.
A máquina de estado não determinística acima é muito semelhante à anterior. As únicas diferenças são:
Com a introdução da aleatoriedade, da negação e das conjunções, agora podemos gerar sentenças mais interessantes e de comprimento variável.
Agora, vamos entender como funciona o padrão de design de estado. Novamente, lembre-se de que estamos tentando traduzir uma máquina de estados em código orientado a objetos.
Na máquina de estado de geração de ensaios, observe que cada estado precisa fazer duas coisas.
Do ponto de vista de um determinado Estado, não há mais nada que ele precise saber ou fazer. Em vez de ficarmos atolados na complexidade de todo o sistema – todos os seus estados e transições – podemos concentrar-nos apenas num estado de cada vez. Na minha opinião, este tipo de isolamento e dissociação é o maior argumento de venda do padrão estatal.
Abaixo, temos um diagrama UML para o padrão de design State. Existe algum contexto no qual cada um dos estados opera, ilustrado pela classe Context
. O objeto de contexto possui um atributo de estado privado, que ele usa para chamar o estado atual para executar sua ação. Cada estado implementa uma interface State
com métodos para executar sua ação ou operação e retornar o próximo estado.
Se mapearmos isso no exemplo de geração de ensaio, o diagrama UML ficará assim.
WordState
agora é uma classe abstrata (indicada em itálico) em vez de uma interface. Classes abstratas podem ter alguns métodos e atributos abstratos (não implementados), enquanto outros podem ser definidos. As interfaces são totalmente abstratas: todos os seus métodos são abstratos. Fiz essa alteração porque a implementação generateWord
é a mesma em todos os estados e é bom evitar código duplicado.
Vamos detalhar cada um dos atributos e métodos acima. Na classe EssayContext
, temos:
state
: referência ao objeto WordState
atual.essayBody
: Lista de todas as palavras geradas até agora.setState()
: Setter para alterar o atributo state
.addWord()
: Método para adicionar a próxima palavra ao corpo do ensaio.generateEssay()
: chamamos esse método para gerar nossa redação; paramos quando o essayBody
tem comprimento maior que length
.printEssay()
: Retorna uma string da redação gerada.
Na classe abstrata WordState
, temos:
wordList
: propriedade abstrata (indicada em itálico) para uma lista de palavras da qual escolhemos palavras para gerar.generateWord()
: Método que adiciona a palavra gerada ao contexto da redação.nextState()
: método abstrato para retornar o próximo estado.
Usaremos NounState
como exemplo representativo para todos os outros estados concretos herdados de WordState
.
wordList
: uma lista de substantivos da qual escolhemos palavras para gerar.nextState()
: Retorna o próximo estado.
Agora, temos tudo o que precisamos para realmente implementar isso no código. Vamos em frente e fazer exatamente isso!
Vamos primeiro escrever a classe EssayContext
em um arquivo chamado essay_context.py
. Vamos abandonar o caso do camelo e mudar para o caso da cobra porque, bem, Python é uma... cobra (desculpe).
from word_state import WordState class EssayContext: def __init__(self, state: WordState): self.state = state self.essay_body: list[str] = [] def set_state(self, state: WordState): self.state = state def add_word(self, word: str): self.essay_body.append(word) def generate_essay(self, length: int): while len(self.essay_body) < length: self.state.generate_word(self) def print_essay(self) -> str: return " ".join(self.essay_body)
Então, vamos adicionar os estados em um arquivo chamado word_state.py
.
import abc import numpy as np class WordState(abc.ABC): word_list: list[str] @classmethod def generate_word(cls, context: "EssayContext"): word = np.random.choice(cls.word_list) context.add_word(word) context.set_state(cls.next_state()) @classmethod @abc.abstractmethod def next_state(cls) -> "WordState": pass class NounState(WordState): word_list: list[str] = ["everything", "nothing"] @classmethod def next_state(cls): return VerbState class VerbState(WordState): word_list: list[str] = ["is", "was", "will be"] @classmethod def next_state(cls): heads = np.random.rand() < 0.5 if heads: return NegationState return AdjectiveState class NegationState(WordState): word_list: list[str] = ["not"] @classmethod def next_state(cls): return AdjectiveState class AdjectiveState(WordState): word_list: list[str] = ["fantastic", "terrible"] @classmethod def next_state(cls): heads = np.random.rand() < 0.5 if heads: return ConjunctionState return EndmarkState class ConjunctionState(WordState): word_list: list[str] = ["and", "but"] @classmethod def next_state(cls): return NounState class EndmarkState(WordState): word_list: list[str] = [".", "!"] @classmethod def next_state(cls): return NounState
Finalmente, vamos adicionar código para executar tudo em main.py
.
from essay_context import EssayContext from word_state import NounState if __name__ == '__main__': ctx = EssayContext(NounState) ctx.generate_essay(100) print(ctx.print_essay())
A execução python main.py
nos fornece a seguinte saída (diferente a cada vez devido ao não determinismo):
'everything is not terrible and nothing was terrible ! everything will be not fantastic but everything is fantastic . everything will be fantastic . nothing will be fantastic and nothing will be terrible ! everything was not fantastic and everything will be not terrible . everything was terrible . nothing was terrible but nothing will be fantastic ! nothing is not terrible . nothing was not fantastic but everything was not fantastic ! everything will be not fantastic but everything will be terrible ! everything will be not fantastic . everything is fantastic but nothing will be not terrible ! everything will be not fantastic but nothing was not fantastic !'
Nada mal para um sistema tão simples! Também podemos estender as várias listas de palavras ou adicionar mais estados para tornar a geração da redação mais sofisticada. Poderíamos até introduzir algumas APIs LLM para levar nossos ensaios ao próximo nível.
As máquinas de estado e o padrão State são ideais para modelar e criar sistemas com uma noção bem definida de “estado”. Ou seja, existem comportamentos e propriedades específicas associadas a cada estado. O aspirador robô está a limpar, encaixado ou a carregar. Sua TV pode estar LIGADA ou DESLIGADA e os botões do controle remoto da TV funcionarão de maneira diferente com base no estado da TV.
Também é uma boa opção para gerar ou identificar sequências com um padrão bem definido. Isso se aplica ao nosso exemplo de geração de ensaio.
Finalmente, você pode perguntar: “Qual é o sentido de tudo isso?” Por que tivemos tantos problemas para definir os vários estados e classes para gerar este ensaio “infinito”? Poderíamos ter escrito 20 (ou menos) linhas de código Python para obter o mesmo comportamento.
A resposta curta é para melhor escalabilidade .
Imagine se, em vez de apenas três ou cinco estados, tivéssemos 50 ou 500 estados. Isso não é uma hipérbole; sistemas empresariais reais têm esse nível de complexidade. De repente, o padrão estatal parece muito mais atraente devido à sua dissociação e isolamento. Podemos concentrar-nos apenas num estado de cada vez, sem ter de manter todo o sistema nas nossas cabeças. É mais fácil introduzir mudanças porque um estado não afetará outros. Também permite testes de unidade mais fáceis, uma grande parte de um sistema escalável e de fácil manutenção.
Em última análise, o padrão estatal não se trata apenas de gerir estados; como todos os padrões de projeto, é um modelo para a construção de sistemas que sejam tão escaláveis e de fácil manutenção quanto sofisticados.