Executando verificação de segurança...
1

SOLID e DRY no Desenvolvimento de Software

Introdução:

Ao desenvolver software, frequentemente nos deparamos com desafios relacionados à manutenção, organização e reutilização de código.

Dois princípios de design de software amplamente reconhecidos, SOLID e DRY, oferecem orientação valiosa para abordar esses desafios de maneira eficaz. No entanto, em certos casos, esses princípios podem parecer conflitantes, especialmente ao lidar com a estruturação de serviços, a reutilização de código e a divisão de responsabilidades.

Neste post, quero explorar esse problema em profundidade, discutindo as abordagens para lidar com ele e como adaptar os princípios para atender às necessidades específicas de um projeto.

O que é SOLID?

SOLID é um acrônimo que representa cinco princípios de design de software, cada um focado em promover código limpo, coeso e modular. Vejamos:

S - Princípio da Responsabilidade Única (Single Responsibility Principle): Uma classe deve ter apenas uma razão para mudar, ou seja, ela deve ter uma única responsabilidade bem definida.
O - Princípio do Aberto/Fechado (Open/Closed Principle): Entidades de software (classes, módulos, funções, etc.) devem ser abertas para extensão, mas fechadas para modificação. Isso significa que devemos ser capazes de estender o comportamento de uma classe sem precisar modificar seu código fonte.
L - Princípio da Substituição de Liskov (Liskov Substitution Principle): Objetos de uma classe base devem ser substituíveis por objetos de suas subclasses sem afetar a integridade do sistema.
I - Princípio da Segregação de Interfaces (Interface Segregation Principle): Muitas interfaces específicas são melhores do que uma única interface genérica. Esse princípio promove o design de interfaces coesas e específicas para as necessidades dos clientes.
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, mas sim detalhes devem depender de abstrações.

O que é DRY?

DRY é um princípio de design de software que incentiva a reutilização de código, evitando a duplicação. A sigla significa "Don't Repeat Yourself" (ou não se repita), o que significa que cada parte do sistema deve ter uma representação única e não ambígua dentro do sistema.

O Conflito entre SRP e DRY:

Embora os princípios SOLID e DRY sejam altamente valorizados na engenharia de software, às vezes surge um conflito aparente entre o Princípio da Responsabilidade Única (SRP) e o Princípio DRY (Don't Repeat Yourself).

O SRP enfatiza que uma classe deve ter apenas uma razão para mudar, o que implica que cada classe deve ter uma única responsabilidade bem definida. Por outro lado, o DRY incentiva a reutilização de código, evitando a duplicação de lógica.

Esses princípios podem entrar em conflito quando, para seguir rigorosamente o SRP, acabamos dividindo a funcionalidade em várias classes menores com responsabilidades específicas, resultando em uma fragmentação excessiva do código. Isso pode levar à duplicação de código, já que cada classe separada pode precisar de funcionalidades semelhantes e a capacidade de abstração do código pode ser afetada.

Portanto, surge a questão: como conciliar esses princípios aparentemente conflitantes e garantir um código limpo, coeso e reutilizável?

Problemas de seguir apenas DRY:

Embora o princípio DRY (Don't Repeat Yourself) seja essencial para promover a reutilização de código e evitar a duplicação, sua aplicação cega pode resultar em alguns problemas, especialmente quando se trata da estruturação de serviços que dependem de uma classe abstrata comum.

Considere uma situação em que você tem uma classe abstrata chamada AbstractService, que contém métodos e lógicas comuns compartilhados por diferentes serviços em sua aplicação. Essa classe é estendida por outras classes de serviço, como UserService e PostService, que fornecem funcionalidades específicas para manipulação de usuários e postagens, respectivamente.

Agora, imagine que você identifica um bug em um dos métodos comuns na classe AbstractService e decide corrigi-lo. No entanto, após a correção, você percebe que essa correção afeta o comportamento de todos os serviços que estendem a classe AbstractService, incluindo UserService e PostService, potencialmente introduzindo comportamentos inesperados no código.

Soluções e Estratégias:

Para evitar esses problemas, é essencial adotar uma abordagem equilibrada, combinando os princípios SOLID e DRY de forma inteligente. Aqui estão algumas estratégias que você pode considerar:

  1. Revisão cuidadosa da abstração: Ao criar uma classe abstrata ou um conjunto de funcionalidades compartilhadas, revise cuidadosamente quais partes do código são realmente comuns e quais são específicas para cada serviço. Evite a tentação de generalizar demais.
  2. Separação de preocupações: Se diferentes serviços têm necessidades diferentes, não hesite em criar classes separadas para cada um, mesmo que isso resulte em alguma duplicação de código. Isso reduzirá a interdependência e os efeitos colaterais entre os diferentes serviços.
  3. Uso de Interfaces e Traits: Em vez de depender estritamente da herança de classes, considere usar interfaces e traits para compartilhar funcionalidades comuns entre classes. Isso permite maior flexibilidade e granularidade no compartilhamento de código.
  4. Testes Unitários: Implemente testes unitários abrangentes para cada serviço e classe de serviço. Isso ajudará a identificar rapidamente quaisquer regressões ou problemas de compatibilidade quando você fizer alterações em funcionalidades comuns.

Ao adotar uma abordagem mais reflexiva e equilibrada, você pode garantir que seu código permaneça modular, reutilizável e fácil de manter, sem comprometer a integridade dos princípios SOLID e DRY.

Adaptação dos Princípios SOLID e DRY

Estrutura de Classes:

Ao enfrentar o desafio de reconciliar os princípios SOLID e DRY em serviços de um sistema, uma abordagem equilibrada pode envolver a criação de uma classe abstrata focada na inicialização dos serviços e funcionalidades basicas, como iniciar um repositório para persistência dos dados, e a partir desse ponto, criar traits para promover uma herança horizontal para cada funcionalidade, como "create", "update" e "delete". Dessa forma a classe que extender a classe abstrata pode implementar traits conforme suas necessidades de forma individualizada.

Exemplo Prático:

Considere um cenário em que temos serviços de usuário e posts, cada um com operações de criação, leitura, atualização e exclusão. A abordagem envolveria:

  • Criação de uma classe abstrata para esses serviços, já que ambos utilizam a mesma implementação para o repositório de dados (aqui é um ótimo uso para interfaces e factories);
  • Criação de traits para as ações de criação, leitura, atualização, e exclusão (CRUD);
  • Criação das classes concretas para os serviços com métodos adicionais caso necessáros;
// Classe abstrata para inicialização de serviços
abstract class AbstractService {
    private RepositoryInterface $repository;
    protected string $entity;
    public function __construct(protected RepositoryFactoryInterface $repositoryFactory) {
        $this->repository = $this->repositoryFactory->make($this->entity);
    }

    protected function repository() {
        return $this->repository;
    }
}

// Trait para criação
trait CreatableTrait {
    abstract public function repository(): RepositoryInterface;
    public function create(...$data) {
        return $this->repository()->create($data); // Lógica para criação
    }
}

// Trait para atualização
trait UpdatableTrait {
    abstract public function repository(): RepositoryInterface;
    public function update(object $entity) {
        return $this->repository()->update($entity); // Lógica para atualização
    }
}

// Trait para exclusão
trait DeletableTrait {
    abstract public function repository(): RepositoryInterface;
    public function delete(object $entity) {
        return $this->repository()->delete($entity); // Lógica para exclusão
    }
}

// Trait para leitura
trait ReadableTrait {
    abstract public function repository(): RepositoryInterface;
    public function find(int $id) {
        return $this->repository()->find($id); // Lógica para leitura
    }
}

final class Users extends AbstractService {
    use CreatableTrait, UpdatableTrait, DeletableTrait, ReadableTrait;
    protected string $entity = 'user';

    public function updatePassword(int $userId, string $newPassword) {
        // Lógica específica de atualização de senha
    }

    public function findByEmail(string $email) {
        // Lógica específica de busca por email
    }

    public function findByUsername(string $username) {
        // Lógica específica de busca por username
    }
}

final class Posts extends AbstractService {
    use CreatableTrait, UpdatableTrait, DeletableTrait, ReadableTrait;
    protected string $entity = 'post';

    public function findByAuthor(int $authorId) {
        // Lógica específica de busca por autor
    }
}

Vantagens dessa Abordagem:

  • Separação Clara de Responsabilidades: Cada classe e trait tem uma responsabilidade única e bem definida, seguindo o princípio da Responsabilidade Única (SRP).
  • Reutilização Controlada de Código: O uso de traits permite a reutilização controlada de funcionalidades comuns entre diferentes classes e as classes abstratas podem fornecer funcionalidades e propriedades usadas pelas classes estendidas, seguindo o princípio DRY.
  • Flexibilidade para Extensão: A estrutura permite a fácil extensão de funcionalidades específicas para cada serviço sem afetar outras partes do sistema.
  • Testabilidade: Cada classe independente pode ser testada de forma isolada, facilitando a identificação e correção de problemas.

A partir dessa abordagem, podemos corrigir ou alterar uma funcionalidade específica para um serviço apenas removendo ou substituindo uma trait.

Dentro dessa solução, ainda podemos adicionar interfaces para serem seguidas pelos serviços, garantindo que substituições feitas ainda sigam a mesma implementação.

Conclusão:

Ao adotar uma abordagem que utiliza classes abstratas para inicialização, traits para funcionalidades comuns e classes independentes para serviços específicos, é possível conciliar de maneira eficaz os princípios SOLID e DRY. Isso resulta em um código modular, reutilizável e de fácil manutenção, mantendo a integridade dos princípios fundamentais de design de software.

Carregando publicação patrocinada...
1

Veja https://pt.stackoverflow.com/q/120931/101.

Quem foge do DRY, real, terá a bunda modida em algum momento.

Complexidade também. Para admitir uma complexidade tem que proivar que ela se paga. O DRY (correto) mata complexidade por definição.

Ajudei? Era o meu desejo.


Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente (não vendo nada, é retribuição na minha aposentadoria) (links aqui).

1

A sua resposta é praticamente uma aula, parabéns pelo conteúdo, vou salvar o link para ler com mais calma.

Gostei muito que em uma primeira vista pelo menos a ideia que tentei passar está de acordo com a sua resposta embora a sua tenha sido muito mais completa e detalhada que meu texto que pretende discutir sobre a aplicação correta apenas (espero que tenha conseguido).