Skip to content

Padrão Composite - Instituição

1. Introdução

Este documento descreve a implementação do padrão de projeto Composite para representar a estrutura hierárquica de instituições de ensino no sistema DicasDeEstagio. O Composite é um padrão estrutural que permite compor objetos em estruturas de árvore para representar hierarquias parte-todo.

2. Objetivo

O padrão Composite foi escolhido para Instituições devido à:

  • Hierarquia natural: Instituição → Departamento/Faculdade → Curso

  • Tratamento uniforme: Operações aplicáveis a toda hierarquia

  • Flexibilidade: Facilita adicionar novos níveis hierárquicos

3. Estrutura do Padrão

3.1 Diagrama de Classes

Diagrama
classDiagram class Instituicao { +id: int +nome: String +cnpj: String +AreaAtuacao: String +__str__() String } class ComponenteInstitucional { <<abstract>> +nome: String +get_nome() String +exibir(nivel: int) String +adicionar(componente) void +remover(componente) void +obter_filhos() List +contar_total_elementos() int } class InstituicaoComposite { +instituicao: Instituicao +get_nome() String +adicionar(componente) void +remover(componente) void +exibir(nivel: int) String +get_estrutura_json() Dict } class UnidadeComposite { +instituicao_composite: InstituicaoComposite +pai: UnidadeComposite +get_nome() String +adicionar(componente) void +remover(componente) void +exibir(nivel: int) String +get_estrutura_json() Dict } class CursoComposite { +unidade_composite: UnidadeComposite +get_nome() String +exibir(nivel: int) String +get_estrutura_json() Dict } Instituicao "1" -- "1" InstituicaoComposite : representa ComponenteInstitucional <|-- InstituicaoComposite ComponenteInstitucional <|-- UnidadeComposite ComponenteInstitucional <|-- CursoComposite InstituicaoComposite "1" *-- "many" UnidadeComposite : contém UnidadeComposite "1" *-- "many" CursoComposite : contém UnidadeComposite "1" *-- "many" UnidadeComposite : subunidades

4. Implementação em Django

4.1 Models (models.py)

Ver código

from django.db import models


class Instituicao(models.Model):
    nome = models.CharField(max_length=255)
    cnpj = models.CharField(max_length=14)
    AreaAtuacao = models.CharField(max_length=255)

    def __str__(self):
        return self.nome

    class Meta:
        verbose_name = 'Instituição'
        verbose_name_plural = 'Instituições'


class ComponenteInstitucional(models.Model):
    nome = models.CharField(max_length=255)

    class Meta:
        abstract = True

    def get_nome(self):
        return self.nome

    def exibir(self, nivel=0):
        raise NotImplementedError("Método abstrato")

    def adicionar(self, componente):
        raise NotImplementedError("Este componente não pode ter filhos")

    def remover(self, componente):
        raise NotImplementedError("Este componente não pode ter filhos")

    def obter_filhos(self):
        return []

    def contar_total_elementos(self):
        total = 1
        for filho in self.obter_filhos():
            total += filho.contar_total_elementos()
        return total


class InstituicaoComposite(ComponenteInstitucional):
    instituicao = models.OneToOneField(
        Instituicao,
        on_delete=models.CASCADE,
        related_name='composite'
    )

    class Meta:
        verbose_name = 'Instituição (Composite)'
        verbose_name_plural = 'Instituições (Composite)'

    def get_nome(self):
        return self.instituicao.nome

    def adicionar(self, componente):
        if isinstance(componente, UnidadeComposite):
            componente.instituicao_composite = self
            componente.save()
        else:
            raise TypeError("Instituição só pode conter Unidades")

    def remover(self, componente):
        if isinstance(componente, UnidadeComposite):
            componente.instituicao_composite = None
            componente.save()

    def obter_filhos(self):
        return list(self.unidades.filter(pai__isnull=True))

    def exibir(self, nivel=0):
        indentacao = "  " * nivel
        instituicao = self.instituicao
        resultado = f"{indentacao} {instituicao.nome} (CNPJ: {instituicao.cnpj}, Área: {instituicao.AreaAtuacao})\n"

        for unidade in self.obter_filhos():
            resultado += unidade.exibir(nivel + 1)

        return resultado


class UnidadeComposite(ComponenteInstitucional):
    instituicao_composite = models.ForeignKey(
        InstituicaoComposite,
        on_delete=models.CASCADE,
        related_name='unidades',
        null=True,
        blank=True
    )
    pai = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        related_name='subunidades',
        null=True,
        blank=True
    )

    class Meta:
        verbose_name = 'Unidade (Composite)'
        verbose_name_plural = 'Unidades (Composite)'

    def adicionar(self, componente):
        if isinstance(componente, CursoComposite):
            componente.unidade_composite = self
            componente.save()
        elif isinstance(componente, UnidadeComposite):
            componente.pai = self
            componente.save()
        else:
            raise TypeError("Unidade só pode conter Cursos ou Subunidades")

    def remover(self, componente):
        if isinstance(componente, CursoComposite):
            componente.unidade_composite = None
            componente.save()
        elif isinstance(componente, UnidadeComposite):
            componente.pai = None
            componente.save()

    def obter_filhos(self):
        cursos = list(self.cursos.all())
        subunidades = list(self.subunidades.all())
        return subunidades + cursos

    def exibir(self, nivel=0):
        indentacao = "  " * nivel
        resultado = f"{indentacao} {self.nome}\n"

        for filho in self.obter_filhos():
            resultado += filho.exibir(nivel + 1)

        return resultado


class CursoComposite(ComponenteInstitucional):
    unidade_composite = models.ForeignKey(
        UnidadeComposite,
        on_delete=models.CASCADE,
        related_name='cursos'
    )

    class Meta:
        verbose_name = 'Curso (Composite)'
        verbose_name_plural = 'Cursos (Composite)'

    def exibir(self, nivel=0):
        indentacao = "  " * nivel
        return f"{indentacao} {self.nome}\n"

    def obter_filhos(self):
        return []

4.2 Serializers (serializers.py)

Código

from rest_framework import serializers
from .models import Instituicao, Unidade, Curso

class CursoSerializer(serializers.ModelSerializer):
    """Serializer para Curso (Leaf)"""

    class Meta:
        model = Curso
        fields = ['id', 'nome', 'unidade']


class UnidadeSerializer(serializers.ModelSerializer):
    """Serializer para Unidade (Composite)"""
    cursos = CursoSerializer(many=True, read_only=True)
    subunidades = serializers.SerializerMethodField()

    class Meta:
        model = Unidade
        fields = ['id', 'nome', 'instituicao', 'pai', 'cursos', 'subunidades']

    def get_subunidades(self, obj):
        """Serializa subunidades recursivamente"""
        subunidades = obj.subunidades.all()
        return UnidadeSerializer(subunidades, many=True).data


class InstituicaoSerializer(serializers.ModelSerializer):
    """Serializer para Instituição (Root Composite)"""
    unidades = UnidadeSerializer(many=True, read_only=True)
    total_elementos = serializers.SerializerMethodField()

    class Meta:
        model = Instituicao
        fields = ['id', 'nome', 'cnpj', 'AreaAtuacao', 'unidades', 'total_elementos']

    def get_total_elementos(self, obj):
        """Conta total de elementos na hierarquia"""
        return obj.contar_total_elementos()


class InstituicaoHierarquiaSerializer(serializers.ModelSerializer):
    """
    Serializer especializado para exibir hierarquia completa.
    Usa o método get_estrutura_json do padrão Composite.
    """
    estrutura = serializers.SerializerMethodField()

    class Meta:
        model = Instituicao
        fields = ['id', 'nome', 'cnpj', 'AreaAtuacao', 'estrutura']

    def get_estrutura(self, obj):
        """Retorna estrutura hierárquica completa"""
        return obj.get_estrutura_json()

4.3 Views (views.py)

Código

from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.db.models import Prefetch
from .models import Instituicao, Unidade, Curso
from .serializers import (
    InstituicaoSerializer,
    InstituicaoHierarquiaSerializer,
    UnidadeSerializer,
    CursoSerializer
)

class InstituicaoViewSet(viewsets.ModelViewSet):
    """
    ViewSet para gerenciar instituições.
    Implementa operações do padrão Composite.
    """
    queryset = Instituicao.objects.all()
    serializer_class = InstituicaoSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        """Otimiza queries com prefetch"""
        return Instituicao.objects.prefetch_related(
            'unidades',
            'unidades__cursos'
        )

    @action(detail=True, methods=['get'], url_path='hierarquia')
    def hierarquia(self, request, pk=None):
        """
        Retorna estrutura hierárquica completa da instituição.
        """
        instituicao = self.get_object()
        serializer = InstituicaoHierarquiaSerializer(instituicao)
        return Response(serializer.data)

    @action(detail=True, methods=['get'], url_path='exibir')
    def exibir_estrutura(self, request, pk=None):
        """
        Retorna representação textual da hierarquia.
        """
        instituicao = self.get_object()
        estrutura_texto = instituicao.exibir()

        return Response({
            'instituicao': instituicao.nome,
            'estrutura': estrutura_texto
        })

    @action(detail=True, methods=['post'], url_path='adicionar-unidade')
    def adicionar_unidade(self, request, pk=None):
        """
        Adiciona uma unidade à instituição usando método do Composite.
        """
        instituicao = self.get_object()

        serializer = UnidadeSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        # Criar unidade
        unidade = Unidade(**serializer.validated_data)

        # Usar método adicionar do Composite
        instituicao.adicionar(unidade)

        return Response(
            UnidadeSerializer(unidade).data,
            status=status.HTTP_201_CREATED
        )


class UnidadeViewSet(viewsets.ModelViewSet):
    """
    ViewSet para gerenciar unidades.
    Suporta operações de composição.
    """
    queryset = Unidade.objects.all()
    serializer_class = UnidadeSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        """Otimiza queries"""
        return Unidade.objects.prefetch_related(
            'cursos',
            'subunidades'
        ).select_related('instituicao', 'pai')

    @action(detail=True, methods=['post'], url_path='adicionar-curso')
    def adicionar_curso(self, request, pk=None):
        """
        Adiciona um curso à unidade.
        """
        unidade = self.get_object()

        serializer = CursoSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        # Criar curso
        curso = Curso(**serializer.validated_data)

        # Usar método adicionar do Composite
        unidade.adicionar(curso)

        return Response(
            CursoSerializer(curso).data,
            status=status.HTTP_201_CREATED
        )

    @action(detail=True, methods=['post'], url_path='adicionar-subunidade')
    def adicionar_subunidade(self, request, pk=None):
        """
        Adiciona uma subunidade.
        """
        unidade_pai = self.get_object()

        serializer = UnidadeSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        subunidade = Unidade(**serializer.validated_data)
        unidade_pai.adicionar(subunidade)

        return Response(
            UnidadeSerializer(subunidade).data,
            status=status.HTTP_201_CREATED
        )


class CursoViewSet(viewsets.ModelViewSet):
    """
    ViewSet para gerenciar cursos.
    Cursos são folhas na hierarquia (não têm filhos).
    """
    queryset = Curso.objects.all()
    serializer_class = CursoSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        """Otimiza queries"""
        return Curso.objects.select_related(
            'unidade',
            'unidade__instituicao'
        )

5. Exemplo de Uso

5.1 Construindo a Hierarquia

Hierarquia
from .models import Instituicao, InstituicaoComposite, UnidadeComposite, CursoComposite

unb_original = Instituicao.objects.create(
    nome='Universidade de Brasília',
    cnpj='12345678000195',
    AreaAtuacao='Ensino Superior'
)

unb_composite = InstituicaoComposite.objects.create(
    instituicao=unb_original,
    nome=unb_original.nome 
)


fga = UnidadeComposite.objects.create(
    nome='Faculdade do Gama',
    instituicao_composite=unb_composite
)


unb_composite.adicionar(fga)

eng_soft = CursoComposite.objects.create(
    nome='Engenharia de Software',
    unidade_composite=fga
)


fga.adicionar(eng_soft)


print("=== HIERARQUIA COMPLETA ===")
print(unb_composite.exibir())


print("\n=== INSTITUIÇÃO ORIGINAL ===")
print(f"Nome: {unb_original.nome}")
print(f"CNPJ: {unb_original.cnpj}")
print(f"Área: {unb_original.AreaAtuacao}")


print(f"\n=== ESTATÍSTICAS ===")
print(f"Total de elementos: {unb_composite.contar_total_elementos()}")

5.2 Requisições API

Requisições
# Listar instituições
    GET /api/instituicoes/

# Ver hierarquia completa
    GET /api/instituicoes/1/hierarquia/

# Exibir estrutura textual
    GET /api/instituicoes/1/exibir/

    # Adicionar departamento
         /api/instituicoes/1/adicionar-departamento/
                "nome": "Faculdade de Tecnologia",
        "codigo": "FT",
    "descricao": "Faculdade de cursos tecnológicos"
    }

    # Adicionar curso a departamento
         /api/departamentos/1/adicionar-curso/
                "nome": "Engenharia de Software",
        "codigo": "ES",
        "nivel": "graduacao",
    "duracao": 10
    }

    # Ver caminho completo do curso
GET /api/cursos/1/caminho-completo/

6. Vantagens da Implementação

  • Uniformidade: Trata objetos individuais e composições de forma uniforme
  • Recursividade: Operações funcionam recursivamente na hierarquia
  • Flexibilidade: Fácil adicionar novos níveis hierárquicos
  • Manutenibilidade: Estrutura clara e organizada
  • Navegação: Facilita navegação na árvore de componentes

    7. Testes

Código

from django.test import TestCase
from .models import Instituicao, InstituicaoComposite, UnidadeComposite, CursoComposite

class CompositeTestCase(TestCase):

    def setUp(self):
        # Criar instituição ORIGINAL
        self.instituicao = Instituicao.objects.create(
            nome='Universidade de Brasília',
            cnpj='12345678000195',
            AreaAtuacao='Ensino Superior'
        )

        # Criar composite
        self.composite = InstituicaoComposite.objects.create(
            instituicao=self.instituicao,
            nome='Universidade de Brasília'
        )

        # Criar unidade
        self.unidade = UnidadeComposite.objects.create(
            nome='Faculdade do Gama',
            instituicao_composite=self.composite
        )

        # Criar curso
        self.curso = CursoComposite.objects.create(
            nome='Engenharia de Software',
            unidade_composite=self.unidade
        )

    def test_instituicao_original_intacta(self):
        """Testa que a instituição original não foi modificada"""
        self.assertEqual(self.instituicao.nome, 'Universidade de Brasília')
        self.assertEqual(self.instituicao.cnpj, '12345678000195')
        self.assertEqual(self.instituicao.AreaAtuacao, 'Ensino Superior')

    def test_composite_hierarchy(self):
        """Testa a hierarquia do composite"""
        self.assertEqual(self.composite.instituicao, self.instituicao)
        self.assertEqual(self.unidade.instituicao_composite, self.composite)
        self.assertEqual(self.curso.unidade_composite, self.unidade)

    def test_exibir_hierarquia(self):
        """Testa a exibição da hierarquia"""
        resultado = self.composite.exibir()
        self.assertIn('Universidade de Brasília', resultado)
        self.assertIn('Faculdade do Gama', resultado)
        self.assertIn('Engenharia de Software', resultado)

    def test_contar_elementos(self):
        """Testa a contagem de elementos"""
        total = self.composite.contar_total_elementos()
        self.assertEqual(total, 3)  # Instituição + Unidade + Curso

8. Referências

  • Gamma, E. et al. (1994). Design Patterns: Elements of Reusable Object-Oriented Software
  • Django Documentation: https://docs.djangoproject.com/
  • Django REST Framework: https://www.django-rest-framework.org/

Histórico de Versões

Versão Data Descrição Autor(es) Revisor(es)
1.0 24/10/2025 Criação do documento Breno Alexandre, Víctor Moreira, Felipe Nunes