SOLID é um acrônimo que representa cinco princípios fundamentais da programação orientada a objetos e design de software. Esses princípios ajudam os desenvolvedores a criar sistemas mais compreensíveis, flexíveis e manuteníveis. Aqui está um breve resumo de cada princípio:
S - Princípio da Responsabilidade Única (Single Responsibility Principle): Uma classe deve ter apenas uma razão para mudar. Isso significa que uma classe deve ter apenas um trabalho ou responsabilidade. Seguir este princípio ajuda a manter as classes coesas e facilita a manutenção do código.
Exemplo Violando o SRP
Suponha que temos uma classe que gerencia usuários, mas ela faz tanto a gestão de dados do usuário quanto a sua validação:
class UserManager {
constructor(user) {
this.user = user;
}
saveUser() {
// Salva o usuário no banco de dados
console.log('Usuário salvo no banco de dados');
}
validateUser() {
// Valida os dados do usuário
if (this.user.name === '') {
console.log('O nome do usuário é obrigatório');
return false;
}
return true;
}
}
**Aplicando o SRP
**Para aderir ao SRP, podemos dividir essa classe em duas: uma para gerenciar usuários (UserManager) e outra para validar os dados do usuário (UserValidator):
class UserValidator {
static validate(user) {
// Valida os dados do usuário
if (user.name === '') {
console.log('O nome do usuário é obrigatório');
return false;
}
return true;
}
}
class UserManager {
constructor(user) {
this.user = user;
}
saveUser() {
if (UserValidator.validate(this.user)) {
// Salva o usuário no banco de dados apenas se for válido
console.log('Usuário salvo no banco de dados');
} else {
console.log('Falha ao salvar o usuário: dados inválidos');
}
}
}
Neste exemplo revisado, UserValidator é responsável exclusivamente pela validação dos dados do usuário, enquanto UserManager gerencia o usuário, incluindo salvar os dados do usuário no banco de dados. Isso torna o código mais modular, fácil de entender e manter, seguindo o Princípio da Responsabilidade Única.
O - Princípio Aberto/Fechado (Open/Closed Principle): Entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação. Isso significa que você deve ser capaz de adicionar novas funcionalidades sem alterar o código existente, o que pode ser alcançado através do uso de interfaces ou classes abstratas, por exemplo.
Vamos considerar um exemplo baseado no trecho de código fornecido, onde temos uma classe InventoryEventsManager que gerencia eventos de inventário. Suponha que queremos adicionar funcionalidades para tratar diferentes tipos de eventos de inventário sem alterar a classe InventoryEventsManager existente.
*Antes: Sem Aplicar o Princípio Aberto/Fechado
*
class InventoryEventsManager {
async save(inventoryEventsData) {
// Lógica para salvar eventos de inventário
}
// Suponha que queremos adicionar um novo tipo de evento de inventário
async saveSpecialEvent(specialEventData) {
// Lógica para salvar um tipo especial de evento de inventário
}
}
Neste exemplo, para adicionar um novo tipo de evento, precisaríamos modificar a classe InventoryEventsManager, adicionando um novo método saveSpecialEvent, o que viola o Princípio Aberto/Fechado.
Depois: Aplicando o Princípio Aberto/Fechado
Para aderir ao princípio, podemos usar a herança ou interfaces para estender a funcionalidade da classe InventoryEventsManager sem modificá-la diretamente.
class InventoryEventsManager {
async save(inventoryEventsData) {
// Lógica para salvar eventos de inventário
}
}
class SpecialInventoryEventsManager extends InventoryEventsManager {
async save(inventoryEventsData) {
// Lógica especializada para salvar eventos de inventário especiais
}
}
Neste exemplo revisado, criamos uma nova classe SpecialInventoryEventsManager que estende InventoryEventsManager. A classe SpecialInventoryEventsManager pode sobrescrever o método save para tratar a lógica específica de eventos de inventário especiais. Dessa forma, adicionamos novas funcionalidades estendendo a classe existente, sem modificar seu código original, cumprindo o Princípio Aberto/Fechado.
Este princípio promove um design de software mais sustentável e fácil de manter, permitindo a extensão de funcionalidades sem a necessidade de modificar o código existente.
**L - Princípio da Substituição de Liskov (Liskov Substitution Principle): **Objetos de uma classe base devem ser substituíveis por objetos de suas classes derivadas sem quebrar a aplicação. Isso enfatiza que uma subclasse deve ser substituível por sua superclasse.
Vamos considerar um exemplo simples em JavaScript para ilustrar o LSP, usando uma classe base Bird e duas subclasses Duck e Ostrich.
*Exemplo Violando o LSP
*
class Bird {
fly() {
console.log("Este pássaro pode voar");
}
}
class Duck extends Bird {
quack() {
console.log("Quack");
}
}
class Ostrich extends Bird {
fly() {
throw new Error("Não pode voar");
}
run() {
console.log("Este pássaro pode correr");
}
}
function makeBirdFly(bird) {
bird.fly();
}
const duck = new Duck();
const ostrich = new Ostrich();
makeBirdFly(duck); // Funciona bem
makeBirdFly(ostrich); // Lança um erro, violando o LSP
Neste exemplo, Ostrich é uma subclasse de Bird, mas avestruzes não podem voar. A função makeBirdFly espera que qualquer subclasse de Bird possa voar, o que não é verdade para Ostrich, violando o LSP.
**Exemplo Aplicando o LSP
**Para aderir ao LSP, podemos reestruturar o design das classes para garantir que subclasses possam ser usadas no lugar de uma classe base sem alterar o comportamento esperado.
class Bird {
}
class FlyingBird extends Bird {
fly() {
console.log("Este pássaro pode voar");
}
}
class Duck extends FlyingBird {
quack() {
console.log("Quack");
}
}
class Ostrich extends Bird {
run() {
console.log("Este pássaro pode correr");
}
}
function makeBirdFly(bird) {
if (bird instanceof FlyingBird) {
bird.fly();
}
}
const duck = new Duck();
const ostrich = new Ostrich();
makeBirdFly(duck); // Funciona bem
makeBirdFly(ostrich); // Não faz nada, mas não quebra o programa
Neste exemplo revisado, criamos uma nova classe FlyingBird para representar pássaros que podem voar. Duck é uma subclasse de FlyingBird, enquanto Ostrich é uma subclasse direta de Bird, refletindo melhor suas capacidades. A função makeBirdFly agora verifica se o pássaro pode voar antes de chamar fly, garantindo que o programa não quebre quando um objeto Ostrich é passado, aderindo ao Princípio da Substituição de Liskov.
I - Princípio da Segregação de Interface (Interface Segregation Principle): Uma classe não deve ser forçada a implementar interfaces que não vai utilizar. Isso significa que é melhor ter várias interfaces específicas do que uma única interface genérica.
Vamos considerar um exemplo em JavaScript, usando classes e herança para simular interfaces, uma vez que JavaScript não tem interfaces nativas como outras linguagens, como Java ou C#.
Exemplo Violando o ISP
Suponha que temos uma interface genérica para trabalhar com conteúdo:
class ContentManager {
create(content) {
// Lógica para criar conteúdo
}
read(id) {
// Lógica para ler conteúdo
}
update(id, content) {
// Lógica para atualizar conteúdo
}
delete(id) {
// Lógica para deletar conteúdo
}
share(id, target) {
// Lógica para compartilhar conteúdo
}
}
Uma classe ArticleManager que gerencia artigos pode precisar implementar todos esses métodos, mas uma classe CommentManager que gerencia comentários pode não precisar do método share, por exemplo. Isso força CommentManager a implementar um método que não é relevante para seu contexto, violando o ISP.
Exemplo Aplicando o ISP
Para aderir ao ISP, podemos dividir a interface ContentManager em várias interfaces menores, cada uma focada em uma responsabilidade específica:
class Creatable {
create(content) {}
}
class Readable {
read(id) {}
}
class Updatable {
update(id, content) {}
}
class Deletable {
delete(id) {}
}
class Shareable {
share(id, target) {}
}
class ArticleManager extends Creatable, Readable, Updatable, Deletable, Shareable {
create(content) {
// Implementação específica
}
read(id) {
// Implementação específica
}
update(id, content) {
// Implementação específica
}
delete(id) {
// Implementação específica
}
share(id, target) {
// Implementação específica
}
}
class CommentManager extends Creatable, Readable, Updatable, Deletable {
create(content) {
// Implementação específica
}
read(id) {
// Implementação específica
}
update(id, content) {
// Implementação específica
}
delete(id) {
// Implementação específica
}
// Note que CommentManager não precisa implementar share
}
Neste exemplo revisado, ArticleManager implementa todas as interfaces, pois artigos precisam de todas essas funcionalidades. Por outro lado, CommentManager implementa apenas as interfaces relevantes para comentários, excluindo Shareable. Isso segue o Princípio da Segregação de Interface, garantindo que as classes implementem apenas as interfaces que realmente utilizam, mantendo o código limpo e focado.
D - Princípio da Inversão de Dependência (Dependency Inversion Principle): Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Além disso, abstrações não devem depender de detalhes; detalhes devem depender de abstrações. Isso incentiva o desacoplamento entre módulos do software.
Vamos considerar um exemplo baseado no trecho de código fornecido, onde temos uma classe InventoryEventsManager que gerencia eventos de inventário. Suponha que queremos aplicar o DIP para reduzir o acoplamento entre InventoryEventsManager e um sistema de log específico.
*Antes: Sem Aplicar o DIP
*
O código fornecido mostra uma dependência direta da classe InventoryEventsManager em um sistema de log específico (logger), o que viola o DIP porque a classe de alto nível (InventoryEventsManager) está diretamente dependente de um detalhe de baixo nível (logger).
class InventoryEventsManager {
async save(inventoryEventsData) {
try {
// Lógica para salvar eventos de inventário
} catch (error) {
logger.error({ // Dependência direta de um sistema de log específico
data: inventoryEventsData,
error,
message: error.message,
tag: this.getTag(this.save.name, statusError.ERROR),
});
throw error;
}
}
}
*Depois: Aplicando o DIP
*
Para aplicar o DIP, podemos introduzir uma abstração (interface) para o sistema de log e fazer com que tanto InventoryEventsManager quanto a implementação específica do sistema de log dependam dessa abstração.
// Interface para abstrair o sistema de log
class ILogger {
error(message) {}
}
// Implementação concreta do sistema de log
class Logger extends ILogger {
error(message) {
console.error(message); // Implementação específica do log
}
}
// Modificando InventoryEventsManager para depender da abstração ILogger
class InventoryEventsManager {
constructor(logger) {
this.logger = logger; // Injeção de dependência
}
async save(inventoryEventsData) {
try {
// Lógica para salvar eventos de inventário
} catch (error) {
this.logger.error({ // Usando a abstração em vez de uma implementação específica
data: inventoryEventsData,
error,
message: error.message,
tag: this.getTag(this.save.name, statusError.ERROR),
});
throw error;
}
}
}
// Uso
const logger = new Logger();
const inventoryEventsManager = new InventoryEventsManager(logger);
Neste exemplo revisado, InventoryEventsManager agora depende de uma abstração (ILogger) em vez de uma implementação específica de log (Logger). Isso permite que a classe InventoryEventsManager seja menos acoplada e mais flexível, pois podemos facilmente substituir a implementação do sistema de log sem alterar o código de InventoryEventsManager, seguindo o Princípio da Inversão de Dependência.
Conclusão:
Seguir os princípios SOLID pode ajudar a criar um código mais limpo, compreensível e fácil de refatorar ou expandir, contribuindo para a qualidade geral do software e reduzindo a complexidade do desenvolvimento e manutenção.
Top comments (0)