src | theme | class | highlighter | mdc | drawings | image | selectable | colorSchema | title | author | export | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
./cover.md |
default |
text-center |
shiki |
true |
|
/side-logo.png |
true |
dark |
S.O.L.I.D. |
Pablo Leon Rodrigues |
|
layout: image-right image: /RobertCMartin.jpg
Publicado em 2000 em um paper intitulado "Design Principles and Design Patterns" por Robert C. Martin (Aka. Uncle Bob).
SOLID é um acrônimo para cinco princípios de design orientado a objetos que visam melhorar a legibilidade, manutenção e flexibilidade do código.
O SRP define que uma classe ou módulo deve ter apenas uma razão para mudar, ou seja, deve possuir uma única responsabilidade. Em termos simples, cada classe, função, ou módulo deve focar em realizar uma única tarefa ou propósito.
Se uma classe tem várias responsabilidades, ela pode ser afetada por mudanças em qualquer uma dessas responsabilidades, tornando o código mais suscetível a falhas.
- Manutenção facilitada:{style="color: green;"} Se uma classe faz apenas uma coisa, é mais fácil localizar e corrigir problemas quando algo der errado.
- Redução de acoplamento:{style="color: green;"} Classes com responsabilidades únicas tendem a ser menos dependentes umas das outras, o que facilita a refatoração e evolução do sistema.
- Reutilização de código:{style="color: green;"} Classes pequenas e especializadas são mais fáceis de reutilizar em diferentes contextos.
- Testabilidade:{style="color: green;"} Classes que cumprem apenas uma responsabilidade têm menos dependências e cenários a serem testados, facilitando a criação de testes unitários.
class Report {
generate() {
// gerar o relatório
}
format() {
// formatar o relatório
}
sendByEmail() {
// lógica para enviar
}
}
::right::
class ReportGenerator {
generate() {
//gerar o relatório
}
}
class ReportFormatter {
format() {
//formatar o relatório
}
}
class EmailSender {
send(report: Report) {
// lógica para enviar
}
}
O Open/Closed Principle afirma que uma classe, módulo ou função deve estar aberto para extensão, mas fechado para modificação. Isso significa que o comportamento de um sistema deve poder ser estendido sem a necessidade de alterar o código-fonte existente.
A ideia principal é separar o código que pode mudar do que permanece estável. Para isso, você pode usar abstrações, como interfaces, herança e polimorfismo, dependendo da linguagem utilizada.
- Maior flexibilidade e extensibilidade:{style="color: green;"} Você pode adicionar novos comportamentos ao software sem alterar seu funcionamento atual, facilitando a evolução do código.
- Menos risco de bugs:{style="color: green;"} Como você não modifica o código que já está em produção, reduz-se o risco de introduzir novos problemas ao adicionar funcionalidades.
- Reutilização de código:{style="color: green;"} As abstrações utilizadas para aplicar o OCP incentivam a reutilização de componentes, o que torna o código mais eficiente.
class Discount {
calculate(type: string): number {
if (type === 'regular') {
return 0.1;
} else if (type === 'vip') {
return 0.2;
} else {
return 0;
}
}
}
::right::
interface Discount {
calculate(): number;
}
class Regular implements Discount {
calculate(): number {
return 0.1;
}
}
class VIP implements Discount {
calculate(): number {
return 0.2;
}
}
O princípio de substituição de Liskov, proposto por Barbara Liskov, afirma que objetos de uma classe derivada (subclasse) devem poder ser substituídos por objetos da classe base (superclasse) sem alterar as propriedades corretas do programa.
Se você tem uma classe base e uma subclasse que herda dela, a subclasse deve preservar o comportamento esperado da classe base. Caso contrário, a herança não está sendo aplicada corretamente, e isso pode quebrar o código que utiliza a classe base.
class Bird {
fly() {
console.log("pássaro voando");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Pinguins não voam");
}
}
::right::
class Bird {
// comportamentos de todos os pássaros
}
class FlyingBird extends Bird {
fly() {
console.log("pássaro voando");
}
}
class Penguin extends Bird {
// pinguins não podem voar
swim() {
console.log("O pinguim nada");
}
}
O ISP afirma que os clientes não devem ser forçados a depender de interfaces que não utilizam. Em outras palavras, uma interface deve ser específica para as necessidades dos clientes que a utilizam, em vez de ser uma interface "inchada" com métodos que alguns clientes não precisam.
Isso significa que é melhor ter várias interfaces pequenas e especializadas, cada uma com métodos diretamente relacionados a um determinado comportamento, do que uma única interface grande e generalizada.
- Evita a implementação de métodos desnecessários:{style="color: green;"} As classes só implementam o que realmente precisam, reduzindo a complexidade.
- Menor acoplamento:{style="color: green;"} Cada cliente só depende de uma interface que faz sentido para ele, o que torna o código mais modular.
- Facilidade de manutenção:{style="color: green;"} Interfaces menores são mais fáceis de entender, modificar e manter, já que mudanças em uma parte do sistema não impactam outras desnecessariamente.
interface MultiPrinter {
print(): void;
scan(): void;
fax(): void;
}
class Printer implements MultiPrinter {
print(): void {
console.log("Imprimindo...");
}
scan(): void {
}
fax(): void {
}
}
::right::
Podemos refatorar o código e criar multiplas interfaces
interface Printer {
print(): void;
}
interface Scanner {
scan(): void;
}
interface Fax {
fax(): void;
}
class SimplePrinter implements Printer {
print(): void {
console.log("Imprimindo...");
}
}
class OfficePrinter
implements Printer, Fax {
print(): void {
console.log("Imprimindo...");
}
fax(): void {
console.log("Enviando fax...");
}
}
::right::
class AdvancedPrinter
implements Printer, Scanner, Fax {
print(): void {
console.log("Imprimindo...");
}
scan(): void {
console.log("Escaneando...");
}
fax(): void {
console.log("Enviando fax...");
}
}
O DIP afirma que módulos de alto nível não devem depender de módulos de baixo nível, ele sugere que devemos depender de interfaces ou abstrações em vez de depender de implementações concretas. Isso reduz o acoplamento entre as diferentes partes de um sistema, tornando-o mais flexível e fácil de modificar.
- Desacoplamento:{style="color: green;"} O DIP reduz o acoplamento entre diferentes partes do sistema, permitindo que mudanças em uma parte não exijam mudanças em outra.
- Facilidade para testar:{style="color: green;"} Quando dependemos de abstrações, é mais fácil substituir implementações reais por mocks ou stubs em testes.
- Flexibilidade:{style="color: green;"} O sistema pode evoluir e adaptar-se a novos requisitos sem grandes refatorações, uma vez que as implementações podem ser alteradas sem impactar os módulos de alto nível.
- Reutilização de código:{style="color: green;"} Interfaces e abstrações bem definidas promovem a reutilização de módulos, já que eles podem trabalhar com diferentes implementações.
Nesse caso, a classe NotificationManager
(alto nível) depende diretamente da classe EmailService
(baixo nível). Isso é uma violação do DIP, porque NotificationManager está fortemente acoplado a um serviço específico para enviar notificações. Se um dia for necessário mudar para um serviço de notificação por SMS, ou adicionar outros tipos de notificação, será necessário modificar o código de NotificationManager, o que quebra a flexibilidade e a modularidade.
class EmailService {
sendEmail(to: string, message: string): void {
console.log(`Enviando e-mail para ${to}: ${message}`);
}
}
class NotificationManager {
private emailService: EmailService;
constructor() {
this.emailService = new EmailService();
}
notify(to: string, message: string): void {
this.emailService.sendEmail(to, message);
}
}
Para corrigir, podemos criar uma abstração (interface).
interface NotificationService {
send(to: string, message: string): void;
}
class EmailService implements NotificationService {
send(to: string, message: string): void { ... }
}
class SMSService implements NotificationService {
send(to: string, message: string): void { ... }
}
class NotificationManager {
private notificationService: NotificationService;
constructor(notificationService: NotificationService) {
this.notificationService = notificationService;
}
notify(to: string, message: string): void {
this.notificationService.send(to, message);
}
}