Formação BackEnd

Padrões de Desenvolvimento de Software

Módulo: JavaScript Backend

Prof. Tiago Segato

Logo Bolsa Futuro Digital

Visão Geral da Aula

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.

Tópicos Abordados:

  • Introdução a Padrões de Desenvolvimento de Software
  • Princípios SOLID completos (S, O, L, I, D)
  • Arquitetura MVC aplicada ao backend
  • Documentação técnica essencial (Diagrama de Caso de Uso, Diagrama de Classes, Diagrama de Sequências)
"Padrões de desenvolvimento são soluções comprovadas para problemas comuns no design de software."

Introdução a Padrões de Desenvolvimento 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.

O que são Padrões de Desenvolvimento?

  • Padrões Arquiteturais: São modelos de alto nível que definem a estrutura geral de um sistema de software. Eles abordam questões como a organização dos componentes, a comunicação entre eles e a distribuição de responsabilidades. Exemplo: MVC (Model-View-Controller).
  • Padrões de Projeto (Design Patterns): São soluções de baixo nível para problemas específicos de design de classes e objetos. Eles fornecem uma forma de resolver problemas de design de software de forma elegante e eficiente. Exemplo: Singleton, Factory, Observer.

Importância:

  • Reuso: Permitem reutilizar soluções testadas e comprovadas, economizando tempo e esforço.
  • Organização: Promovem uma estrutura clara e organizada para o código, facilitando a compreensão e a manutenção.
  • Manutenção: Sistemas construídos com padrões são mais fáceis de manter e estender, pois seguem convenções conhecidas.
  • Comunicação: Fornecem um vocabulário comum entre desenvolvedores, facilitando a comunicação sobre o design do sistema.

Diferença entre Padrão Arquitetural e Padrão 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.

Princípios SOLID Completos

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.

S - Single Responsibility Principle (SRP) - Princípio da Responsabilidade Única:

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) { /* ... */ }
}
                

O - Open/Closed Principle (OCP) - Princípio Aberto/Fechado:

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();
}
                

L - Liskov Substitution Principle (LSP) - Princípio da Substituição de Liskov:

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; }
}
                

I - Interface Segregation Principle (ISP) - Princípio da Segregação de Interfaces:

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"); }
}
                

D - Dependency Inversion Principle (DIP) - Princípio da Inversão de Dependência:

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();
                

Arquitetura MVC aplicada ao backend

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.

Conceito e Estrutura:

  • Model (Modelo): Representa os dados e a lógica de negócios da aplicação. É responsável por gerenciar o estado dos dados, as regras de negócio e a interação com o banco de dados. O Model é independente da interface do usuário.
  • View (Visão): Responsável pela apresentação dos dados ao usuário. No contexto de backend, a View geralmente não existe como uma interface gráfica, mas sim como a formatação da resposta (JSON, XML) que será enviada ao cliente. Pode ser um serializador de dados.
  • Controller (Controlador): Atua como um intermediário entre o Model e a View. Ele recebe as requisições do usuário, processa-as (interagindo com o Model para obter ou manipular dados) e, em seguida, seleciona a View apropriada para apresentar a resposta.

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.

Implementação prática com uma API REST simples (MVC)

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.

Estrutura de Pastas:


. 
├── 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;
                

Separação de responsabilidades no MVC

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;
                

Manutenibilidade e Escalabilidade com MVC

A arquitetura MVC oferece benefícios significativos para a manutenibilidade e escalabilidade de aplicações backend.

Manutenibilidade:

  • Código Organizado: A separação clara de Model, View (serialização) e Controller torna o código mais fácil de entender e navegar.
  • Testabilidade: Cada componente pode ser testado de forma isolada, facilitando a identificação e correção de bugs.
  • Reuso de Código: A lógica de negócios no Model pode ser reutilizada em diferentes Controllers ou até mesmo em outras aplicações.
  • Facilidade de Debugging: Problemas podem ser rastreados mais facilmente para o componente responsável.

Escalabilidade:

  • Desenvolvimento Paralelo: Diferentes equipes podem trabalhar em Models, Views e Controllers simultaneamente sem grandes conflitos.
  • Distribuição de Carga: Em sistemas distribuídos, os componentes podem ser escalados independentemente. Por exemplo, se a lógica de negócios for mais pesada, apenas os servidores que rodam o Model podem ser aumentados.
  • Flexibilidade para Mudanças: Novas funcionalidades ou alterações em requisitos podem ser implementadas com menor impacto no sistema como um todo.

Documentação Técnica Essencial

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.

Importância da Documentação:

  • Compreensão do Sistema: Ajuda novos membros da equipe a entenderem rapidamente a arquitetura e o funcionamento do software.
  • Manutenção e Evolução: Facilita a manutenção, depuração e a adição de novas funcionalidades ao longo do tempo.
  • Colaboração: Promove uma comunicação clara e eficaz entre os membros da equipe e stakeholders.
  • Garantia de Qualidade: Serve como base para a criação de planos de teste e validação do sistema.

Introdução à UML (Unified Modeling Language):

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.

Diagrama de Caso de Uso

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.

O que é?

  • Representa as interações entre os usuários (atores) e o sistema.
  • Foca nos requisitos funcionais do sistema.

Para que serve?

  • Capturar e organizar os requisitos funcionais.
  • Comunicar a funcionalidade do sistema para stakeholders não técnicos.
  • Definir o escopo do sistema.

Elementos Principais:

  • Ator: Representa um usuário, outro sistema ou qualquer entidade externa que interage com o sistema.
  • Caso de Uso: Representa uma funcionalidade específica que o sistema oferece.
  • Relacionamentos: Incluem associação (ator com caso de uso), inclusão (um caso de uso inclui outro), extensão (um caso de uso estende outro) e generalização.

Exemplo Prático: Sistema de E-commerce


@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
                

Diagrama de Classes

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.

O que é?

  • Representa as classes de um sistema e como elas se relacionam.
  • Foca na estrutura do sistema.

Para que serve?

  • Visualizar a estrutura de um sistema.
  • Compreender as relações entre diferentes partes do código.
  • Facilitar o design e a implementação de novas funcionalidades.

Elementos Principais:

  • Classe: Representa um conjunto de objetos com características (atributos) e comportamentos (operações) semelhantes.
  • Atributos: Propriedades que descrevem o estado de um objeto.
  • Operações (Métodos): Comportamentos que um objeto pode realizar.
  • Relacionamentos:
    • Associação: Conexão entre classes.
    • Agregação: Um tipo de associação que representa um relacionamento "todo-parte" onde a parte pode existir independentemente do todo.
    • Composição: Um tipo forte de agregação onde a parte não pode existir sem o todo.
    • Generalização (Herança): Representa um relacionamento "é um tipo de".
    • Dependência: Uma classe depende de outra.

Exemplo Prático: Sistema de Biblioteca


@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
                

Diagrama de Sequência

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.

O que é?

  • Representa a interação entre objetos em uma sequência temporal.
  • Foca no comportamento dinâmico do sistema.

Para que serve?

  • Modelar a lógica de um caso de uso ou operação.
  • Entender a ordem das chamadas de métodos.
  • Identificar gargalos ou problemas de comunicação.

Elementos Principais:

  • Ator: Entidade externa que inicia a sequência.
  • Linha de Vida (Lifeline): Representa a existência de um objeto ou ator durante a interação.
  • Mensagem: Representa a comunicação entre objetos, geralmente uma chamada de método.
  • Ativação (Activation Bar): Indica o período em que um objeto está executando uma ação.

Exemplo Prático: Login de Usuário


@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
                

Conclusão

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.

Resumo dos Tópicos:

  • Padrões de desenvolvimento (arquiteturais e de projeto) são soluções reutilizáveis para problemas comuns.
  • Os princípios SOLID (SRP e OCP) promovem código mais limpo e manutenível.
  • A arquitetura MVC organiza o backend em Model, View e Controller, melhorando a manutenibilidade e escalabilidade.
  • A documentação técnica, especialmente com UML (Caso de Uso, Classes, Sequência), é vital para a compreensão e evolução do sistema.

Próximos Passos:

  • Pratique a aplicação dos padrões de projeto em seus próprios projetos.
  • Explore outros princípios SOLID e padrões de projeto.
  • Desenvolva APIs RESTful utilizando a arquitetura MVC.
  • Crie diagramas UML para documentar seus sistemas.
"Aprender padrões é como aprender um novo idioma. Uma vez que você o domina, pode expressar ideias mais complexas e elegantes."