Guia de estudo

Aula 21 · Qua, 01/07

Padrões de projeto — Observer e Strategy

Os padrões comportamentais: como os objetos AVISAM uns aos outros e como TROCAM de algoritmo sem mudar quem chama.

Ontem vimos os criacionais (como CRIAR objetos). Hoje, os COMPORTAMENTAIS: como os objetos se comunicam e dividem responsabilidades. Observer (quando algo muda, AVISA vários interessados) e Strategy (troca o COMO — o algoritmo — sem mexer em quem usa). Os dois vivem de ABC, polimorfismo e composição.

Observer Strategy comportamentais

roteiro da aula

O que vamos aprender hoje

01

Comportamentais

A segunda família de padrões: a que cuida de COMO os objetos se comunicam e dividem o trabalho — não de como são criados.

02

Observer

Um Subject mantém uma lista de interessados e, quando muda de estado, AVISA todos eles. Um para muitos, sem conhecer cada um.

03

Strategy

Cada algoritmo vira uma classe trocável. Um Contexto guarda a estratégia ativa e DELEGA a ela — dá pra trocar o COMO em tempo de execução.

04

Os dois juntos

Um pedido de e-commerce: avisa interessados quando o status muda (Observer) e calcula o frete por estratégia trocável (Strategy).

ponte com a aula 20

De onde paramos — criacionais montam o elenco

  • 🏭 Na aula 20 vimos os criacionais: Singleton (QUANTAS instâncias existem) e Factory (QUAL classe criar). Eles cuidam de COMO os objetos nascem.
  • 💬 Hoje, a segunda família GoF — os comportamentais: depois que os objetos existem, COMO eles conversam e dividem responsabilidades entre si.
  • 🎭 A imagem que ajuda: os criacionais montam o elenco (decidem quem entra em cena e quantos); os comportamentais ensaiam a peça (decidem como os atores interagem).
  • 🧱 Mesmas ferramentas de sempre: ABC e @abstractmethod (10), polimorfismo (09), composição (05). Não é Python novo — é arranjo novo do que você já sabe.

comportamentais · panorama

Os dois padrões de hoje, em uma frase cada

  • 📣 Observer (Observador): quando algo muda, AVISA vários interessados de uma vez. Um Subject → muitos observers. É a base de eventos e notificações.
  • 🔀 Strategy (Estratégia): troca o COMO — o algoritmo — sem mudar quem chama. Um Contexto → uma estratégia trocável. É a base de "escolher o cálculo em runtime".
  • 🧩 Os dois são comportamentais e vivem do mesmo tripé: ABC pro contrato, polimorfismo pra chamar sem isinstance, e composição pra guardar os colaboradores. Vamos pela dor de cada um.

observer · definição

O que é o Observer

  • 📡 Observer é um padrão comportamental: um objeto (o Subject) mantém uma lista de interessados (os Observers) e, ao mudar de estado, AVISA todos eles.
  • 1️⃣ É uma relação de UM para MUITOS: o Subject não conhece cada observer pelo nome — ele só percorre a lista e chama o mesmo método em cada um. Quem reage faz a própria coisa.
  • 🔌 Observer novo? inscrever() e pronto. O Subject não muda. É o oposto de ter os avisos chumbados dentro do código que dispara a mudança.

observer · o problema

O problema do Observer: avisos chumbados no código

  • 📺 Um canal de vídeos publica um vídeo novo e precisa avisar os inscritos. A solução ingênua: dentro de publicar(), chamar enviar_email(ana), enviar_push(bruno)... um a um, na mão.
  • 🌿 Cada inscrito novo vira mais uma linha dentro de publicar(); cada forma de aviso vira mais um if/elif. O método que devia só "publicar" sabe demais sobre QUEM avisar e COMO.
  • 🔗 O resultado é acoplamento: o Subject conhece cada interessado pelo nome. Mudou a lista de inscritos? Mexe no publicar(). Queríamos que ele só avisasse — sem saber quem está ouvindo.

observer · ancorando a ideia

Analogia: o grupo de avisos da escola

  • 📨 Pense no grupo de avisos da escola. A secretaria manda UMA mensagem — "aula cancelada amanhã" — e TODO mundo no grupo recebe de uma vez. Ela não manda um por um.
  • Entrou um responsável novo? Ele entra no grupo e passa a receber. Saiu? Sai do grupo e para de receber. A secretaria não muda nada no jeito de mandar a mensagem.
  • 📡 Observer é exatamente isso no código: o Subject (a secretaria) tem uma lista de inscritos e dispara um aviso pra todos; cada inscrito decide o que fazer com ele.

observer · o problema

Antes e depois do Observer

O mesmo canal, dois jeitos de notificar: cada aviso chumbado dentro de publicar() (o problema) e uma lista de inscritos que recebe o aviso por polimorfismo (o Observer).

Antes — avisos na mão

antes.py
def publicar(self, video):
    # cada inscrito chumbado aqui dentro
    enviar_email("Ana", video)
    enviar_push("Bruno", video)
    # inscrito novo? mais uma linha aqui.
    # forma de aviso nova? mais um if.
    # o publicar() conhece cada um pelo nome.

Depois — uma lista que reage

depois.py
def publicar(self, video):
    # só percorre a lista e avisa
    for inscrito in self.inscritos:
        inscrito.atualizar(video)
    # inscrito novo? só inscrever().
    # o publicar() NÃO conhece ninguém
    # pelo nome — só chama atualizar().
Observer tira os avisos de dentro do publicar(): o Subject guarda uma lista de inscritos e chama o mesmo método (atualizar) em todos. Quem entra ou sai da lista não muda o publicar() — e cada inscrito reage do seu jeito, por polimorfismo.

observer · o código

Observer em Python: Canal e Inscritos

Uma ABC Inscrito (aula 10) com o método atualizar() abstrato — o contrato de "sei reagir a um aviso". O Canal é o Subject: guarda a lista de inscritos e, ao publicar(), percorre e avisa todos por polimorfismo (aula 09).

📋
self.inscritos é a lista de observers (aula 18). O Canal COMPÕE os inscritos (composição, aula 05): ele tem uma lista deles, mas não os cria.
🔄
O for chama atualizar() em cada inscrito sem isinstance: cada um reage do seu jeito (e-mail ou push). É o polimorfismo da aula 09 fazendo o trabalho.
canal.py
from abc import ABC, abstractmethod

class Inscrito(ABC):
    @abstractmethod
    def atualizar(self, video):
        pass

class Email(Inscrito):
    def __init__(self, nome):
        self.nome = nome
    def atualizar(self, video):
        print(f"[E-mail] {self.nome}: novo vídeo '{video}'")

class Push(Inscrito):
    def __init__(self, nome):
        self.nome = nome
    def atualizar(self, video):
        print(f"[Push] {self.nome}: saiu '{video}'!")

class Canal:                          # o Subject
    def __init__(self, nome):
        self.nome = nome
        self.inscritos = []           # composição: o Canal COMPÕE inscritos
    def inscrever(self, inscrito):
        self.inscritos.append(inscrito)
    def publicar(self, video):
        print(f"== {self.nome} publicou: {video} ==")
        for inscrito in self.inscritos:
            inscrito.atualizar(video)  # avisa todos, sem saber o tipo

canal = Canal("DevTube")
canal.inscrever(Email("Ana"))
canal.inscrever(Push("Bruno"))
canal.publicar("POO em Python")
Repare: o publicar() não conhece Email nem Push — só sabe que todo inscrito tem um atualizar(video). É a ABC garantindo o contrato. Inscrito novo (ex.: um WebhookInscrito) = classe nova com atualizar(); o publicar() não muda uma vírgula.

observer · o código

Saída no terminal

Rodando o canal.py, um único publicar() dispara o aviso para os dois inscritos:

📣
Um publicar() só, dois avisos: o Subject percorreu a lista e chamou atualizar() em cada inscrito. Um para muitos.
🎭
Cada linha tem um formato diferente ([E-mail] e [Push]) porque cada subclasse implementou atualizar() do seu jeito — polimorfismo puro.
terminal
== DevTube publicou: POO em Python ==
[E-mail] Ana: novo vídeo 'POO em Python'
[Push] Bruno: saiu 'POO em Python'!
É a solução do problema do s6: o publicar() não tem mais avisos chumbados. Para adicionar um inscrito, basta canal.inscrever(...) — a lista cresce, o método de publicar fica intacto.

observer · na UML

O Observer em UML

O Canal e os inscritos que acabamos de codar, desenhados como caixas de classe (a notação da aula 19): o Subject que compõe uma lista de Observers.

Diagrama UML do Observer: a classe Canal (com nome, inscritos, inscrever() e publicar()) ligada por uma seta de losango vazio a Inscrito, indicando que o Canal compõe uma lista de inscritos e os notifica; Inscrito é uma classe abstrata com o método atualizar(), e Email e Push herdam dela por setas de triângulo vazio.
Leia o diagrama: Canal ◆── Inscrito (composição: o Subject guarda uma lista de observers) e Inscrito «abstract» △ Email/Push (herança: cada observer implementa o contrato atualizar). É a mesma UML da aula 19 — o losango ◆ é o Subject conhecendo seus observers.

observer · onde você já viu (e vai ver)

Ecossistema: os callbacks de GUI SÃO Observer

  • 🖱️ Toda interface gráfica é feita de Observer. Quando você liga uma função a um botão — button.config(command=ao_clicar) — você está REGISTRANDO um observer no botão.
  • 🔔 Quando o botão é clicado, ele AVISA: chama o ao_clicar() que você registrou. O botão é o Subject; a sua função é o observer. No navegador é o mesmo: addEventListener("click", ...).
  • 🪟 Por isso o Observer é a espinha dorsal da GUI de quarta-feira: ligar uma função a um botão = inscrever um observer. O padrão de hoje é o que vai fazer a interface da aula 22 reagir aos cliques.

observer · amarrando com a POO

Conexão: o Observer só junta o que você já sabe

  • 🧬 ABC + @abstractmethod (aula 10): Inscrito é o contrato — todo observer PRECISA ter atualizar(). O Subject confia nesse contrato pra avisar qualquer um.
  • 🎭 Polimorfismo (aula 09): o for inscrito in ...: inscrito.atualizar(video) chama o método certo de cada subclasse, sem isinstance, sem if/elif.
  • 🧱 Composição (aula 05): o Canal COMPÕE uma lista de inscritos — tem-um, não é-um. A relação Subject↔Observers é composição pura.
  • 📋 Lista de objetos (aula 18): self.inscritos é uma lista que você percorre. Tudo o que muda em relação à aula 20 é a INTENÇÃO: lá a gente criava objetos; aqui eles se comunicam.

observer · quando NÃO usar

Armadilha: observers que vazam (e nunca saem da lista)

O erro mais comum: inscrever e nunca remover. A lista só cresce, observers "mortos" continuam recebendo aviso. A defesa é ter um cancelar() que tira da lista.

O vazamento

vaza.py
# só inscreve, nunca remove:
loja.inscrever("Ana")
loja.inscrever("Bruno")
# Ana saiu do site, mas continua
# na lista — vai receber avisos
# pra sempre. A lista só CRESCE.

A defesa: cancelar()

cancela.py
def cancelar(self, obs):
    self.inscritos.remove(obs)

loja.inscrever("Ana")
loja.inscrever("Bruno")
loja.cancelar("Ana")
loja.avisar_promocao("Notebook")
# Bruno: promoção de Notebook!
# (só Bruno — Ana saiu da lista)
Todo Subject que tem inscrever() deveria ter cancelar(). Sem isso, observers vazam e a lista cresce sem fim. Outras armadilhas: não ponha lógica que dispara NOVAS notificações dentro do atualizar() (cascata imprevisível), e não use Observer se há um único interessado fixo — aí um aviso direto basta.

strategy · definição

O que é o Strategy

  • 🔀 Strategy é um padrão comportamental: cada algoritmo vira uma classe própria com um método comum, e um objeto (o Contexto) guarda a estratégia ativa e DELEGA o trabalho a ela.
  • 🎛️ Você troca o COMO — o algoritmo — em tempo de execução, só trocando o objeto de estratégia. Quem chama (o Contexto) não muda; muda o objeto pra quem ele delega.
  • 📦 Algoritmo novo? Uma classe nova de estratégia. O Contexto continua igual: ele só sabe que a estratégia tem o método certo e o chama. Encapsula cada cálculo no seu próprio objeto.

strategy · o problema

O problema do Strategy: o if/elif de algoritmos

  • 🛒 Um carrinho calcula o total com desconto. A solução ingênua: if tipo == "black": total *= 0.7 elif tipo == "cupom": total -= 15... tudo dentro de calcular_total().
  • 🌿 Cada promoção nova é mais um elif. O calcular_total() incha: vira um emaranhado de regras misturadas, e mexer numa promoção arrisca quebrar as outras.
  • 🎯 O que a gente queria: cada regra de desconto morando no SEU próprio objeto, e o carrinho só DELEGANDO o cálculo pra estratégia ativa — trocável a qualquer momento. Esse é o trabalho do Strategy.

strategy · ancorando a ideia

Analogia: o app de rotas escolhendo o trajeto

  • 🗺️ Pense no Waze. O destino é o MESMO, mas você escolhe a estratégia de rota: "mais rápida", "evitar pedágio", "a pé". Cada uma é um algoritmo diferente de calcular o caminho.
  • 🔀 Você troca a estratégia no meio do caminho e o app recalcula — sem trocar de app, sem mudar o destino. O app DELEGA o cálculo do trajeto à estratégia que você escolheu.
  • 🎛️ Strategy é esse seletor no código: o Contexto (o app) guarda a estratégia ativa (o tipo de rota) e delega a ela. Trocar o COMO é só trocar o objeto da estratégia.

strategy · o problema

Antes e depois do Strategy

O mesmo carrinho, dois jeitos de aplicar desconto: um if/elif que incha (o problema) e cada regra num objeto trocável a que o carrinho delega (o Strategy).

Antes — if/elif inchando

antes.py
def total_final(self):
    if self.tipo == "black":
        return self.total * 0.70
    elif self.tipo == "cupom":
        return self.total - 15.00
    elif self.tipo == "nenhum":
        return self.total
    # promoção nova? mais um elif aqui.

Depois — delega à estratégia

depois.py
def total_final(self):
    # delega o cálculo à estratégia ativa
    return self.desconto.calcular(self.total)

# trocar o COMO em runtime:
carrinho.desconto = DescontoBlackFriday()
# promoção nova? uma CLASSE nova.
# o total_final() não muda.
Strategy tira o if/elif de dentro do total_final(): cada desconto vira uma classe com calcular(valor), e o carrinho só DELEGA à estratégia ativa. Trocar a promoção = trocar o objeto (carrinho.desconto = ...). O método que calcula o total nunca mais muda.

strategy · o código

Strategy em Python: Carrinho e Descontos

Uma ABC Desconto (aula 10) com calcular(valor) abstrato — o contrato comum. Cada promoção é uma subclasse. O Carrinho é o Contexto: guarda a estratégia ativa (self.desconto) e DELEGA a ela. Trocar a estratégia em runtime é só reatribuir o atributo.

📦
self.desconto guarda a estratégia ATIVA — uma só por vez. O Carrinho COMPÕE a estratégia (composição, aula 05): tem-uma, e delega o cálculo a ela.
🔀
carrinho.desconto = DescontoBlackFriday() troca o algoritmo em tempo de execução — sem mexer no total_final(). Mesma chamada, resultado diferente: polimorfismo (aula 09).
carrinho.py
from abc import ABC, abstractmethod

class Desconto(ABC):
    @abstractmethod
    def calcular(self, valor):
        pass

class SemDesconto(Desconto):
    def calcular(self, valor):
        return valor

class DescontoBlackFriday(Desconto):
    def calcular(self, valor):
        return valor * 0.70

class DescontoCupom(Desconto):
    def calcular(self, valor):
        return valor - 15.00

class Carrinho:                       # o Contexto
    def __init__(self, total, desconto):
        self.total = total
        self.desconto = desconto      # composição: COMPÕE a estratégia ativa
    def total_final(self):
        return self.desconto.calcular(self.total)  # DELEGA à estratégia

carrinho = Carrinho(100.00, SemDesconto())
print(f"Sem desconto:  R$ {carrinho.total_final():.2f}")
carrinho.desconto = DescontoBlackFriday()   # troca o COMO em runtime
print(f"Black Friday:  R$ {carrinho.total_final():.2f}")
carrinho.desconto = DescontoCupom()         # troca de novo
print(f"Cupom:         R$ {carrinho.total_final():.2f}")
O total_final() não tem nenhum if de promoção: ele só delega à estratégia ativa. Promoção nova (ex.: DescontoProgressivo) = subclasse nova de Desconto; o Carrinho não muda. É a diferença com a aula 20: lá a Factory escolhia qual objeto CRIAR; aqui o Strategy escolhe qual algoritmo USAR num objeto que já existe.

strategy · o código

Saída no terminal

Rodando o carrinho.py, o MESMO carrinho devolve três totais diferentes — só trocando a estratégia entre as chamadas:

🔀
Um carrinho só, três resultados: trocamos self.desconto entre cada total_final(). O Contexto não mudou — mudou a estratégia a quem ele delega.
🧹
Nenhum if tipo == ... apareceu. Cada cálculo ficou encapsulado na sua classe de desconto, e o carrinho só chamou calcular().
terminal
Sem desconto:  R$ 100.00
Black Friday:  R$ 70.00
Cupom:         R$ 85.00
70.00 = 100 × 0.70 (Black Friday); 85.00 = 100 − 15 (Cupom). É a solução do problema do s16: cada regra no seu objeto, trocável em runtime, sem inchar o total_final().

strategy · na UML

O Strategy em UML

O Carrinho e os descontos que acabamos de codar: o Contexto que compõe UMA estratégia trocável de uma hierarquia de algoritmos.

Diagrama UML do Strategy: a classe Carrinho (com total, desconto e total_final()) ligada por uma seta de losango vazio a Desconto, indicando que o carrinho compõe e delega a uma estratégia; Desconto é uma classe abstrata com o método calcular(), e SemDesconto, DescontoBlackFriday e DescontoCupom herdam dela por setas de triângulo vazio.
Leia o diagrama: Carrinho ◆── Desconto (composição: o Contexto guarda UMA estratégia) e Desconto «abstract» △ as três promoções (herança: cada algoritmo implementa calcular). Compare com o Observer (s11): lá o losango ◆ aponta pra uma LISTA de observers; aqui aponta pra UMA estratégia ativa. Essa é a diferença visual entre os dois.

strategy · onde você já viu (sem saber o nome)

Ecossistema: vocês já usaram Strategy na aula 18

O sorted(lista, key=...) da aula 18 é Strategy puro. A função que você passa em key= É a estratégia (o algoritmo de ordenação); o sorted é o Contexto que delega a ela. Trocar a key é trocar a estratégia.

💡
Trocar key=lambda p: p["preco"] por key=lambda p: p["nome"] troca o ALGORITMO de ordenação sem mudar quem chama (sorted). É exatamente o Strategy.
🧠
Vocês usaram Strategy na aula 18 sem saber o nome. Hoje a gente só deu nome ao que já fazia: encapsular um algoritmo e passá-lo como valor pra quem vai usá-lo.
sorted_strategy.py
produtos = [
    {"nome": "Teclado", "preco": 120},
    {"nome": "Mouse", "preco": 80},
    {"nome": "Monitor", "preco": 950},
]

# a key É a estratégia; o sorted é o Contexto que delega a ela
por_preco = sorted(produtos, key=lambda p: p["preco"])
por_nome = sorted(produtos, key=lambda p: p["nome"])

print([p["nome"] for p in por_preco])   # ['Mouse', 'Teclado', 'Monitor']
print([p["nome"] for p in por_nome])    # ['Monitor', 'Mouse', 'Teclado']
A ponte com funções como valor (aula 18): em Python, a estratégia nem sempre precisa virar classe. Quando o algoritmo não tem estado, uma função basta — é o que o próximo slide mostra.

strategy · variação pythônica

Strategy mais leve: a estratégia pode ser só uma função

Em Python, quando a estratégia não tem estado, ela pode ser apenas uma FUNÇÃO passada como valor (aula 18) — sem precisar de uma classe inteira. O Contexto recebe a função e a chama.

🪶
Aqui a estratégia é a função black_friday, passada como argumento. O total_final é o Contexto: recebe a função e a chama. Sem ABC, sem subclasse — leve e direto.
⚖️
Regra prática: se a estratégia tem ESTADO (ex.: a taxa de um cupom), suba pra classe (s19). Se é só um cálculo sem estado, uma função basta. Python deixa as duas formas à mão.
estrategia_funcao.py
def black_friday(valor):
    return valor * 0.70

def total_final(valor, estrategia):
    return estrategia(valor)        # delega à função-estratégia

print(f"R$ {total_final(100.0, black_friday):.2f}")   # R$ 70.00
As duas formas são Strategy: a versão com classe (s19) e a versão com função (aqui). A classe paga seu preço quando a estratégia precisa guardar dados; a função ganha quando é só um algoritmo puro. Escolha pela necessidade, não pela cerimônia.

strategy · amarrando com a POO

Conexão: o Strategy só junta o que você já sabe

  • 🧬 ABC + @abstractmethod (aula 10): Desconto é o contrato — toda estratégia PRECISA ter calcular(). O Contexto confia nesse contrato pra delegar a qualquer uma.
  • 🎭 Polimorfismo (aula 09): self.desconto.calcular(self.total) chama o método certo da estratégia ativa, sem isinstance. Mesma chamada, resultados diferentes.
  • 🧱 Composição (aula 05): o Carrinho COMPÕE a estratégia — tem-uma, trocável. E funções como valor (aula 18): a estratégia pode ser só uma função.
  • 🏭 Contraste com a aula 20: a Factory escolhe qual objeto CRIAR; o Strategy escolhe qual algoritmo USAR num objeto que já existe. Criar ≠ comportar-se.

strategy · quando NÃO usar

Armadilha: nem toda variação vira Strategy

  • 🪶 Se o cálculo NUNCA varia — é sempre o mesmo —, não crie uma hierarquia de estratégias pra um if que não existe. Strategy é pra QUANDO o algoritmo precisa trocar.
  • ⚖️ Se a estratégia não tem estado, em Python uma FUNÇÃO basta (s23). Não suba pra classe só por cerimônia — uma classe vazia que só embrulha um cálculo é over-engineering.
  • 🚫 Não confunda com Observer: Strategy tem UMA estratégia ativa por vez e DELEGA a ela. Observer tem MUITOS interessados e AVISA todos. Um delega; o outro notifica.

fixando os dois

A frase-âncora: Observer vs Strategy

Os dois são comportamentais e vivem de ABC + polimorfismo + composição. A diferença é a INTENÇÃO. Grave: Observer = quando algo muda, AVISA vários; Strategy = troca o COMO sem mudar quem chama.

Observer — AVISA vários

observer.txt
Quando algo muda, AVISA vários
interessados de uma vez.

1 Subject  →  MUITOS observers
Mecanismo: uma LISTA + notificar
(for obs: obs.atualizar(...))

Exemplo: Canal avisa inscritos;
cliques de botão na GUI.

Strategy — troca o COMO

strategy.txt
Troca o COMO — o algoritmo —
sem mudar quem chama.

1 Contexto  →  1 estratégia trocável
Mecanismo: um ATRIBUTO + delegar
(self.estrategia.calcular(...))

Exemplo: Carrinho delega o desconto;
sorted(key=...) da aula 18.
A pergunta que separa os dois: ele AVISA uma lista quando muda de estado? É Observer. Ele DELEGA um cálculo a um objeto trocável? É Strategy. Os dois usam ABC + polimorfismo + composição — muda a intenção: notificar muitos vs. delegar a um.

ao vivo · vamos construir juntos

Exercício ao Vivo: um pedido com os dois padrões

Partindo do diagrama do próximo slide, a gente implementa juntos um pedido de e-commerce que usa Strategy (pra calcular o frete por estratégia trocável) e Observer (pra avisar os interessados quando o status muda). Acompanhem a tradução de cada parte pra código, camada por camada.

1

Leia o diagrama

Frete «abstract» △ FreteNormal/FreteExpresso (a estratégia de frete). Observador «abstract» △ PainelLojista/SMSCliente (os interessados). Pedido ◆── compõe um frete (Strategy) E uma lista de observadores (Observer).

2

Camada 1 — a Strategy (frete trocável)

Frete(ABC) com @abstractmethod calcular(peso), e FreteNormal (peso * 2.00) e FreteExpresso (peso * 2.00 + 20.00) implementando cada um o seu cálculo.

3

Camada 2 — os Observers (interessados)

Observador(ABC) com @abstractmethod atualizar(pedido, status), e PainelLojista e SMSCliente, cada um reagindo do seu jeito ao novo status.

4

Camada 3 — o Pedido (Contexto + Subject)

Pedido guarda self.frete (a estratégia) e self.observadores = [] (a lista). valor_frete() DELEGA ao frete; mudar_status() AVISA todos os observadores. Um objeto, os dois papéis.

ao vivo · vamos construir juntos

O diagrama do nosso pedido

Partimos deste diagrama. Leia as caixas e os estereótipos — e nos próximos slides a gente traduz cada parte pra código, camada por camada.

Diagrama UML do exercício: a classe Pedido (com codigo, peso, frete, observadores, inscrever(), valor_frete() e mudar_status()) ligada por uma seta de losango vazio a Frete e outra a Observador; Frete é classe abstrata com calcular() e tem FreteNormal e FreteExpresso herdando; Observador é classe abstrata com atualizar() e tem PainelLojista e SMSCliente herdando.
Duas ideias no mesmo diagrama: Pedido ◆── Frete «abstract» △ Normal/Expresso é o Strategy (o pedido DELEGA o cálculo ao frete ativo); Pedido ◆── Observador «abstract» △ Painel/SMS é o Observer (o pedido AVISA a lista quando o status muda). Nos próximos slides, uma camada de cada vez.

resolução · camada 1

A Strategy: Frete (ABC) e as subclasses

Começamos pela estratégia de frete: a ABC Frete com calcular(peso) abstrato e as duas formas de cobrar. É o triângulo △ da esquerda do diagrama virando código.

📐
Frete(ABC) com @abstractmethod é o «abstract» do diagrama (aula 10): define o contrato calcular(peso), comum a todas as estratégias de frete.
🔀
Cada subclasse cobra de um jeito — o expresso soma R$ 20,00 fixos. É a estratégia que o Pedido vai trocar em runtime, logo à frente.
pedido.py
from abc import ABC, abstractmethod

# Camada 1: Strategy — cálculo de frete (trocável)
class Frete(ABC):
    @abstractmethod
    def calcular(self, peso):
        pass

class FreteNormal(Frete):
    def calcular(self, peso):
        return peso * 2.00

class FreteExpresso(Frete):
    def calcular(self, peso):
        return peso * 2.00 + 20.00
Se você tentar Frete() direto, o Python 3.9 recusa: TypeError: Can't instantiate abstract class Frete with abstract method calcular — o contrato da ABC sendo cobrado. Só as subclasses concretas podem ser instanciadas.

resolução · camada 2

Os Observers: Observador (ABC) e as subclasses

Agora os interessados: a ABC Observador com atualizar(pedido, status) abstrato, e os dois que vão reagir quando o status mudar. É o triângulo △ da direita do diagrama.

📐
Observador(ABC) define o contrato atualizar(pedido, status): todo observer sabe reagir a um aviso de mudança de status — cada um do seu jeito.
🎭
PainelLojista escreve no painel interno; SMSCliente manda um SMS pro cliente. Mesma chamada, reações diferentes — o polimorfismo que o Subject vai explorar.
pedido.py
# Camada 2: Observer — os interessados no status
class Observador(ABC):
    @abstractmethod
    def atualizar(self, pedido, status):
        pass

class PainelLojista(Observador):
    def atualizar(self, pedido, status):
        print(f"[Painel] Pedido {pedido} -> {status}")

class SMSCliente(Observador):
    def atualizar(self, pedido, status):
        print(f"[SMS] Seu pedido {pedido} agora está: {status}")
Repare que o atualizar() recebe o código do pedido e o novo status: o aviso carrega a informação que mudou. Observer novo (ex.: EmailLojista) = subclasse nova com atualizar(); o Pedido não muda.

resolução · camada 3

O Pedido: Contexto (Strategy) + Subject (Observer)

A última camada junta os dois papéis num objeto só. O Pedido COMPÕE um frete (a estratégia, a que ele delega) e uma lista de observadores (os interessados, que ele avisa).

🔀
valor_frete() é o lado Strategy: DELEGA o cálculo a self.frete. Trocar a estratégia é pedido.frete = FreteExpresso() — sem mexer neste método.
📣
mudar_status() é o lado Observer: percorre self.observadores e AVISA todos. Um Subject, muitos observers — sem conhecer cada um pelo tipo.
pedido.py
# Camada 3: o Pedido é Contexto (Strategy) E Subject (Observer)
class Pedido:
    def __init__(self, codigo, peso, frete):
        self.codigo = codigo
        self.peso = peso
        self.frete = frete            # Strategy: a estratégia ativa
        self.observadores = []        # Observer: a lista de interessados
    def inscrever(self, obs):
        self.observadores.append(obs)
    def valor_frete(self):
        return self.frete.calcular(self.peso)   # DELEGA ao frete (Strategy)
    def mudar_status(self, status):
        for obs in self.observadores:           # AVISA todos (Observer)
            obs.atualizar(self.codigo, status)
Um objeto, dois papéis: como Contexto (Strategy), o Pedido delega o frete a UMA estratégia; como Subject (Observer), ele avisa MUITOS observadores. É a diferença dos dois padrões convivendo na mesma classe — delegar a um vs. notificar vários.

resolução · o código completo

Tudo junto: a resolução completa

As três camadas num arquivo só — é exatamente isto que você roda. Repare como Strategy e Observer convivem: o pedido delega o frete à estratégia ativa e avisa os observadores quando o status muda.

🔀
pedido.frete = FreteExpresso() é o Strategy em ação: o frete recalcula sem mexer no valor_frete(). O Contexto delega à estratégia ativa.
📣
pedido.mudar_status("ENVIADO") é o Observer em ação: um aviso só dispara as reações do painel e do SMS. A saída exata vem no próximo slide.
pedido.py
from abc import ABC, abstractmethod

# Camada 1: Strategy — cálculo de frete
class Frete(ABC):
    @abstractmethod
    def calcular(self, peso):
        pass

class FreteNormal(Frete):
    def calcular(self, peso):
        return peso * 2.00

class FreteExpresso(Frete):
    def calcular(self, peso):
        return peso * 2.00 + 20.00

# Camada 2: Observer — os interessados no status
class Observador(ABC):
    @abstractmethod
    def atualizar(self, pedido, status):
        pass

class PainelLojista(Observador):
    def atualizar(self, pedido, status):
        print(f"[Painel] Pedido {pedido} -> {status}")

class SMSCliente(Observador):
    def atualizar(self, pedido, status):
        print(f"[SMS] Seu pedido {pedido} agora está: {status}")

# Camada 3: o Pedido é Contexto (Strategy) E Subject (Observer)
class Pedido:
    def __init__(self, codigo, peso, frete):
        self.codigo = codigo
        self.peso = peso
        self.frete = frete
        self.observadores = []
    def inscrever(self, obs):
        self.observadores.append(obs)
    def valor_frete(self):
        return self.frete.calcular(self.peso)
    def mudar_status(self, status):
        for obs in self.observadores:
            obs.atualizar(self.codigo, status)

# Tudo junto: Strategy delega o frete, Observer avisa o status
pedido = Pedido("A-100", 3.0, FreteNormal())
pedido.inscrever(PainelLojista())
pedido.inscrever(SMSCliente())

print(f"Frete normal: R$ {pedido.valor_frete():.2f}")
pedido.frete = FreteExpresso()                # troca a estratégia em runtime
print(f"Frete expresso: R$ {pedido.valor_frete():.2f}")

pedido.mudar_status("ENVIADO")                 # avisa todos os observadores
Dois padrões num objeto só: o Strategy decide COMO calcular o frete (estratégia trocável); o Observer decide QUEM avisar quando o status muda (lista de interessados). Cada um resolve uma pergunta diferente, e juntos formam o pedido.

resolução · saída esperada

A saída no terminal

Rodando o pedido.py, é isto que aparece no terminal:

🔀
As duas primeiras linhas são o Strategy: 6.00 = 3.0 × 2.00 (normal) e 26.00 = 3.0 × 2.00 + 20.00 (expresso). O mesmo pedido, frete trocado em runtime.
📣
As duas últimas são o Observer: um mudar_status("ENVIADO") disparou o painel E o SMS. Um aviso, dois interessados reagindo, cada um do seu jeito.
terminal
Frete normal: R$ 6.00
Frete expresso: R$ 26.00
[Painel] Pedido A-100 -> ENVIADO
[SMS] Seu pedido A-100 agora está: ENVIADO
As quatro linhas provam os dois padrões: Strategy (dois fretes do mesmo pedido) e Observer (um status avisando dois observadores). É o que você vai replicar — em outros domínios — nos exercícios.

recapitulando

Glossário rápido de hoje

item detalhe
Padrão comportamental A família que cuida de COMO os objetos se comunicam e dividem responsabilidades. Observer e Strategy estão aqui. Contraste: os criacionais (aula 20) cuidam de COMO criar objetos.
Observer Padrão comportamental: um Subject avisa vários Observers quando muda de estado. Um para muitos. Quando algo muda, AVISA vários interessados.
Subject (Observável) No Observer, o objeto observado: mantém a lista de observers e os notifica ao mudar (ex.: o Canal, o Pedido). Tem inscrever()/cancelar() e notifica a lista.
notificar / atualizar notificar = o Subject percorre a lista e chama o método de aviso; atualizar() = o método que cada Observer implementa pra reagir ao aviso.
Strategy Padrão comportamental: encapsula cada algoritmo numa classe e deixa o Contexto trocar de algoritmo em runtime. Troca o COMO sem mudar quem chama.
Contexto No Strategy, o objeto que guarda a estratégia ativa e DELEGA a ela (ex.: o Carrinho, o sorted). Não implementa o algoritmo — só chama o da estratégia.
estratégia intercambiável Cada algoritmo num objeto com o mesmo método comum, trocável a qualquer momento (carrinho.desconto = ...). Pode ser uma classe ou, sem estado, só uma função.
delegar No Strategy, o Contexto não calcula — passa o trabalho à estratégia ativa (self.desconto.calcular(...)). Delegar a UM ≠ notificar VÁRIOS (Observer).
composição A base dos dois (aula 05): o Subject COMPÕE a lista de observers; o Contexto COMPÕE a estratégia ativa. Tem-um, não é-um.

O que dominamos hoje

  • Sei o que distingue a família dos padrões comportamentais dos criacionais (aula 20): comunicação e responsabilidade vs. criação.
  • Explico o problema que o Observer resolve: avisos chumbados e acoplados dentro do código, em vez de uma lista de interessados.
  • Implemento um Observer: um Subject com lista de observers + um método que percorre e chama atualizar() em todos, por polimorfismo, e um cancelar() pra evitar vazamento.
  • Reconheço que os callbacks de GUI são Observer: ligar uma função a um botão (command=) é inscrever um observer — a base da interface da aula 22.
  • Explico o problema que o Strategy resolve: o if/elif de algoritmos inchando um método, em vez de cada algoritmo num objeto trocável.
  • Implemento um Strategy: uma ABC de algoritmo + um Contexto que guarda a estratégia ativa e DELEGA a ela, trocando o COMO em runtime — e sei que o sorted(key=) da aula 18 já era Strategy.
  • Sei a frase-âncora dos dois: Observer = quando algo muda, AVISA vários interessados; Strategy = troca o COMO (o algoritmo) sem mudar quem chama.

exercícios

Mãos à obra: os exercícios

Quatro exercícios em três níveis: um de Observer, um de Strategy, um de Observer a partir de um diagrama, e um desafio que junta os dois padrões. Cada um é INDIVIDUAL e termina em arquivo entregável. Comece em sala; termine o que faltar em casa. Mire pelo menos um de cada nível.

01

Fácil · Observer de estoque

Crie uma classe Estoque (o Subject) com uma lista de assinantes e um método repor(produto) que AVISA todos quando um produto volta ao estoque. Faça uma ABC Assinante com o método abstrato atualizar(produto) e duas subclasses — AssinanteEmail e AssinanteApp — cada uma guardando um nome no __init__ e imprimindo do seu jeito (ex.: [E-mail] Fulano: Notebook voltou ao estoque!). Inclua um método inscrever() e um bloco de teste que inscreve 2 assinantes e chama repor("Notebook") uma vez (os dois devem ser avisados, por loop polimórfico). Entregue o arquivo .py. Foco: Subject com lista + notificar todos por polimorfismo.

02

Fácil/Médio · Strategy de frete

Crie uma ABC CalculoFrete com o método abstrato calcular(valor_compra) e três estratégias: FreteGratis (devolve o valor sem alterar), FretePorRegiao(taxa) (soma uma taxa fixa recebida no __init__) e FretePercentual (soma 5% do valor). Crie um Checkout (o Contexto) que recebe uma estratégia e tem total(valor_compra) delegando a ela. No bloco de teste, troque a estratégia 3 vezes no MESMO checkout sobre uma compra de R$ 200,00. Saída de referência: Grátis R$ 200.00 · Por região (taxa 25) R$ 225.00 · Percentual R$ 210.00. Entregue o arquivo .py. Foco: Contexto que delega + estratégia trocável em runtime.

03

Médio · Observer: do diagrama ao código

A partir do diagrama do próximo slide — um Leilao (o Subject) com uma lista de participantes, inscrever() e dar_lance(valor) que NOTIFICA todos os participantes; e Participante «abstract» com atualizar(valor), com ParticipanteApp e ParticipanteEmail ──△ Participante (herança), cada um reagindo do seu jeito (ex.: [App] Ana: novo lance de R$ 500.00) — implemente todas as classes em Python. Inclua um bloco de teste que inscreve 2 participantes e dá um lance (loop polimórfico — o leilão não sabe o tipo de cada participante). Entregue o arquivo .py. Foco: Observer de verdade — o Subject avisa a lista, cada observer reage por polimorfismo.

04

Desafio · Observer + Strategy juntos

Escolha UM mini sistema: (a) uma estação meteorológica que AVISA vários displays quando a temperatura muda [Observer] e converte a temperatura entre °C e °F por estratégia trocável [Strategy] — a estação acumula os dois papéis, Subject e Contexto, como o Pedido fez em s31; ou (b) um jogo onde um evento AVISA vários sistemas (HUD, som, log) [Observer] e o dano é calculado por uma fórmula trocável (físico, mágico, crítico) [Strategy]. Primeiro DESENHE o diagrama de classes numa ferramenta (draw.io ou PlantUML) com: 1 Subject + observers (ABC) e 1 Contexto + estratégia (ABC), ambos com pelo menos 2 subclasses concretas (herança △). Depois IMPLEMENTE a partir do SEU diagrama: ABC com @abstractmethod nos dois contratos, __str__ nos produtos/observers pra identificar quem reagiu ao imprimir, e um teste que prova o Observer (um aviso → vários reagindo) e o Strategy (mesma operação, estratégia trocada em runtime), tudo por uso polimórfico sem isinstance. Entregue o diagrama em PDF e o código no arquivo .py. Foco: combinar Observer e Strategy no mesmo sistema (preparação pro projeto final da aula 25).

exercício 3 · do diagrama ao código

O diagrama do Exercício 3 — Leilão

Este é o diagrama do exercício 3. Leia a relação Subject↔Observers — o Leilao e os Participantes — e traduza cada parte pra código Python.

Diagrama UML de Observer: a classe Leilao (com item, participantes, inscrever() e dar_lance()) ligada por uma seta de losango vazio a Participante, indicando que o leilão compõe uma lista de participantes e os notifica; Participante é classe abstrata com atualizar(), e ParticipanteApp e ParticipanteEmail herdam dela por setas de triângulo vazio.
É a mesma forma do Observer do Canal (s11): um Subject (Leilao) que ◆── compõe uma lista de observers (Participante «abstract» △ App/Email) e os AVISA ao dar_lance(). O losango ◆ é o leilão conhecendo seus participantes; cada um implementa atualizar(valor) do seu jeito.

Antes de entregar: confira

  • Meu Observer prova que VÁRIOS são avisados com um único disparo, e tem cancelar()/remover pra não vazar observers da lista.
  • Meu Strategy troca o algoritmo SEM mudar o Contexto: a mesma operação dá resultados diferentes só trocando a estratégia ativa.
  • Os dois contratos (Observer e Strategy) são ABCs com @abstractmethod, e as subclasses os implementam.
  • O uso é polimórfico: percorro a lista / delego à estratégia sem nenhum isinstance ou if por tipo.
  • Os nomes, strings e comentários estão em PT-BR, e o arquivo .py roda do começo ao fim sem erro.
  • No desafio, entreguei o diagrama em PDF E o código .py, com 1 Subject+observers e 1 Contexto+estratégia, e o diagrama bate com o que implementei.
próxima aula

Aula 22 · 06/07

Interface gráfica com Tkinter — parte 1

Vimos que ligar uma função a um botão é inscrever um observer. Na próxima, a gente coloca isso na tela: o Tkinter, a biblioteca de interface gráfica do Python. O command= de cada botão é exatamente o Observer de hoje — registrar quem reage ao clique.

Tkinter eventos interface gráfica