Modularização
❤️ Arquivos .h
(ou .hpp
)
Lembrando do nosso objetivo
- Com TADs queremos que o resto do programa seja cliente
- Apenas use as operações do mesmo.
Antes de tudo
Vamos entender um pouco como organizar um código
- Todos os nossos arquivos
.cpp
vão ficar na pastasrc
src
é source ou fontes: aqui moram os arquivos fontes.
- Vamos criar arquivos
.h
para definir o contrato do TAD - Os arquivos
.h
moram na pastainclude
(inclua isso) - Por um
Makefile
vai compilar tudo (vamos falar dele depois!)
. project
├── Makefile
├── include
│ └── ponto.h
│ └── retangulo.h
└── src
│ └── ponto.cpp
│ └── retangulo.cpp
E estes arquivos .h?
Lembre-se que TADs são contratos. Os arquivos .h
é a forma que C/C++ tem de separar o contrato da implementação.
Observe o exemplo de ponto.h
abaixo:
#ifndef PDS2_PONTO_H
#define PDS2_PONTO_H
/*
* Representa um ponto em duas dimensões. Não faz muito
* mais do que isso :-)
*/
class Ponto {
private:
double _x;
double _y;
public:
/*
* @brief Constutor do nosso ponto.
*/
Public(double x, double y);
/*
* @brief Retorna o valor na coordenada x
*/
double get_x();
/*
* @brief Retorna o valor na coordenada y
*/
double get_y();
};
#endif
Vamos por partes
Include Guards
Observe que o arquivo começa e termina com:
#ifndef PDS2_PONTO_H
#define PDS2_PONTO_H
// ...
#endif
Essas três chamadas são conhecidas como “include guards”. Tais linhas evitam bugs (ver aqui na hora de fazermos uso dos nossos arquivos .h
.
Definições
Em segundo lugar, observe que temos só o contrato
/*
* Representa um ponto em duas dimensões. Não faz muito
* mais do que isso :-)
*/
class Ponto {
private:
double _x;
double _y;
public:
/*
* @brief Constutor do nosso ponto.
*/
Public(double x, double y);
/*
* @brief Retorna o valor na coordenada x
*/
double get_x();
/*
* @brief Retorna o valor na coordenada y
*/
double get_y();
};
- Observe que todos os métodos não tem comportamento nenhum.
- Além disso, todos estão documentados. Não é obrigatório mas é uma boa prática.
- Assim qualquer pessoa que vai usar nosso código consegue entender quais são os contratos!
Vamos fazer o outro contrato
O módulo retângulo é cliente do módulo ponto
- Observe como o código abaixo inclui o
ponto.h
- Quando um include é
#include <assim>
- Estamos incluindo uma biblioteca do sistema
- Quando é
#include "assim"
- Estamos incluindo um arquivo nosso
O módulo retângulo
#include "../include/ponto.h"
/*
* Um Retângulo pode ser representado com um
* ponto de origem, uma altura e uma largura.
*
* largura
* ---------------
* | |
* | | altura
* | |
* ---------------
* (x,y)
* origem
*/
class Retangulo {
private:
double _x;
double _y;
public:
/*
* @brief Constutor do nosso ponto.
*/
Retangulo(Ponto _origem, double altura, double largura);
/*
* @brief Pega a área do retângulo
*/
double get_area();
/*
* Testa se dois retângulos tem alguma interseção.
* parte da premissa que altura e larguras só podem
* ser positivas!
*
* @brief Retorna true se este retângulo tem intersção com
* o outro
*/
bool interseccao(Retangulo outro);
};
Caminhos relativos
Sobre o ../include/ponto.h
Vai parece redundante, mas vai ficar mais claro quando fizer o .cpp
- Para entender a linha
#include "../include/ponto.h"
- Precisamos entender de caminhos relativos
- .. é um caminho para a pasta superior à
include
- Ou seja, a pasta raiz do projeto
- Dentro dela, naturalmente, existe a pasta
include
- Se eu saio de uma pasta, e vou para a logo a acima a pasta e onde saí existe na logo acima
- Dentro de include existe o arquivo
ponto.h
- Veja a figura abaixo.
- Antes de ver o código vamos entender o motivo pelo qual estamos fazendo essa nova organização.
“Agrupar para conquistar”
Juntar elementos inter-relacionados
- Manutenção, compreensão, …
- Aspectos da funcionalidade do programa
- Programação modular
- Partes independentes e intercambiáveis
- Quebrar em módulos permite desenvolver um código mais fácil de manter.
Boas Práticas
O Módulo
- Tem um propósito único
- Interface apropriada com outros módulos
- Pode ser compilado separadamente
- Reutilizáveis e Modificáveis
- Geralmente, um único TAD ou um TAD com
structs
eenums
que são apoio ao TAD
Exemplo 1
Observe como tenho os enums abaixo que serão utilizados pelo TAD (além de clientes do TAD)
Arquivo .h
#ifndef PDS2_CARTA_H
#define PDS2_CARTA_H
enum num {
AS, N2, N3, N4, N5, N6, N7, N8, N9, N10, J, Q, K
};
enum naipe {
COPAS, ESPADAS, OURO, PAUS
};
/*
* A classe carta cuida de representar
* uma carta do baralho padrão. Fazemos
* uso de enums para garantir que nunca
* seja inválida.
*/
class Carta {
private:
num _numero;
naipe _naipe;
public:
/*
* @brief Constrói uma carta
*/
Carta(num numero, naipe naipe);
/*
* @brief Pega o número
*/
num get_numero();
/*
* @brief Pega o naipe
*/
naipe get_naipe();
};
#endif
Exemplo 2
#ifndef PDS2_PESSOA_H
#define PDS2_PESSOA_H
#include <string>
struct endereco_t {
std::string rua,
std::string bairro,
std::string cidade
};
/*
* A classe pessoa serve para associarmos
* nomes aos endereços.
*/
class Pessoa {
private:
endereco_t _endereco;
std::string _nome;
public:
/*
* @brief Constrói uma pessoa
*/
Pessoa(std::string nome, std::string pessoa);
// ... Resto da classe aqui
};
#endif
Ok, mas e o código?
- O código vai morar na pasta
Arquivos .cpp
- Os arquivos .cpp vão guardar a implementação do contrato
- Os mesmos moram na pasta
src
- Antes de criar os mesmos temos que fazer o include do
.h
que criamos antes.
Exemplo 1
Arquivo .h
/*
* Representa um ponto em duas dimensões. Não faz muito
* mais do que isso :-)
*/
class Ponto {
private:
double _x;
double _y;
public:
/*
* @brief Constutor do nosso ponto.
*/
Public(double x, double y);
/*
* @brief Retorna o valor na coordenada x
*/
double get_x();
/*
* @brief Retorna o valor na coordenada y
*/
double get_y();
};
Arquivo .cpp
#include "../include/ponto.h"
Ponto::Ponto(double x, double y) {
_x = x;
_y = y;
}
double Ponto::get_x() {
return _x;
}
double Ponto::get_y() {
return _y;
}
Como que funciona?
O include
- A primeira linha é:
#include "../include/ponto.h"
- Caminhe até a pasta acima de source com .. (já falamos disso ainda agora)
- Na pasta mãe de
src
existe uma pastainclude
- Lá existe o arquivo
Preste atenção em alguns detalhes
- O arquivo
.cpp
é um pouco diferente das outras formas que declaramos métodos e funções - Porém lembre-se, em C++,
::
significa pertence
// retorno Classe::nome
double Ponto::get_x()
- A linha acima fala que: na classe Ponto, existe um método
get_x
que retorna um double. - Depois de dizer isso você diz: tá aqui o códgo do método
double Ponto::get_x() {
return _x
}
E o construtor?
- Na classe Ponto existe um construtor que não retorna nada
- Afinal, é um construtor
- Tá aqui como ele é implementado
Ponto::Ponto(double x, double y) {
_x = x;
_y = y;
}
Mais um exemplo
Exemplo 2
Arquivo .h
#ifndef PDS2_CARTA_H
#define PDS2_CARTA_H
enum num {
AS, N2, N3, N4, N5, N6, N7, N8, N9, N10, J, Q, K
};
enum naipe {
COPAS, ESPADAS, OURO, PAUS
};
/*
* A classe carta cuida de representar
* uma carta do baralho padrão. Fazemos
* uso de enums para garantir que nunca
* seja inválida.
*/
class Carta {
private:
num _numero;
naipe _naipe;
public:
/*
* @brief Constrói uma carta
*/
Carta(num numero, naipe naipe);
/*
* @brief Pega o número
*/
num get_numero();
/*
* @brief Pega o naipe
*/
naipe get_naipe();
};
#endif
Arquivo .cpp
#include "../include/carta.h" // Vou implementar esse contrato
Carta::Carta( // Na classe Carta tem um construtor
num numero, naipe naipe
) {
_numero = numero;
_naipe = naipe;
}
num Carta::get_numero() { // Na classe Carta tem um `get_numero` que retorna um num
return _numero;
}
naipe Carta::get_naipe() {
return _naipe;
}
- Vamos agora falar um pouco das vantagens antes de entrar em mais exemplos
Projeto Modular
Propriedades
- Decomposição
- Composição
- Significado fechado
- Continuidade
- Proteção
Decomposição
- Nível de Projeto
- Capaz de separar uma tarefa em subtarefas, que podem ser abordadas separadamente
- Nível de Software
- Capaz de trabalhar em cada um dos módulos do software independente do outros módulos
- O que pode prejudicar a decomposição?
Composição
- Capacidade de conseguir combinar de forma livre diferentes elementos de software
Continuidade
- Alterações em parte da especificação demandam alterações em poucos módulos
- Bom exemplo
- Utilização de constantes
- Mau exemplo
- Dependência forte de um único módulo
Proteção
- Situações anormais em tempo de execução não são propagadas para outros módulos
- Erros não detectados em outras partes
- Extensibilidade
- Validação dos dados nos módulos
- Tipos, asserções, exceções
Compilação
- Grandes sistemas
- Equipes de programadores
- Código distribuído em vários arquivos fonte
Não é conveniente recompilar partes (todo) do programa que não foram alteradas
- Princípio do encapsulamento
- Separar a especificação de como a classe é usada dos detalhes de como é implementada
Um exemplo maior
Como compilar o código?
g++ src/ponto.cpp src/retangulo.cpp main.cpp -o main
- Passamos TODOS os arquivos do nosso código para o compilador
- O compilador cuida de compilar cada parte separada
Exemplos das Cartas no GitHub
- Na pasta abaixo temos um exemplo maior com um arquivo main
- Exemplo
- Para compilar o mesmo devemos fazer
g++ src/jogador.cpp src/carta.cpp src/baralho.cpp main.cpp -o main
Compilacao
- Na prática o compilador segue o esquema abaixo
- Cada
.cpp
é compilado separadamente - Depois colamos todos com um
linker
- Tudo é oculto de você como programador
Uma visão abstrata do processo
- Cada passo gera código de máquina
- O linker cola tudo junto
- Os arquivos
.h
ajudam a compilar os.cpp
individualmente - O
retangulo.cpp
pode ser compilado sem oponto.cpp
- Qual o motivo? O
retangulo.cpp
sabe do contrato daponto.h
- Então eu posso compilar mesmo sem ter o outro pronto
- Qual o motivo? O
- Depois o linker cola tudo junto
Precisa de .h e .cpp?
Sobre o processo de compilação
- Compilar código é um processo custoso
- Aqui, por custoso leia-se,lento e que demanda muito uso de CPU (e leitura do disco)
- Compilar partes separadas nos permite realizar o processo em paralelo
- Algo que não fazemos aqui, mas ok
Sobre módulos
- Usando
gcc -c
você pode compilar seu módulo e re-utilizar o mesmo - Se o seu módulo usa
<string>
, não tem motivo para compilar o módulo<string>
- Sempre que você usa
gcc -c
você gera um arquivo.o
. Seu computador é cheio de tais arquivos.- Ou de arquivos
.so
,.a
,.dll
etc. - São módulos pré-compilados
- Ou de arquivos
- Cada módulo até sabe de outros (através arquivos
.h
) mas foi compilado isoladamente.
Como automatizar o processo de compilação?
Ideia (Sistemas de Build/Construção)
- Compilar código “na mão” é um processo tedioso
- Existem ferramentas para automatizar esse processo
- Pense em um script, um pequeno código de roteiro, que diz: estes são os passos para compular este código
Ferramentas
- Sistemas de build, ou seja de construção, são comuns no processo de desenvolvimento de software
- “People love to hate build systems.” ([ref])[https://cliutils.gitlab.io/modern-cmake/]
- A grande verdade é que não existe uma ferramenta única para este propósito
Sistema Make
- No nosso curso vamos fazer uso do sistema make
- Caso você use um sistema Linux, como o WSL, já deve ter o make
- Se usa Windows, veja esta pergunta aqui
- O comando básico é
make
- O comando depende de um arquivo chamado de
Makefile
Makefile
- Arquivo de texto especialmente formatado para um programa Unix chamado
make
- Contém uma lista de requisitos para que um programa seja considerado ‘up to date’
- O programa make examina esses requisitos
- verifica os timestamps em todos os arquivos de origem;
- recompila apenas os arquivos com um registro desatualizado
- Uma regra no arquivo make tem a seguinte forma:
target: requisitos ; comando
- Como que leio isso?
- Para construir o
target
- Eu preciso primeiro construir os requisitos
- E depois executar tal comando
- Para construir o
Exemplo Makefile (1)
Arquivo Make
all: main
main:
g++ src/jogador.cpp src/carta.cpp src/baralho.cpp main.cpp -o meuprograma
clean:
rm meuprograma
- Aqui, para executar o comando
all
primeiro eu preciso executarmain
(all: main
) - Para executar o
main
eu não preciso de nada- O main executa o comando:
g++ src/jogador.cpp src/carta.cpp src/baralho.cpp main.cpp -o main
- Ou seja, compila o programa
- Gera um executável chamado de
meuprograma
- O main executa o comando:
- Para executar o
clean
eu não preciso de nada- O clean executa o comando
rm
que apaga um arquivo - Estou apagamento o arquivo
meuprograma
- O clean executa o comando
Fazendo uso
- Para compilar seu código, execute o comando
make
make
- ou
make all
- Para limpar tudo
make clean
Exemplo Makefile (2)
- Abaixo temos outro Makefile mais complexo
- O mesmo faz uso de variáveis no começo
- Para referenciar umas variável, use
${NOME_DA_VAR}
Arquivo
CC=g++
CFLAGS=-std=c++20 -Wall
all: main
ponto.o: include/ponto.h src/ponto.cpp
${CC} ${CFLAGS} -c src/ponto.cpp
retangulo.o: ponto.o include/retangulo.h src/retangulo.cpp
${CC} ${CFLAGS} -c src/retangulo.cpp
main.o: include/ponto.h src/main.cpp
${CC} ${CFLAGS} -c src/main.cpp
main: main.o ponto.o retangulo.o
${CC} ${CFLAGS} -o main main.o ponto.o retangulo.o
clean:
rm -f main *.o
Entendendo
- O
all: main
depende do main - O
main: main.o ponto.o retangulo.o
depende das regrasmain.o
,ponto.o
eretangulo.o
. - A regra
ponto.o: include/ponto.h src/ponto.cpp
depende dos arquivos existirem.- Isto é, quando uma dependência no make não é uma regra, o sistema simplesmente verifica se o arquivo existe
- Aqui
include/ponto.h
esrc/ponto.cpp
são dois arquivos que tem que existir para compilar o ponto - Faz sentido, são os arquivos que definem o código
- Para compilar execute:
${CC} ${CFLAGS} -c src/ponto.cpp
, só que${CC}
é uma variável, qual o valor dela?- Basta substituir com a definição no começo do arquivi
- Ou seja, para compilar execute
g++ -std=c++20 -Wall -c src/ponto.cpp
- Dado que:
${CC}
virag++
e${CFLAGS}
vira `-std=c++20 -Wall
Exemplo Makefile “Genérico”
- Make é um sistema poderoso que permite executar comandos
- Nosso foco aqui não é passar por todos esses comandos
- Possivelmente, um Makefile simples vai ser o suficiente para você na disciplina
- Porém, e como sou preguiçoso, tenho um Makefile genérico para uma organização de pastas da forma abaixo
- Copie e cole esse make, organize seu código em pastas similares que vai funcionar em sistemas unix
Como organizar os arquivos
. project
├── Makefile
├── include // aqui moram os seus .h ou .hpp
│ └── arq1.h
│ └── arq2.h
└── src // aqui moram os seus .cpp
│ └── arq1.cpp
│ └── arq2.cpp
└── third_party // aqui mora código de terceiros que você pegou da internet
│ └── doctest.h // vai ficar mais claro quando falarmos de testes
O makefile
CC := g++
SRCDIR := src
BUILDDIR := build
TARGET := main
SRCEXT := cpp
SOURCES := $(shell find $(SRCDIR) -type f -name *.$(SRCEXT))
OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.o))
CFLAGS := -g -Wall -O3 -std=c++20
INC := -I include/ -I third_party/
$(TARGET): $(OBJECTS)
$(CC) $^ -o $(TARGET)
$(BUILDDIR)/%.o: $(SRCDIR)/%.$(SRCEXT)
@mkdir -p $(@D)
$(CC) $(CFLAGS) $(INC) -c -o $@ $<
clean:
$(RM) -r $(BUILDDIR)/* $(TARGET)
.PHONY: clean
Considerações Finais
Qual o motivo de modularizar?
- Maior reusabilidade
- Melhoria da legibilidade
- Modificações facilitadas (e mais seguras)
- Maior confiabilidade
- Aumento da produtividade
Qual de usar um sistema de construção estilo o Make?
- Constriir o código em partes e de forma automatizada