Prof. Tiago Segato
Nesta aula, exploraremos os conceitos fundamentais dos Padrões de Desenvolvimento de Software, focando em sua importância, na arquitetura MVC e na documentação técnica essencial com UML.
"Padrões de desenvolvimento são soluções comprovadas para problemas comuns no design de software."
Padrões de desenvolvimento de software são soluções reutilizáveis para problemas comuns que surgem durante o design de software. Eles são categorizados principalmente em padrões arquiteturais e padrões de projeto.
A principal diferença reside no nível de abstração. Padrões arquiteturais definem a estrutura macro do sistema, enquanto padrões de projeto focam na microestrutura, ou seja, no design de classes e objetos dentro de um componente.
SOLID é um acrônimo para cinco princípios de design de software que visam tornar os designs de software mais compreensíveis, flexíveis e manuteníveis.
Um módulo (classe, função, etc.) deve ter apenas uma razão para mudar, ou seja, deve ter apenas uma responsabilidade. Isso significa que cada módulo deve ser responsável por uma única parte da funcionalidade do software.
// Exemplo RUIM: Uma classe que gerencia usuários e envia e-mails
class UserHandler {
createUser(user) { /* ... */ }
sendWelcomeEmail(user) { /* ... */ }
}
// Exemplo BOM: Separação de responsabilidades
class UserManager {
createUser(user) { /* ... */ }
}
class EmailService {
sendWelcomeEmail(user) { /* ... */ }
}
Entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para modificação. Isso significa que o comportamento de um módulo pode ser estendido sem a necessidade de modificar seu código-fonte existente.
// Exemplo RUIM: Adicionar novo tipo de relatório exige modificar a função
function generateReport(type) {
if (type === 'PDF') { /* ... */ }
else if (type === 'CSV') { /* ... */ }
}
// Exemplo BOM: Usando polimorfismo para extensão
class ReportGenerator {
generate() { throw new Error('Method not implemented'); }
}
class PdfReportGenerator extends ReportGenerator {
generate() { /* Gerar PDF */ }
}
class CsvReportGenerator extends ReportGenerator {
generate() { /* Gerar CSV */ }
}
function generate(reportGenerator) {
reportGenerator.generate();
}
Objetos de um programa devem ser substituíveis por instâncias de seus subtipos sem alterar a corretude do programa. Isso significa que uma classe derivada deve ser capaz de ser usada no lugar de sua classe base sem quebrar a aplicação.
// Exemplo RUIM: Quadrado não é um retângulo perfeito
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) { this.width = width; }
setHeight(height) { this.height = height; }
getArea() { return this.width * this.height; }
}
class Square extends Rectangle {
constructor(size) {
super(size, size);
}
setWidth(width) { this.width = this.height = width; }
setHeight(height) { this.width = this.height = height; }
}
function increaseRectangleWidth(rectangle) {
rectangle.setWidth(rectangle.width + 1);
}
let rect = new Rectangle(10, 5);
increaseRectangleWidth(rect); // rect.width = 11, rect.height = 5
let square = new Square(10);
increaseRectangleWidth(square); // square.width = 11, square.height = 11 (quebra a expectativa)
// Exemplo BOM: Interfaces ou classes base que definem contratos claros
class Shape {
getArea() { throw new Error("Method not implemented"); }
}
class Rect extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() { return this.width * this.height; }
}
class Sq extends Shape {
constructor(size) {
super();
this.size = size;
}
getArea() { return this.size * this.size; }
}
Clientes não devem ser forçados a depender de interfaces que não utilizam. Isso significa que interfaces grandes e monolíticas devem ser divididas em interfaces menores e mais específicas, de modo que os clientes só precisem implementar os métodos que realmente lhes interessam.
// Exemplo RUIM: Interface grande e genérica
class Worker {
work() { throw new Error("Method not implemented"); }
eat() { throw new Error("Method not implemented"); }
sleep() { throw new Error("Method not implemented"); }
}
class HumanWorker extends Worker {
work() { console.log("Human working"); }
eat() { console.log("Human eating"); }
sleep() { console.log("Human sleeping"); }
}
class RobotWorker extends Worker {
work() { console.log("Robot working"); }
eat() { /* Robôs não comem, mas são forçados a implementar */ }
sleep() { /* Robôs não dormem, mas são forçados a implementar */ }
}
// Exemplo BOM: Interfaces segregadas
class Workable {
work() { throw new Error("Method not implemented"); }
}
class Eatable {
eat() { throw new Error("Method not implemented"); }
}
class Sleepable {
sleep() { throw new Error("Method not implemented"); }
}
class HumanWorkerISP extends Workable implements Eatable, Sleepable {
work() { console.log("Human working"); }
eat() { console.log("Human eating"); }
sleep() { console.log("Human sleeping"); }
}
class RobotWorkerISP extends Workable {
work() { console.log("Robot working"); }
}
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações. Isso promove o desacoplamento entre os módulos, tornando o sistema mais flexível e testável.
// Exemplo RUIM: Módulo de alto nível (ReportGenerator) depende de módulo de baixo nível (MySQLDatabase)
class MySQLDatabase {
getData() { console.log("Getting data from MySQL"); return [1, 2, 3]; }
}
class ReportGeneratorDIP {
constructor() {
this.database = new MySQLDatabase();
}
generateReport() {
const data = this.database.getData();
console.log("Generating report with data:", data);
}
}
// Exemplo BOM: Dependência de abstrações
class Database {
getData() { throw new Error("Method not implemented"); }
}
class MySQLDatabaseDIP extends Database {
getData() { console.log("Getting data from MySQL"); return [1, 2, 3]; }
}
class MongoDBDatabaseDIP extends Database {
getData() { console.log("Getting data from MongoDB"); return [4, 5, 6]; }
}
class ReportGeneratorDIPCorrect {
constructor(database) {
this.database = database;
}
generateReport() {
const data = this.database.getData();
console.log("Generating report with data:", data);
}
}
const mysqlDb = new MySQLDatabaseDIP();
const mongoDb = new MongoDBDatabaseDIP();
const report1 = new ReportGeneratorDIPCorrect(mysqlDb);
report1.generateReport();
const report2 = new ReportGeneratorDIPCorrect(mongoDb);
report2.generateReport();
O padrão arquitetural Model-View-Controller (MVC) é amplamente utilizado no desenvolvimento de aplicações web para separar as preocupações em três componentes principais.
No backend, o MVC ajuda a organizar o código de forma lógica, tornando-o mais fácil de entender, testar e manter. A separação de preocupações garante que as alterações em uma parte da aplicação não afetem desnecessariamente outras partes.
Vamos considerar um exemplo simples de uma API REST para gerenciar produtos, utilizando Node.js com Express para ilustrar a aplicação do MVC no backend.
.
├── app.js # Ponto de entrada da aplicação
├── controllers
│ └── productController.js
├── models
│ └── productModel.js
└── routes
└── productRoutes.js
models/productModel.js (Model - Lógica de Dados)
const products = []; // Simula um banco de dados
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
static getAll() {
return products;
}
static getById(id) {
return products.find(p => p.id === id);
}
static add(product) {
products.push(product);
return product;
}
static update(id, updatedProduct) {
const index = products.findIndex(p => p.id === id);
if (index !== -1) {
products[index] = { ...products[index], ...updatedProduct };
return products[index];
}
return null;
}
static delete(id) {
const initialLength = products.length;
products = products.filter(p => p.id !== id);
return products.length < initialLength;
}
}
module.exports = Product;
Continuando o exemplo da API de produtos, veja como os Controllers e as Rotas se encaixam na separação de responsabilidades.
controllers/productController.js (Controller - Lógica de Requisição/Resposta)
const Product = require("../models/productModel");
exports.getAllProducts = (req, res) => {
res.json(Product.getAll());
};
exports.getProductById = (req, res) => {
const product = Product.getById(parseInt(req.params.id));
if (product) {
res.json(product);
} else {
res.status(404).send("Produto não encontrado");
}
};
exports.createProduct = (req, res) => {
const newProduct = Product.add(req.body);
res.status(201).json(newProduct);
};
exports.updateProduct = (req, res) => {
const updatedProduct = Product.update(parseInt(req.params.id), req.body);
if (updatedProduct) {
res.json(updatedProduct);
} else {
res.status(404).send("Produto não encontrado");
}
};
exports.deleteProduct = (req, res) => {
const deleted = Product.delete(parseInt(req.params.id));
if (deleted) {
res.status(204).send(); // No Content
} else {
res.status(404).send("Produto não encontrado");
}
};
routes/productRoutes.js (Rotas - Mapeamento de URLs)
const express = require("express");
const router = express.Router();
const productController = require("../controllers/productController");
router.get("/", productController.getAllProducts);
router.get("/:id", productController.getProductById);
router.post("/", productController.createProduct);
router.put("/:id", productController.updateProduct);
router.delete("/:id", productController.deleteProduct);
module.exports = router;
A arquitetura MVC oferece benefícios significativos para a manutenibilidade e escalabilidade de aplicações backend.
A documentação técnica é crucial para o sucesso de qualquer projeto de software. Ela serve como um guia para desenvolvedores, testadores e usuários, garantindo que todos compreendam o funcionamento do sistema.
A UML é uma linguagem de modelagem visual padronizada para especificar, visualizar, construir e documentar os artefatos de um sistema de software. Ela oferece diversos tipos de diagramas para representar diferentes aspectos do sistema.
O Diagrama de Caso de Uso descreve a funcionalidade de um sistema do ponto de vista do usuário (ator). Ele mostra o que o sistema faz, mas não como ele faz.
@startuml
left to right direction
actor Cliente
actor Administrador
rectangle "Sistema de E-commerce" {
usecase "Fazer Login" as UC1
usecase "Pesquisar Produto" as UC2
usecase "Adicionar Produto ao Carrinho" as UC3
usecase "Realizar Pagamento" as UC4
usecase "Visualizar Histórico de Pedidos" as UC5
usecase "Gerenciar Produtos" as UC6
usecase "Gerenciar Usuários" as UC7
}
Cliente -- UC1
Cliente -- UC2
Cliente -- UC3
Cliente -- UC4
Cliente -- UC5
Administrador -- UC1
Administrador -- UC6
Administrador -- UC7
UC4 .> (UC1) : include
UC6 .> (UC1) : include
UC7 .> (UC1) : include
@enduml
O Diagrama de Classes é o diagrama mais comum na UML e é fundamental para a modelagem orientada a objetos. Ele descreve a estrutura estática de um sistema, mostrando suas classes, atributos, operações e os relacionamentos entre elas.
@startuml
class Livro {
-titulo: String
-autor: String
-isbn: String
+getTitulo(): String
+getAutor(): String
}
class Membro {
-nome: String
-idMembro: String
+getNome(): String
}
class Emprestimo {
-dataEmprestimo: Date
-dataDevolucao: Date
+getDataEmprestimo(): Date
}
Livro "1" -- "*" Emprestimo : "contém"
Membro "1" -- "*" Emprestimo : "realiza"
@enduml
O Diagrama de Sequência mostra a ordem cronológica das interações entre objetos em um cenário específico. Ele é excelente para visualizar o fluxo de controle e a troca de mensagens entre os objetos.
@startuml
actor Usuário
participant "Interface de Login" as UI
participant "Controlador de Autenticação" as AuthController
participant "Serviço de Usuário" as UserService
participant "Banco de Dados" as DB
Usuário -> UI: Digita credenciais
UI -> AuthController: autenticar(username, password)
AuthController -> UserService: validarCredenciais(username, password)
UserService -> DB: buscarUsuario(username)
DB --> UserService: retorna dados do usuário
UserService --> AuthController: retorna resultado da validação
alt Credenciais Válidas
AuthController --> UI: sucessoLogin()
UI --> Usuário: Exibe tela principal
else Credenciais Inválidas
AuthController --> UI: erroLogin()
UI --> Usuário: Exibe mensagem de erro
end
@enduml
Nesta aula, exploramos a importância dos padrões de desenvolvimento de software, a aplicação da arquitetura MVC no backend e a relevância da documentação técnica com diagramas UML.
"Aprender padrões é como aprender um novo idioma. Uma vez que você o domina, pode expressar ideias mais complexas e elegantes."