Trilha de aprendizado · Nível 7 · Tutorial 3

Preservar regras de validade em objetos

Modelar objetos que mantêm suas regras de validade desde a inicialização até as mudanças de estado, oferecendo operações com responsabilidades claras.

  • Nível: Intermediário
  • Duração: 20 min
  • 8 passos
Preservar regras de validade em objetos

O que você vai percorrer

  1. Definir o que torna um objeto válido Reconheça as invariantes da classe Turma e use essas condições para distinguir estados válidos de inválidos. 2 min
  2. Oferecer operações, não alterações arbitrárias Defina uma interface pública baseada em ações do domínio e diferencie-a dos detalhes internos da classe. 2 min
  3. Estabelecer um estado inicial válido Valide todos os dados e suas relações antes de armazenar o estado inicial de uma Turma. 2 min
  4. Validar cada mudança de estado Implemente operações públicas que validam o argumento e o estado atual antes de modificar a instância. 3 min
  5. Rejeitar sem deixar alterações pela metade Prepare e valide o estado completo proposto antes de substituir os atributos da instância. 3 min
  6. Concentrar regras em um método interno Reutilize as invariantes na inicialização e nas mudanças de estado, mantendo separadas a validação e a alteração dos atributos. 2 min
  7. Impedir que coleções contornem as regras Controle as referências recebidas e devolvidas pela classe para impedir mudanças externas no estado interno. 3 min
  8. Aplicar e revisar a preservação das regras Implemente uma operação de substituição e confirme que mudanças válidas são aplicadas, mudanças inválidas preservam o estado anterior e listas externas não alteram a coleção interna. 4 min

O que você vai aprender

  • Expressar as condições que caracterizam um estado válido para uma classe.
  • Validar a inicialização e as operações que podem alterar essas condições.
  • Rejeitar uma operação inválida sem deixar a instância parcialmente atualizada.
  • Distinguir a interface pública dos detalhes internos e evitar expor coleções que permitam contornar as regras.

Antes de começar

  • Criar objetos com classes e métodos de instância
  • Distinguir atributos de instância e de classe
  • Validar entradas e sinalizar falhas com raise
  • Controlar referências e cópias de coleções

Passo 1 de 8

Definir o que torna um objeto válido

Reconheça as invariantes da classe Turma e use essas condições para distinguir estados válidos de inválidos.

Regras que devem permanecer verdadeiras

Invariantes do objeto

Uma invariante é uma condição que caracteriza todo estado válido de um objeto. Ela deve ser verdadeira logo após a inicialização e continuar verdadeira depois de cada operação pública bem-sucedida.

Considere uma classe Turma com dois dados: uma capacidade e uma lista de identificadores de inscritos. A própria classe deve manter suas invariantes, em vez de depender apenas do cuidado de quem a utiliza.

Anatomia de um estado válido

O estado da turma combina uma capacidade com a coleção de identificadores inscritos.

Diagrama de uma turma com capacidade para três pessoas e dois identificadores distintos e preenchidos, mostrando que a quantidade está dentro do limite.

A validade depende tanto de cada dado quanto da relação entre eles.

As invariantes da Turma

Condições verificáveis

Neste exemplo, um estado é válido somente quando todas estas condições são verdadeiras:

  • a capacidade é um número inteiro positivo, e bool não é aceito;
  • cada identificador é uma str não vazia;
  • não há identificadores duplicados;
  • a quantidade de inscritos é menor ou igual à capacidade.

As três primeiras condições verificam atributos ou elementos isoladamente. A última relaciona duas partes do estado: a lista pode conter identificadores perfeitamente válidos e, ainda assim, ser grande demais para a capacidade.

Exemplo

Estados válidos e inválidos

Válido: capacidade 3; inscritos ["A17", "B04"].

Inválido por um valor isolado: capacidade True; inscritos []. Embora bool tenha relação com int em Python, a regra da turma o rejeita.

Inválido por duplicação: capacidade 3; inscritos ["A17", "A17"].

Inválido pela relação entre atributos: capacidade 1; inscritos ["A17", "B04"]. Cada atributo parece aceitável quando observado sozinho, mas há dois inscritos para uma única vaga.

Identifique a regra violada

Validade do estado

Uma turma tem capacidade 2 e inscritos ["A17", "B04", "C91"]. Qual invariante é violada?

Passo 2 de 8

Oferecer operações, não alterações arbitrárias

Defina uma interface pública baseada em ações do domínio e diferencie-a dos detalhes internos da classe.

Ações com intenção explícita

A classe controla as mudanças

As invariantes da Turma podem ser contornadas se qualquer código alterar livremente sua capacidade ou sua lista de inscritos. Em vez disso, a classe deve oferecer operações que expressem intenções do domínio: inscrever, cancelar_inscricao e alterar_capacidade.

Cada operação se torna um ponto controlado para decidir se a mudança é permitida. O código consumidor pede uma ação; a própria Turma fica responsável por preservar seu estado válido.

Uma fronteira para o estado

As operações públicas funcionam como entradas controladas para consultar ou modificar o objeto. O estado interno não deve ser o caminho normal de uso da classe.

Diagrama de uma Turma delimitada por uma fronteira, com capacidade e inscritos no interior e operações controladas atravessando a fronteira.

A interface pública recebe as solicitações; os detalhes internos permanecem sob controle da classe.

Interface pública e detalhes internos

Como o código consumidor conversa com a Turma

A interface pública é o conjunto de operações e consultas que outros trechos do programa devem utilizar. Métodos como inscrever() expressam mudanças; consultas como consultar_capacidade() e quantidade_inscritos() permitem observar o estado.

Atributos como _capacidade e _inscritos representam detalhes internos. O prefixo _ comunica: “este nome é para uso interno da classe”.

Uso pela interface pública

Este trecho destaca as chamadas que o código consumidor deve fazer. A implementação das validações será construída nos próximos steps.

python
turma = Turma(30)

# Operações públicas: expressam intenções do domínio.
turma.inscrever("ana")
turma.cancelar_inscricao("ana")
turma.alterar_capacidade(35)

# Consultas públicas: observam o estado.
print(turma.consultar_capacidade())
print(turma.quantidade_inscritos())

# Evite manipular diretamente os detalhes internos:
# turma._capacidade = 0
# turma._inscritos.append("")

Dica

Sublinhado é convenção, não bloqueio

Python permite acessar turma._capacidade deliberadamente. O prefixo _ não oferece segurança nem impede alterações: ele apenas sinaliza que o código consumidor não deve depender desse detalhe. A preservação das regras vem do uso disciplinado da interface pública e das validações implementadas pela classe.

Reconheça a fronteira

Interface ou detalhe?

Associe cada nome ao seu papel na classe Turma.

Toque em um item e depois no par correspondente.

Por que evitar a alteração direta?

A instrução turma._capacidade = 0 pode contornar as invariantes porque modifica um detalhe interno sem passar por uma operação pública de validação.

Passo 3 de 8

Estabelecer um estado inicial válido

Valide todos os dados e suas relações antes de armazenar o estado inicial de uma Turma.

Validar antes de estabelecer o estado

O contrato começa em __init__

As invariantes da Turma já devem valer quando a inicialização termina: a capacidade precisa ser um inteiro positivo, sem aceitar bool; os inscritos devem formar uma lista de identificadores textuais não vazios e sem duplicatas; e a quantidade não pode superar a capacidade.

Por isso, __init__ deve primeiro validar a capacidade, os identificadores e a relação entre eles. Somente depois ele atribui _capacidade e _inscritos.

Da entrada ao objeto válido

A inicialização funciona como uma fronteira: os dados recebidos atravessam todas as verificações antes de formar o estado da instância.

Diagrama em que uma capacidade e vários cartões de inscritos passam por verificações; uma combinação válida chega a um objeto completo, enquanto uma combinação inválida é rejeitada antes dele.

Nenhuma parte do estado é estabelecida antes de todas as invariantes serem confirmadas.

Uma inicialização completa

Validar candidatos e só então atribuir

A lista candidata é própria da instância. As exceções distinguem tipos inadequados de valores que violam as regras.

python
class Turma:
    def __init__(self, capacidade, inscritos):
        if isinstance(capacidade, bool) or not isinstance(capacidade, int):
            raise TypeError("capacidade deve ser um número inteiro, sem aceitar bool")
        if capacidade <= 0:
            raise ValueError("capacidade deve ser positiva")

        if not isinstance(inscritos, list):
            raise TypeError("inscritos deve ser uma lista")

        inscritos_candidatos = list(inscritos)

        for identificador in inscritos_candidatos:
            if not isinstance(identificador, str):
                raise TypeError("cada identificador deve ser um texto")
            if identificador == "":
                raise ValueError("identificadores não podem ser vazios")

        if len(set(inscritos_candidatos)) != len(inscritos_candidatos):
            raise ValueError("identificadores não podem estar duplicados")

        if len(inscritos_candidatos) > capacidade:
            raise ValueError("quantidade de inscritos excede a capacidade")

        self._capacidade = capacidade
        self._inscritos = inscritos_candidatos

Exemplo

Quando a inicialização é rejeitada

Neste caso, há dois inscritos para apenas uma vaga:

try:
    turma = Turma(1, ["ana", "bia"])
except ValueError as erro:
    print(erro)

Resultado:

quantidade de inscritos excede a capacidade

Como __init__ sinalizou a falha, a chamada não terminou normalmente e não entregou uma nova Turma pronta para uso ao código chamador.

Dica

A ordem protege o estado inicial

Use variáveis locais durante a validação. As atribuições a self ficam no final, depois de todas as verificações.

Confira a estratégia

Qual inicialização preserva todas as regras?

Qual sequência representa corretamente a responsabilidade de __init__ na classe Turma?

Passo 4 de 8

Validar cada mudança de estado

Implemente operações públicas que validam o argumento e o estado atual antes de modificar a instância.

Um valor válido pode formar uma operação inválida

O estado atual também participa da decisão

Um identificador como "bia" pode ser válido isoladamente, mas a inscrição ainda deve ser rejeitada se ele já estiver na turma ou se não houver vaga. Da mesma forma, a capacidade 2 é positiva, porém não pode substituir uma capacidade maior quando já existem três inscritos.

Cada operação pública precisa verificar duas coisas: o argumento recebido e as condições atuais da instância. Somente depois de todas as verificações ela pode alterar o estado.

Validar antes de alterar

A proposta passa por verificações antes de alcançar o estado armazenado. Se alguma regra falhar, o caminho termina sem modificação.

Diagrama com uma proposta de mudança passando por verificações de validade, duplicação e limite antes de alcançar o estado de uma turma.

A alteração acontece apenas no caminho aceito; uma rejeição preserva o estado anterior.

Operações que preservam as regras

Verificar, rejeitar, alterar

Nas operações simples abaixo, cada raise ocorre antes da instrução que modifica o atributo. Assim, uma inscrição duplicada, um cancelamento inexistente ou uma redução incompatível não produz nenhuma mudança.

Métodos públicos da classe Turma

Estes métodos consideram os argumentos e o estado atual da turma.

python
class Turma:
    # __init__ validado no step anterior

    def inscrever(self, identificador):
        if not isinstance(identificador, str):
            raise TypeError("identificador deve ser uma string")
        if not identificador.strip():
            raise ValueError("identificador não pode estar vazio")
        if identificador in self._inscritos:
            raise ValueError("identificador já está inscrito")
        if len(self._inscritos) >= self._capacidade:
            raise ValueError("não há vaga disponível")

        self._inscritos.append(identificador)

    def cancelar(self, identificador):
        if not isinstance(identificador, str):
            raise TypeError("identificador deve ser uma string")
        if not identificador.strip():
            raise ValueError("identificador não pode estar vazio")
        if identificador not in self._inscritos:
            raise ValueError("identificador não está inscrito")

        self._inscritos.remove(identificador)

    def alterar_capacidade(self, nova_capacidade):
        if isinstance(nova_capacidade, bool) or not isinstance(
            nova_capacidade, int
        ):
            raise TypeError("nova capacidade deve ser um inteiro")
        if nova_capacidade <= 0:
            raise ValueError("nova capacidade deve ser positiva")
        if nova_capacidade < len(self._inscritos):
            raise ValueError(
                "nova capacidade não comporta os inscritos atuais"
            )

        self._capacidade = nova_capacidade

Dica

Localize o ponto de alteração

Em cada método, procure a linha que realmente altera _inscritos ou _capacidade. Todas as condições capazes de rejeitar a operação devem aparecer antes dela.

Acompanhar uma sequência de chamadas

Exemplo

O estado muda somente nas chamadas aceitas

Considere uma turma iniciada com capacidade 2 e inscritos ["ana"]:

  1. inscrever("bia") é aceita: há vaga e não existe duplicação. Estado: capacidade 2, inscritos ["ana", "bia"].
  2. inscrever("ana") é rejeitada: o identificador já existe. O estado não muda.
  3. alterar_capacidade(1) é rejeitada: duas inscrições não cabem na nova capacidade. O estado não muda.
  4. cancelar("ana") é aceita. Estado: capacidade 2, inscritos ["bia"].
  5. alterar_capacidade(1) agora é aceita, pois a única inscrição existente cabe no novo limite.

A mesma entrada, outra decisão

Nas chamadas 3 e 5, o argumento é o mesmo: 1. O resultado muda porque a validade da operação depende da ocupação no momento da chamada.

Verifique as condições

Complete a proteção da capacidade

Complete a condição:

if nova_capacidade < ____:

raise ValueError("nova capacidade não comporta os inscritos atuais")

Preveja o estado final

Uma turma começa com capacidade 2 e inscritos ["ana"]. As exceções são capturadas para que a sequência continue:

  1. inscrever("bia")
  2. inscrever("ana")
  3. alterar_capacidade(1)
  4. cancelar("caio")

Qual é o estado final?

Passo 5 de 8

Rejeitar sem deixar alterações pela metade

Prepare e valide o estado completo proposto antes de substituir os atributos da instância.

Uma mudança que afeta o estado inteiro

Reconfigurar exige uma decisão completa

Considere uma operação reconfigurar que recebe, ao mesmo tempo, uma nova capacidade e uma nova lista de inscritos. As duas partes precisam formar juntas um estado válido.

A operação deve primeiro preparar um estado candidato em variáveis locais. Somente depois de validar todas as regras ela pode substituir _capacidade e _inscritos.

Se qualquer regra falhar, uma exceção interrompe o fluxo enquanto o estado anterior da instância permanece intacto.

Aceitação e rejeição de uma proposta

A validação funciona como uma barreira entre a proposta e o estado armazenado.

Diagrama comparando uma proposta aceita, que substitui todo o estado anterior, com uma proposta rejeitada, que mantém o estado anterior intacto.

Uma proposta aceita substitui o conjunto completo; uma proposta rejeitada não altera nenhuma parte do objeto.

O perigo de alterar antes de terminar

Uma atribuição antecipada quebra a proteção

No código abaixo, a capacidade é alterada antes de a lista proposta ser completamente validada. Se uma verificação posterior executar raise, Python interromperá o método, mas não desfará a atribuição já realizada.

Implementação problemática

Esta versão pode deixar a instância parcialmente atualizada.

python
def reconfigurar(self, capacidade, inscritos):
    if isinstance(capacidade, bool) or not isinstance(capacidade, int):
        raise TypeError("capacidade deve ser um inteiro")
    if capacidade <= 0:
        raise ValueError("capacidade deve ser positiva")

    self._capacidade = capacidade  # alteração antecipada

    if not isinstance(inscritos, list):
        raise TypeError("inscritos deve ser uma lista")
    if any(not isinstance(item, str) for item in inscritos):
        raise TypeError("cada identificador deve ser uma string")
    if any(item == "" for item in inscritos):
        raise ValueError("identificadores não podem ser vazios")
    if len(set(inscritos)) != len(inscritos):
        raise ValueError("identificadores não podem se repetir")
    if len(inscritos) > capacidade:
        raise ValueError("quantidade de inscritos excede a capacidade")

    self._inscritos = list(inscritos)

Atenção

raise não desfaz atribuições anteriores

Se a turma tinha capacidade 3 e três inscritos, uma tentativa de mudar para capacidade 2 com uma lista inválida pode alterar _capacidade para 2 e depois falhar. A instância fica com três inscritos para apenas duas vagas, violando suas próprias regras.

Preparar, validar e só então efetivar

Mudança baseada em estado candidato

As variáveis locais recebem a proposta completa. Nenhum atributo é alterado durante a validação.

python
def reconfigurar(self, capacidade, inscritos):
    if isinstance(capacidade, bool) or not isinstance(capacidade, int):
        raise TypeError("capacidade deve ser um inteiro")
    if not isinstance(inscritos, list):
        raise TypeError("inscritos deve ser uma lista")

    nova_capacidade = capacidade
    novos_inscritos = list(inscritos)

    if nova_capacidade <= 0:
        raise ValueError("capacidade deve ser positiva")
    if any(not isinstance(item, str) for item in novos_inscritos):
        raise TypeError("cada identificador deve ser uma string")
    if any(item == "" for item in novos_inscritos):
        raise ValueError("identificadores não podem ser vazios")
    if len(set(novos_inscritos)) != len(novos_inscritos):
        raise ValueError("identificadores não podem se repetir")
    if len(novos_inscritos) > nova_capacidade:
        raise ValueError("quantidade de inscritos excede a capacidade")

    self._capacidade = nova_capacidade
    self._inscritos = novos_inscritos

Exemplo

O estado anterior sobrevive à rejeição

Se o estado atual for capacidade 3 e inscritos ["ana", "bia", "caio"], a proposta 2, ["ana", "ana"] será rejeitada pela duplicação. Como as atribuições aos atributos estão no final, o objeto continuará com capacidade 3 e os mesmos três inscritos.

Já a proposta 2, ["ana", "bia"] passa por todas as verificações e substitui o estado completo.

Dica

A ordem protege a instância

Use a sequência: construir o estado candidato, validar todas as suas partes e relações e, por último, efetivar as atribuições. As variáveis locais podem ser descartadas em caso de erro sem comprometer o objeto.

Verifique a estratégia

Ordene uma mudança segura

Coloque as etapas de uma reconfiguração na ordem que preserva o estado anterior quando ocorre uma rejeição.

  1. Efetivar as atribuições aos atributos da instância.
  2. Construir a capacidade e a lista candidatas em variáveis locais.
  3. Validar os valores e a relação entre quantidade de inscritos e capacidade.

Identifique a alteração parcial

Uma turma começa com capacidade 3 e inscritos ["ana", "bia", "caio"]. A implementação problemática atribui primeiro self._capacidade = 2 e depois rejeita a proposta por conter identificadores duplicados. Como a instância fica?

Passo 6 de 8

Concentrar regras em um método interno

Reutilize as invariantes na inicialização e nas mudanças de estado, mantendo separadas a validação e a alteração dos atributos.

Um único ponto para as invariantes

Validar o estado candidato

Quando várias operações precisam preservar as mesmas invariantes, repetir as verificações em cada método aumenta o risco de inconsistência. Um método interno pode receber explicitamente a capacidade e os inscritos candidatos, sinalizar qualquer falha e terminar sem modificar a instância. Somente depois dessa validação a operação efetiva as atribuições.

Validar antes de efetivar

A inicialização e as operações públicas enviam suas propostas ao mesmo validador. Uma proposta válida pode substituir o estado; uma proposta inválida é rejeitada, preservando o estado anterior.

Fluxo em que diferentes propostas de estado passam por um único ponto de validação antes de alcançar a instância.

O validador examina valores candidatos, não apenas os atributos que já estão armazenados.

Separar validação e alteração

Reutilização das invariantes

O método _validar_estado apenas verifica os valores recebidos e levanta exceções. Ele não atribui valores a _capacidade nem a _inscritos.

python
class Turma:
    def __init__(self, capacidade, inscritos):
        self._validar_estado(capacidade, inscritos)
        self._capacidade = capacidade
        self._inscritos = list(inscritos)

    def _validar_estado(self, capacidade, inscritos):
        if isinstance(capacidade, bool) or not isinstance(capacidade, int):
            raise TypeError("capacidade deve ser um inteiro")
        if capacidade <= 0:
            raise ValueError("capacidade deve ser positiva")
        if not isinstance(inscritos, list):
            raise TypeError("inscritos deve ser uma lista")

        for identificador in inscritos:
            if not isinstance(identificador, str):
                raise TypeError("cada identificador deve ser textual")
            if not identificador.strip():
                raise ValueError("identificadores não podem ser vazios")

        if len(set(inscritos)) != len(inscritos):
            raise ValueError("não pode haver inscrições duplicadas")
        if len(inscritos) > capacidade:
            raise ValueError("quantidade de inscritos excede a capacidade")

    def inscrever(self, identificador):
        candidatos = self._inscritos + [identificador]
        self._validar_estado(self._capacidade, candidatos)
        self._inscritos = candidatos

    def cancelar_inscricao(self, identificador):
        if identificador not in self._inscritos:
            raise ValueError("a inscrição informada não existe")

        candidatos = list(self._inscritos)
        candidatos.remove(identificador)
        self._validar_estado(self._capacidade, candidatos)
        self._inscritos = candidatos

    def alterar_capacidade(self, nova_capacidade):
        self._validar_estado(nova_capacidade, self._inscritos)
        self._capacidade = nova_capacidade

Regras compartilhadas e específicas

Exemplo

Cada regra no lugar adequado

“A capacidade deve ser positiva” e “não pode haver duplicatas” são invariantes do estado completo, por isso pertencem a _validar_estado. Já “a inscrição precisa existir antes do cancelamento” é uma condição específica de cancelar_inscricao: sem ela, remover o item produziria uma falha da implementação ou transformaria o cancelamento em uma operação silenciosa.

Dica

Evite validar somente o estado atual

Se o validador consultasse apenas self._capacidade e self._inscritos, ele não conseguiria rejeitar uma proposta antes da alteração. Receber os candidatos também permite reutilizá-lo durante __init__, quando os atributos ainda não foram estabelecidos.

Verifique a responsabilidade do validador

Classifique e explique

Quais regras da classe Turma devem ficar no validador interno, qual condição deve permanecer especificamente em cancelar_inscricao e por que o validador deve receber valores candidatos sem alterar a instância?

Escreva pelo menos 100 caracteres (0/100).

Passo 7 de 8

Impedir que coleções contornem as regras

Controle as referências recebidas e devolvidas pela classe para impedir mudanças externas no estado interno.

Quando a referência atravessa a interface

Um caminho indireto para alterar o estado

Validar as operações públicas não basta se uma lista externa apontar para a mesma coleção usada pela instância. Isso acontece quando a classe guarda diretamente a lista recebida ou devolve sua própria lista interna.

Nesse caso, outro código pode usar append, remove ou uma atribuição por índice para introduzir duplicatas ou exceder a capacidade — tudo sem chamar inscrever, cancelar_inscricao ou outra operação validada. Esse compartilhamento indevido é um vazamento de referência.

Referência exposta e retrato seguro

Compare os dois caminhos entre o código externo e a coleção interna.

Comparação entre uma lista interna compartilhada diretamente e uma lista interna protegida por cópias de entrada e saída.

À esquerda, a mesma lista atravessa a interface e pode ser alterada por fora. À direita, cópias independentes interrompem esse caminho.

Como as regras são contornadas

Implementação com vazamento de referências

Os dois retornos marcados entregam acesso à mesma lista mantida pela instância.

python
class Turma:
    def __init__(self, capacidade, inscritos):
        self._validar_estado(capacidade, inscritos)
        self._capacidade = capacidade
        self._inscritos = inscritos  # guarda a lista recebida

    def consultar_inscritos(self):
        return self._inscritos  # devolve a lista interna

    def _validar_estado(self, capacidade, inscritos):
        if type(capacidade) is not int:
            raise TypeError("capacidade deve ser um inteiro")
        if capacidade <= 0:
            raise ValueError("capacidade deve ser positiva")
        if any(type(item) is not str or not item.strip() for item in inscritos):
            raise ValueError("cada identificador deve ser um texto não vazio")
        if len(set(inscritos)) != len(inscritos):
            raise ValueError("não pode haver identificadores duplicados")
        if len(inscritos) > capacidade:
            raise ValueError("quantidade de inscritos excede a capacidade")


lista_externa = ["ana"]
turma = Turma(1, lista_externa)

lista_externa.append("bia")
turma.consultar_inscritos().append("ana")

print(turma.consultar_inscritos())
# ['ana', 'bia', 'ana']

Exemplo

O que falhou?

O estado foi válido durante a inicialização, mas a instância continuou compartilhando sua lista com o código externo. As duas chamadas a append ignoraram a validação: a capacidade foi excedida e surgiu uma duplicata. O prefixo _ comunica que _inscritos é interno, mas não corrige o compartilhamento da referência.

Copiar na entrada e controlar a saída

Referências sob controle da instância

Crie uma lista própria antes de validar e armazenar. Na consulta, devolva um retrato que não permita alterar a lista interna.

python
class Turma:
    def __init__(self, capacidade, inscritos):
        inscritos_candidatos = list(inscritos)
        self._validar_estado(capacidade, inscritos_candidatos)

        self._capacidade = capacidade
        self._inscritos = inscritos_candidatos

    def consultar_inscritos(self):
        return tuple(self._inscritos)

    def _validar_estado(self, capacidade, inscritos):
        if type(capacidade) is not int:
            raise TypeError("capacidade deve ser um inteiro")
        if capacidade <= 0:
            raise ValueError("capacidade deve ser positiva")
        if any(type(item) is not str or not item.strip() for item in inscritos):
            raise ValueError("cada identificador deve ser um texto não vazio")
        if len(set(inscritos)) != len(inscritos):
            raise ValueError("não pode haver identificadores duplicados")
        if len(inscritos) > capacidade:
            raise ValueError("quantidade de inscritos excede a capacidade")

Duas decisões independentes

list(inscritos) cria a coleção que ficará sob responsabilidade da instância. Alterar a lista original depois da inicialização não modifica _inscritos.

Na saída, tuple(self._inscritos) produz um retrato das inscrições naquele momento. Quem recebe a tupla pode consultá-la, mas não usar operações de lista para modificar a coleção interna. Outra opção segura seria devolver list(self._inscritos), isto é, uma nova lista que o código externo poderia alterar sem afetar a instância.

Dica

Limite dessa proteção

Essas são cópias rasas. Uma tupla impede a substituição, inclusão ou remoção de seus itens, mas não torna imutáveis objetos mutáveis armazenados dentro dela. Neste exemplo, isso é suficiente porque os identificadores são strings, e strings são imutáveis.

Preveja as referências

Entrada copiada e retrato de saída

Considere uma Turma que copia a lista recebida, oferece inscrever com validação e retorna tuple(self._inscritos) na consulta. Após o código abaixo, retrato continua sendo ('ana',), enquanto uma nova consulta retorna ('ana', 'bia').

original = ["ana"]
turma = Turma(2, original)
retrato = turma.consultar_inscritos()

original.append("caio")
turma.inscrever("bia")

O limite da tupla

Retornar uma tupla garante que qualquer elemento mutável armazenado dentro dela também fique completamente protegido contra alterações externas.

Passo 8 de 8

Aplicar e revisar a preservação das regras

Implemente uma operação de substituição e confirme que mudanças válidas são aplicadas, mudanças inválidas preservam o estado anterior e listas externas não alteram a coleção interna.

O contrato completo da classe

Quatro compromissos

Uma classe preserva suas invariantes quando:

  1. só conclui a inicialização com um estado válido;
  2. oferece operações públicas coerentes com o domínio;
  3. valida o estado candidato antes de alterar seus atributos;
  4. não entrega referências que permitam modificar suas coleções internas.

Você aplicará esses compromissos à operação substituir_inscrito(atual, novo). Ela não cria uma regra nova: apenas troca um identificador existente por outro e reutiliza as invariantes da Turma.

Validar antes de efetivar

A substituição deve ser preparada em uma cópia. Somente um estado candidato aprovado pode substituir o estado atual.

Diagrama com um estado atual protegido, uma cópia candidata submetida à validação e dois resultados: candidato aceito torna-se o novo estado; candidato rejeitado é descartado e o estado atual permanece intacto.

A validação acontece sobre o candidato; uma rejeição não alcança o estado armazenado.

Implemente a substituição

Prática no seu computador

Crie um arquivo chamado turma.py e copie o código completo abaixo. Depois, substitua apenas o corpo de substituir_inscrito.

Sua implementação deve:

  1. rejeitar a operação se atual não estiver inscrito;
  2. criar uma cópia de _inscritos;
  3. substituir o identificador somente nessa cópia;
  4. chamar _validar_estado com a capacidade atual e a lista candidata;
  5. atribuir a candidata a _inscritos apenas depois da validação.

Não altere _inscritos antes de todas essas verificações.

Classe-base e roteiro de execução

Complete o método indicado e execute o script localmente.

python
class Turma:
    def __init__(self, capacidade, inscritos):
        if not isinstance(inscritos, list):
            raise TypeError("inscritos deve ser uma lista")

        candidatos = list(inscritos)
        self._validar_estado(capacidade, candidatos)
        self._capacidade = capacidade
        self._inscritos = candidatos

    def _validar_estado(self, capacidade, inscritos):
        if isinstance(capacidade, bool) or not isinstance(capacidade, int):
            raise TypeError("capacidade deve ser um inteiro, sem aceitar bool")
        if capacidade <= 0:
            raise ValueError("capacidade deve ser positiva")

        for identificador in inscritos:
            if not isinstance(identificador, str):
                raise TypeError("cada identificador deve ser textual")
            if not identificador.strip():
                raise ValueError("identificadores não podem ser vazios")

        if len(set(inscritos)) != len(inscritos):
            raise ValueError("não pode haver identificadores duplicados")
        if len(inscritos) > capacidade:
            raise ValueError("a quantidade de inscritos excede a capacidade")

    def consultar_capacidade(self):
        return self._capacidade

    def consultar_inscritos(self):
        return list(self._inscritos)

    def inscrever(self, identificador):
        candidatos = self._inscritos + [identificador]
        self._validar_estado(self._capacidade, candidatos)
        self._inscritos = candidatos

    def cancelar_inscricao(self, identificador):
        if identificador not in self._inscritos:
            raise ValueError("a inscrição informada não existe")

        candidatos = list(self._inscritos)
        candidatos.remove(identificador)
        self._validar_estado(self._capacidade, candidatos)
        self._inscritos = candidatos

    def alterar_capacidade(self, nova_capacidade):
        self._validar_estado(nova_capacidade, self._inscritos)
        self._capacidade = nova_capacidade

    def reconfigurar(self, nova_capacidade, novos_inscritos):
        if not isinstance(novos_inscritos, list):
            raise TypeError("novos_inscritos deve ser uma lista")

        candidatos = list(novos_inscritos)
        self._validar_estado(nova_capacidade, candidatos)
        self._capacidade = nova_capacidade
        self._inscritos = candidatos

    def substituir_inscrito(self, atual, novo):
        # Implemente aqui.
        raise NotImplementedError


lista_inicial = ["ana", "bia"]
turma = Turma(3, lista_inicial)
print("Inicial:", turma.consultar_inscritos())

# Nem a lista de entrada nem o resultado da consulta devem alterar a turma.
lista_inicial.append("externo-1")
retrato = turma.consultar_inscritos()
retrato.append("externo-2")
print("Após listas externas:", turma.consultar_inscritos())

# Mudança válida.
turma.substituir_inscrito("ana", "caio")
print("Substituição válida:", turma.consultar_inscritos())

# A duplicação deve ser rejeitada sem remover "caio".
antes = turma.consultar_inscritos()
try:
    turma.substituir_inscrito("caio", "bia")
except ValueError as erro:
    print("Rejeitada:", erro)

print("Estado preservado:", turma.consultar_inscritos())
print("Igual ao estado anterior:", turma.consultar_inscritos() == antes)

Observe o comportamento

Exemplo

Resultado esperado

Ao executar sua implementação, a saída deve seguir este formato:

Inicial: ['ana', 'bia']
Após listas externas: ['ana', 'bia']
Substituição válida: ['caio', 'bia']
Rejeitada: não pode haver identificadores duplicados
Estado preservado: ['caio', 'bia']
Igual ao estado anterior: True

A mensagem da rejeição vem de _validar_estado. O ponto decisivo é o estado final: a tentativa de substituir caio por bia não pode remover caio nem criar uma duplicata.

Registre sua verificação

Explique o que aconteceu na substituição válida, na tentativa que produziria uma duplicata e nas alterações feitas às duas listas externas.

Escreva pelo menos 80 caracteres (0/80).

Compare e conclua

Solução completa

Compare sua tentativa com a solução abaixo. Observe especialmente que self._inscritos aparece no lado esquerdo de uma atribuição somente depois que o candidato foi aprovado.

Implementação final da operação

Este é o corpo completo que substitui o método incompleto da classe-base.

python
def substituir_inscrito(self, atual, novo):
    if atual not in self._inscritos:
        raise ValueError("a inscrição informada não existe")

    candidatos = list(self._inscritos)
    indice = candidatos.index(atual)
    candidatos[indice] = novo

    self._validar_estado(self._capacidade, candidatos)
    self._inscritos = candidatos

Resumo

Decisões que preservam a validade

  • A classe assume a responsabilidade por começar e permanecer em um estado válido.
  • Operações públicas expressam mudanças permitidas, em vez de oferecer alterações arbitrárias dos atributos.
  • Uma mudança que envolve o estado é preparada em variáveis e coleções locais.
  • O método interno valida o estado candidato sem modificar a instância.
  • As atribuições finais só acontecem depois que todas as regras foram aprovadas.
  • Se uma exceção rejeita o candidato, o estado anterior permanece integralmente preservado.
  • Cópias na entrada e na consulta impedem que listas externas contornem as operações públicas.

Tutorial concluído

Parabéns! Você concluiu: Preservar regras de validade em objetos

Você concluiu “Preservar regras de validade em objetos”: agora consegue validar estados iniciais, controlar mudanças, rejeitar candidatos sem alterações parciais e proteger coleções internas.

100 XP

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso