Os princípios SOLID são cinco diretrizes de design que ajudam os programadores a escrever código sustentável, flexível e escalável. Compreender e aplicar estes princípios distingue código de nível profissional de soluções rápidas. Este guia aborda a aplicação prática de SOLID na perspetiva de um programador sénior.
Porque é que SOLID Importa
Seguir os princípios SOLID conduz a:
- Manutenibilidade: O código é mais fácil de modificar sem quebrar
- Testabilidade: Os componentes podem ser testados de forma isolada
- Flexibilidade: Novas funcionalidades exigem menos refatorização
- Legibilidade: Responsabilidades e interfaces claras
- Colaboração: As equipas podem trabalhar em componentes separados
S — Princípio da Responsabilidade Única (SRP)
Definição: Uma classe deve ter um, e apenas um, motivo para mudar.
Mau Exemplo
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# Lógica da base de dados
db.execute("INSERT INTO users...")
def send_welcome_email(self):
# Lógica de e-mail
smtp.send(self.email, "Welcome!")
def generate_report(self):
# Lógica de relatórios
return f"User Report: {self.name}"
Esta classe tem três motivos para mudar: dados do utilizador, operações de base de dados, envio de e-mails e relatórios.
Bom Exemplo
class User:
"""Only handles user data."""
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
"""Handles user persistence."""
def save(self, user: User):
db.execute("INSERT INTO users...", user.name, user.email)
def find_by_email(self, email: str) -> User:
row = db.query("SELECT * FROM users WHERE email = ?", email)
return User(row['name'], row['email'])
class EmailService:
"""Handles email sending."""
def send_welcome(self, user: User):
smtp.send(user.email, "Welcome!", self._welcome_template(user))
def _welcome_template(self, user: User) -> str:
return f"Hello {user.name}, welcome to our platform!"
class UserReportGenerator:
"""Handles user reports."""
def generate(self, user: User) -> str:
return f"User Report: {user.name}, Email: {user.email}"
Cada classe tem agora uma única responsabilidade e pode ser modificada de forma independente.
O — Princípio Aberto/Fechado (OCP)
Definição: As classes devem estar abertas à extensão, mas fechadas à modificação.
Mau Exemplo
class PaymentProcessor:
def process(self, payment_type: str, amount: float):
if payment_type == "credit_card":
# Lógica de cartão de crédito
return self._process_credit_card(amount)
elif payment_type == "paypal":
# Lógica de PayPal
return self._process_paypal(amount)
elif payment_type == "bitcoin":
# Lógica de Bitcoin
return self._process_bitcoin(amount)
# Adicionar um novo método de pagamento exige modificar esta classe
Cada novo método de pagamento exige modificar o processador.
Bom Exemplo
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
"""Abstract base class for payment methods."""
@abstractmethod
def process(self, amount: float) -> bool:
pass
class CreditCardPayment(PaymentMethod):
def __init__(self, card_number: str, cvv: str):
self.card_number = card_number
self.cvv = cvv
def process(self, amount: float) -> bool:
# Lógica de processamento de cartão de crédito
return gateway.charge(self.card_number, amount)
class PayPalPayment(PaymentMethod):
def __init__(self, email: str):
self.email = email
def process(self, amount: float) -> bool:
# Lógica de processamento de PayPal
return paypal.charge(self.email, amount)
class BitcoinPayment(PaymentMethod):
def __init__(self, wallet_address: str):
self.wallet_address = wallet_address
def process(self, amount: float) -> bool:
# Lógica de processamento de Bitcoin
return bitcoin.transfer(self.wallet_address, amount)
class PaymentProcessor:
"""Processor is closed for modification, open for extension."""
def process(self, method: PaymentMethod, amount: float) -> bool:
return method.process(amount)
# Utilização: adicionar um novo método de pagamento não exige alterar PaymentProcessor
class ApplePayPayment(PaymentMethod):
def process(self, amount: float) -> bool:
return apple_pay.charge(amount)
Novos métodos de pagamento podem ser adicionados sem tocar no código existente.
L — Princípio da Substituição de Liskov (LSP)
Definição: As subclasses devem poder substituir as suas classes base sem alterar a correção do programa.
Mau Exemplo
class Rectangle:
def __init__(self, width: int, height: int):
self._width = width
self._height = height
def set_width(self, width: int):
self._width = width
def set_height(self, height: int):
self._height = height
def area(self) -> int:
return self._width * self._height
class Square(Rectangle):
def set_width(self, width: int):
self._width = width
self._height = width # Quebra o LSP!
def set_height(self, height: int):
self._width = height # Quebra o LSP!
self._height = height
def calculate_area(rect: Rectangle):
rect.set_width(5)
rect.set_height(4)
assert rect.area() == 20 # Falha para Square!
Square altera o comportamento herdado de Rectangle, quebrando a substituibilidade.
Bom Exemplo
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def print_area(shape: Shape):
print(f"Area: {shape.area()}") # Funciona para qualquer Shape
# Ambos funcionam corretamente
print_area(Rectangle(5, 4)) # Área: 20
print_area(Square(5)) # Área: 25
I — Princípio da Segregação de Interfaces (ISP)
Definição: Os clientes não devem ser forçados a depender de interfaces que não utilizam.
Mau Exemplo
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
class Human(Worker):
def work(self):
print("Working")
def eat(self):
print("Eating")
def sleep(self):
print("Sleeping")
class Robot(Worker):
def work(self):
print("Working")
def eat(self):
pass # Os robots não comem — forçados a implementar
def sleep(self):
pass # Os robots não dormem — forçados a implementar
Bom Exemplo
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Sleepable(ABC):
@abstractmethod
def sleep(self):
pass
class Human(Workable, Eatable, Sleepable):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
def sleep(self):
print("Human sleeping")
class Robot(Workable):
def work(self):
print("Robot working")
# Factory que só precisa de workers
def assign_work(workers: list[Workable]):
for worker in workers:
worker.work()
# Funciona com ambos
assign_work([Human(), Robot()])
Os clientes dependem apenas das interfaces de que realmente precisam.
D — Princípio da Inversão de Dependências (DIP)
Definição: Os módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
Mau Exemplo
class MySQLDatabase:
def connect(self):
return mysql.connect("localhost", "user", "pass")
def query(self, sql: str):
return self.connect().execute(sql)
class UserService:
def __init__(self):
self.db = MySQLDatabase() # Dependência direta de MySQL
def get_user(self, user_id: int):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
UserService está fortemente acoplado a MySQL — não é fácil mudar de base de dados.
Bom Exemplo
from abc import ABC, abstractmethod
class Database(ABC):
"""Abstraction that both high and low-level modules depend on."""
@abstractmethod
def query(self, sql: str):
pass
@abstractmethod
def execute(self, sql: str, params: tuple):
pass
class MySQLDatabase(Database):
def __init__(self, host: str, user: str, password: str):
self.connection = mysql.connect(host, user, password)
def query(self, sql: str):
return self.connection.execute(sql)
def execute(self, sql: str, params: tuple):
return self.connection.execute(sql, params)
class PostgreSQLDatabase(Database):
def __init__(self, host: str, user: str, password: str):
self.connection = psycopg2.connect(host=host, user=user, password=password)
def query(self, sql: str):
return self.connection.cursor().execute(sql)
def execute(self, sql: str, params: tuple):
return self.connection.cursor().execute(sql, params)
class UserService:
def __init__(self, database: Database): # Depende de uma abstração
self.db = database
def get_user(self, user_id: int):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# Fácil mudar implementações
mysql_db = MySQLDatabase("localhost", "user", "pass")
user_service = UserService(mysql_db)
# Ou usar PostgreSQL sem alterar UserService
postgres_db = PostgreSQLDatabase("localhost", "user", "pass")
user_service = UserService(postgres_db)
# Ou simular (mock) para testes
class MockDatabase(Database):
def query(self, sql: str):
return [{"id": 1, "name": "Test User"}]
def execute(self, sql: str, params: tuple):
pass
test_service = UserService(MockDatabase())
Exemplo Completo: Sistema de Processamento de Encomendas
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List
# Modelos de Domínio (SRP)
@dataclass
class Product:
id: str
name: str
price: float
@dataclass
class Order:
id: str
products: List[Product]
@property
def total(self) -> float:
return sum(p.price for p in self.products)
# Abstrações (DIP)
class OrderRepository(ABC):
@abstractmethod
def save(self, order: Order) -> None:
pass
@abstractmethod
def find(self, order_id: str) -> Order:
pass
class NotificationService(ABC):
@abstractmethod
def notify(self, message: str, recipient: str) -> None:
pass
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount: float, payment_details: dict) -> bool:
pass
# Implementações (OCP — é possível adicionar novas implementações)
class DatabaseOrderRepository(OrderRepository):
def save(self, order: Order) -> None:
db.execute("INSERT INTO orders...")
def find(self, order_id: str) -> Order:
return db.query("SELECT * FROM orders WHERE id = ?", order_id)
class EmailNotificationService(NotificationService):
def notify(self, message: str, recipient: str) -> None:
smtp.send(recipient, "Order Update", message)
class SMSNotificationService(NotificationService):
def notify(self, message: str, recipient: str) -> None:
twilio.send(recipient, message)
class StripePaymentGateway(PaymentGateway):
def charge(self, amount: float, payment_details: dict) -> bool:
return stripe.charge(amount, payment_details['card_token'])
# Serviço de alto nível (DIP — depende de abstrações)
class OrderService:
def __init__(
self,
repository: OrderRepository,
notification: NotificationService,
payment: PaymentGateway
):
self.repository = repository
self.notification = notification
self.payment = payment
def place_order(self, order: Order, payment_details: dict, customer_email: str) -> bool:
if self.payment.charge(order.total, payment_details):
self.repository.save(order)
self.notification.notify(
f"Order {order.id} confirmed! Total: ${order.total}",
customer_email
)
return True
return False
# Injeção de Dependências (composition root)
def create_order_service() -> OrderService:
return OrderService(
repository=DatabaseOrderRepository(),
notification=EmailNotificationService(),
payment=StripePaymentGateway()
)
# Utilização
service = create_order_service()
order = Order("123", [Product("1", "Widget", 29.99)])
service.place_order(order, {"card_token": "tok_xxx"}, "[email protected]")
Principais Conclusões
- SRP: Uma classe, uma responsabilidade
- OCP: Estender através de abstração, não de modificação
- LSP: As subclasses devem respeitar os contratos da classe pai
- ISP: Interfaces pequenas e focadas em vez de grandes
- DIP: Depender de abstrações, injetar dependências
Os princípios SOLID não são regras rígidas — são diretrizes que melhoram a qualidade do código quando aplicadas de forma ponderada. Comece por identificar «code smells» e, depois, refatore progressivamente na direção destes princípios.