👩‍💻 Entenda de uma vez o que é SOLID

2024-05-2040 minTécnico

O que é SOLID?

SOLID é um acrônimo criado por Michael Feathers, após observar que cinco princípios da orientação a objetos e design de código — Criados por Robert C. Martin _(_a.k.a. Uncle Bob) e abordados no artigo The Principles of OOD  poderiam se encaixar nesta palavra.

S.O.L.I.D: Os 5 princípios da POO

  1. S — Single Responsibility Principle (Princípio da responsabilidade única)
  2. O — Open-Closed Principle (Princípio Aberto - Fechado)
  3. L — Liskov Substitution Principle (Princípio da substituição de Liskov)
  4. I — Interface Segregation Principle (Princípio da Segregação da Interface)
  5. D — Dependency Inversion Principle (Princípio da inversão da dependência)

Esses princípios auxiliam o programador a escrever códigos mais limpos, separando responsabilidades, diminuindo acoplamentos, facilitando na refatoração e estimulando o reaproveitamento do código.

S → SRP - Single Responsibility Principle

Princípio da Responsabilidade Única ⇒ Uma classe deve ter um, e somente um, motivo para mudar.

Esse princípio declara que uma classe deve ser especializada em um único assunto e possuir apenas uma responsabilidade no software, ou seja, a classe deve ter uma única tarefa ou ação para executar.

Quando estamos aprendendo programação orientada a objetos, sem sabermos, damos a uma classe mais de uma responsabilidade e acabamos criando classes que fazem de tudo — God Class*. Num primeiro momento isso pode parecer eficiente, mas como as responsabilidades acabam se misturando, quando há necessidade de realizar alterações nessa classe, será difícil modificar uma dessas responsabilidades sem comprometer as outras.

💡 God Class — Classe Deus: Na programação orientada a objetos, é uma classe que sabe muito ou faz coisas demais.

A violação do Single Responsibility Principle pode gerar alguns problemas, sendo eles:

  • Falta de coesão → uma classe não deve assumir responsabilidades que não são suas;
  • Alto acoplamento → Mais responsabilidades geram um maior nível de dependências, deixando o sistema engessado e frágil para alterações;
  • Dificuldades na implementação de testes automatizados — É difícil de “mockar” esse tipo de classe;
  • Dificuldades para reaproveitar o código;

Violação SRP

Considere uma classe chamada Feed que representa o feed de uma pessoa no Instagram. Essa classe seria responsável por armazenar as postagens da pessoa, exibi-las na tela e permitir que a pessoa interaja com elas.

O código da classe Feed poderia ser o seguinte:

public class Feed {

    private List<Post> posts;

    public Feed() {
        this.posts = new ArrayList<>();
    }

    public void addPost(Post post) {
        this.posts.add(post);
    }

    public List<Post> getPosts() {
        return this.posts;
    }

    public void display() {
        for (Post post : this.posts) {
            System.out.println(post);
        }
    }

    public void interactWithPost(Post post) {
        if (post.isLiked()) {
            post.setLiked(false);
        } else {
            post.setLiked(true);
        }
    }
}

Essa classe viola o princípio da responsabilidade única porque tem duas responsabilidades principais:

  • Armazenar as postagens da pessoa
  • Exibir as postagens da pessoa e permitir que a pessoa interaja com elas

Essas duas responsabilidades são muito diferentes e podem mudar por motivos diferentes. Por exemplo, a forma como as postagens são armazenadas pode mudar se o aplicativo for portado para uma nova plataforma. Já a forma como as postagens são exibidas e como a pessoa interage com elas pode mudar se o aplicativo for atualizado com novos recursos.

Correção

Para corrigir a violação do princípio da responsabilidade única na classe Feed, podemos dividir a classe em duas classes menores:

1. Classe PostStore:

  • Responsável por armazenar e recuperar postagens.
  • Possui métodos como addPost()getPost()deletePost() etc.
  • Implementa uma interface PostStoreInterface que define os métodos de armazenamento e recuperação de postagens.

2. Classe FeedView:

  • Responsável por exibir as postagens e permitir que a pessoa interaja com elas.
  • Possui métodos como display()interactWithPost() etc.
  • Recebe um objeto PostStore como dependência no construtor.
  • Usa o objeto PostStore para recuperar as postagens e exibi-las na tela.

Exemplo de código:

PostStore.java:


public class PostStore{

    private List<Post> posts;

    public PostStoreImpl() {
        this.posts = new ArrayList<>();
    }

    public void addPost(Post post) {
        this.posts.add(post);
    }

    public List<Post> getPosts() {
        return this.posts;
    }

    public void deletePost(long id) {
        this.posts.removeIf(post -> post.getId() == id);
    }
}

FeedView.java:

public class FeedView {

    private PostStore postStore;

    public FeedView(PostStore postStore) {
        this.postStore = postStore;
    }

    public void display() {
        for (Post post : postStore.getPosts()) {
            System.out.println(post);
        }
    }

    public void interactWithPost(Post post) {
        if (post.isLiked()) {
            post.setLiked(false);
        } else {
            post.setLiked(true);
        }
    }
}

O → OCP - Open-Closed Principle

Princípio Aberto-Fechado — Objetos ou entidades devem estar abertos para extensão, mas fechados para modificação, ou seja, quando novos comportamentos e recursos precisam ser adicionados no software, devemos estender e não alterar o código-fonte original.

Lembre-se: OCP preza que uma classe deve estar fechada para alteração e aberta para extensão. (extensão pode ser feitar via interface)

Como adicionamos um novo comportamento sem alterar o código-fonte já existente?

O guru Uncle Bob resumiu a solução em uma frase:

💡 Separe o comportamento extensível por trás de uma interface e inverta as dependências.

O que devemos fazer é concentrar nos aspectos essenciais do contexto, abstraindo-os para uma interface. Se as abstrações são bem definidas, logo o software estará aberto para extensão.

Open-Closed Principle também é base para o padrão de projeto Strategy, Abstract Factory... A maior parte dos design patterns tem foco exatamente nesse principio. A principal vantagem é a facilidade na adição de novos requisitos, diminuindo as chances de introduzir novos bugs, pois o novo comportamento fica isolado, e o que estava funcionando provavelmente continuará funcionando.

Violação OCP

Considere uma classe chamada Post que representa uma postagem no Instagram. Essa classe teria os seguintes atributos:

  • id: O identificador da postagem
  • author: O autor da postagem
  • content: O conteúdo da postagem
  • likes: O número de curtidas da postagem

O código da classe Post poderia ser o seguinte:

public class Post {
    private long id;
    private User author;
    private String content;
    private int likes;

    public Post(long id, User author, String content, int likes) {
        this.id = id;
        this.author = author;
        this.content = content;
        this.likes = likes;
    }

    public long getId() {
        return this.id;
    }

    public User getAuthor() {
        return this.author;
    }

    public String getContent() {
        return this.content;
    }

    public int getLikes() {
        return this.likes;
    }

    public void setLikes(int likes) {
        this.likes = likes;
    }
}

Essa classe viola o princípio do aberto/fechado porque precisa ser modificada sempre que um novo tipo de postagem for adicionado. Por exemplo, se quisermos adicionar um novo tipo de postagem chamado "vídeo", precisamos adicionar um novo atributo à classe Post para armazenar o link para o vídeo.

Aqui estão alguns outros exemplos de classes em Java que podem violar o princípio do aberto/fechado e também viola o principio da responsabilidade unica:

  • Uma classe que representa um produto e que é responsável por calcular o preço do produto, calcular o imposto sobre o produto e calcular o frete do produto.
  • Uma classe que representa uma transação bancária e que é responsável por processar a transação, registrar a transação no banco de dados e enviar um e-mail para o cliente.
  • Uma classe que representa um usuário e que é responsável por autenticar o usuário, gerenciar as permissões do usuário e enviar e-mails para o usuário.

Correção

Para corrigir essa violação, podemos usar o princípio da herança. Podemos criar uma classe abstrata chamada Postable que represente uma postagem genérica. A classe Post seria uma classe concreta que herda da classe Postable.


public abstract class Postable<T> {
//postable recebe um generic para poder sofrer alteração do tipo de cada implementação
    public abstract long getId();
    public abstract User getAuthor();
    public abstract T getContent();
    public abstract int getLikes();
    public abstract void setLikes(int likes);
}

Post.java:

public class Post extends Postable<String> {

    private String content;

    public Post(long id, User author, String content, int likes) {
        super(id, author, likes);
        this.content = content;
    }

    @Override
    public String getContent() {
        return this.content;
    }
}

VideoPost.java:

public class VideoPost extends Postable<Video> {

    private Video content;

    public VideoPost(long id, User author, String content, int likes) {
        super(id, author, likes);
        this.content = content;
    }

    @Override
    public String getContent() {
        return this.content;
    }
}

L → LSP— Liskov Substitution Principle

Princípio da substituição de Liskov — Uma classe derivada deve ser substituível por sua classe base. Foi introduzido por Barbara Liskov em sua conferência “Data abstraction” em 1987.

Um exemplo fácil compreensão dessa definição. Seria:

💡 Se S é um subtipo de T, então os objetos do tipo T, em um programa, podem ser substituídos pelos objetos de tipo S sem que seja necessário alterar as propriedades deste programa.

Violação LSP

  • Sobrescrever/implementar um método que não faz nada;
  • Lançar uma exceção inesperada;
  • Retornar valores de tipos diferentes da classe base;

Para não violar o Liskov Substitution Principle, além estruturar muito bem as abstrações, em alguns casos, precisara usar a injeção de dependência e também usar outros princípios do SOLID, como, por exemplo, o Open-Closed Principle e o Interface Segregation Principle.

Seguir o LSP nos permite usar o polimorfismo com mais confiança Untitled

Para não quebrar essa regra tem que seguir alguns requerimentos:

  1. Os tipos de parâmetros em uma classe filha devem ser iguais ou serem abstratos que o tipo de da classe mãe.
  2. O tipo de retorno de uma classe filha deve coincidir ou ser um subtipo do retorno na classe mãe. (uma pré-condição não pode existir em uma classe filha, tem que se respeitar o que foi estendido da classe mãe)
  3. Um método de uma classe filha não deve lançar tipos de exceções que não são esperados que o método lançaria.
  4. Invariantes de uma classe mão deve ser preservadas:
    1. Invariantes são condições nas quais um objeto faz sentido. Ex: não faz sentido um gato não miar, ou não ter cauda. Às vezes essa regra não faz sentido na realidade.

Correção

Considere uma classe chamada Feed que representa o feed de uma pessoa no Instagram. Essa classe seria responsável por armazenar as postagens da pessoa, exibi-las na tela e permitir que a pessoa interaja com elas.

public class Feed {

    private List<Post> posts;

    public Feed() {
        this.posts = new ArrayList<>();
    }

    public void addPost(Post post) {
        this.posts.add(post);
    }

    public List<Post> getPosts() {
        return this.posts;
    }

    public void display() {
        for (Post post : this.posts) {
            System.out.println(post);
        }
    }

    public void interactWithPost(Post post) {
        if (post.isLiked()) {
            post.setLiked(false);
        } else {
            post.setLiked(true);
        }
    }
}

Essa classe viola o princípio da substituição de Liskov porque a classe Video não substitui a classe base Post. Isso ocorre porque a classe Video não implementa o método setLikes() (imagina que video não poderia receber a ação de curti).

public class Video {

    private String link;

    public Video(long id, User author, String content, int likes, String link) {
        super(id, author, content, likes);
        this.link = link;
    }
    
    public String getContent() {
        return this.content;
    }

    public int getLikes() {
        return this.likes;
    }
}

Para corrigir essa violação, podemos modificar a classe Video para extender de Post usando assim a herança da POO e fazer com que a classe video seja um sub classe de Post. O código da classe Video ficaria assim:

public class Video extends Post {

    private String link;

    public Video(long id, User author, String content, int likes, String link) {
        super(id, author, content, likes);
        this.link = link;
    }

    @Override
    public String getContent() {
        return this.content;
    }

    @Override
    public int getLikes() {
        return this.likes;
    }

    @Override
    public void setLikes(int likes) {
        this.likes = likes;
    }
}

No exemplo, a classe Post é uma classe abstrata que representa uma postagem genérica. A classe Video é uma subclasse de Post que representa uma postagem de vídeo.

O método setLikes() é um método importante da classe Post. Ele é usado para aumentar ou diminuir o número de curtidas de uma postagem.

A classe Video não implementa o método setLikes(). Isso significa que uma instância de Video não pode ser usada em um contexto onde o método setLikes() é chamado.

Por exemplo, o método display() da classe Feed usa o método setLikes() para aumentar o número de curtidas de uma postagem. Se uma instância de Video for adicionada ao feed, o método display() não funcionará corretamente.

Para corrigir essa violação, podemos modificar a classe Video para implementar o método setLikes(). Isso garantirá que uma instância de Video possa ser usada em qualquer contexto onde uma instância de Post possa ser usada.

Em termos mais gerais, a violação da lei da substituição de Liskov ocorre quando uma subclasse redefine um método de uma classe base de uma forma que muda o comportamento do método. Isso pode causar problemas porque o código que espera um comportamento específico da classe base pode não funcionar corretamente com a subclasse.

Aqui estão alguns exemplos de como a violação da lei da substituição de Liskov pode causar problemas:

  • O código pode gerar erros ou resultados inesperados.
  • O código pode se tornar mais difícil de manter e depurar.
  • O código pode se tornar menos flexível e reutilizável.

I → ISP — Interface Segregation Principle

Princípio da Segregação da Interface — Uma classe não deve ser forçada a implementar interfaces e métodos que não irão utilizar. Esse princípio basicamente diz que é melhor criar interfaces mais específicas ao invés de termos uma única interface genérica.

Programe para uma interface, não uma implementação. Dependa de abstrações, não classes concretas

Violação ISP:

Considere uma interface chamada Postable que define os seguintes métodos:

interface Posteble{
	getId()
	getAuthor()
	getContent()
	getLikes()
}

Essa interface é muito abrangente, pois define métodos que são aplicáveis a todos os tipos de postagens. Isso pode causar problemas porque:

  • Classes que implementam a interface Postable podem precisar implementar métodos que não são relevantes para elas.

Para corrigir essa violação, podemos dividir a interface Postable em várias interfaces mais específicas:

interface ImagePostable extend Posteble{
	getImageUrl**()**
}
interface VideoPostable extend Posteble{
	getVideoUrl**()**
}

Essas interfaces são mais específicas porque definem apenas os métodos que são aplicáveis aos tipos de postagens específicos que representam. Isso torna o código mais modular e reutilizável.

Aqui está um exemplo de como essa solução pode ser aplicada:

public class Post implements Postable {
    private long id;
    private User author;
    private String content;
    private int likes;

    public Post(long id, User author, String content, int likes) {
        this.id = id;
        this.author = author;
        this.content = content;
        this.likes = likes;
    }

    @Override
    public long getId() {
        return this.id;
    }

    @Override
    public User getAuthor() {
        return this.author;
    }

    @Override
    public String getContent() {
        return this.content;
    }

    @Override
    public int getLikes() {
        return this.likes;
    }
}

public class ImagePost extends Post implements ImagePostable {

    private String imageUrl;

    public ImagePost(long id, User author, String content, int likes, String imageUrl) {
        super(id, author, content, likes);
        this.imageUrl = imageUrl;
    }

    @Override
    public String getImageUrl() {
        return this.imageUrl;
    }
}

public class VideoPost extends Post implements VideoPostable {

    private String videoUrl;

    public VideoPost(long id, User author, String content, int likes, String videoUrl) {
        super(id, author, content, likes);
        this.videoUrl = videoUrl;
    }

    @Override
    public String getVideoUrl() {
        return this.videoUrl;
    }
}

Nesse exemplo, a classe Post implementa a interface Postable. A classe ImagePost implementa as interfaces Postable e ImagePostable. A classe VideoPost implementa as interfaces Postable e VideoPostable.

Assim, cada classe implementa apenas os métodos que são relevantes para ela. Isso torna o código mais modular e reutilizável.

Por exemplo, a classe Feed pode usar a interface Postable para exibir postagens de qualquer tipo. Isso porque todas as classes que implementam a interface Postable têm os métodos getId(), getAuthor(), getContent() e getLikes().

No entanto, a classe Feed pode usar a interface ImagePostable para exibir apenas postagens de imagens. Isso porque apenas as classes que implementam a interface ImagePostable têm o método getImageUrl().

O mesmo vale para a classe VideoPostable.

D → DIP — Dependency Inversion Principle

De acordo com Uncle Bob, esse princípio pode ser definido da seguinte forma:

1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.

2. Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

💡 Importante: Inversão de Dependência não é igual a Injeção de Dependência, fique ciente disso! A Inversão de Dependência é um princípio (Conceito) e a Injeção de Dependência é um padrão de projeto (Design Pattern).

  • Classes de baixo nível - implementam operações básicas.
  • Classes de alto nível - contém regras de negócio complexa que direcionam para classes de baixo nível.

Conforme a definição do DIPum módulo de alto nível não deve depender de módulos de baixo nível, ambos devem depender da abstração. Então, a primeira coisa que precisamos fazer é identificar no nosso código qual é o módulo de alto nível e qual é o módulo de baixo nível. Módulo de alto nível é um módulo que depende de outros módulos.

Violação DIP

Considere uma classe chamada PostService que é responsável por gerenciar postagens no Instagram. Essa classe seria responsável por criar, ler, atualizar e excluir postagens.

O código da classe PostService poderia ser o seguinte:

public class PostService {

    private PostRepository postRepository;

    public PostService() {
        this.postRepository = new PostRepository();
    }

    public void createPost(Post post) {
        this.postRepository.addPost(post);
    }

    public Post readPost(long id) {
        return this.postRepository.getPost(id);
    }

    public void updatePost(Post post) {
        this.postRepository.updatePost(post);
    }

    public void deletePost(long id) {
        this.postRepository.deletePost(id);
    }
}

Essa classe viola o princípio da inversão de dependência porque a classe PostService depende diretamente da classe PostRepository. Isso significa que a classe PostService deve ser alterada sempre que a classe PostRepository for alterada.

Para corrigir essa violação, podemos usar o padrão de projeto Injeção de dependência. Podemos criar uma interface chamada PostRepository e fazer com que a classe PostService dependa dessa interface.

O código das classes PostRepository e PostService ficaria assim:

public interface PostRepository {

    public void addPost(Post post);

    public Post getPost(long id);

    public void updatePost(Post post);

    public void deletePost(long id);
}

public class PostService {

    private PostRepository postRepository;

    public PostService(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    public void createPost(Post post) {
        this.postRepository.addPost(post);
    }

    public Post readPost(long id) {
        return this.postRepository.getPost(id);
    }

    public void updatePost(Post post) {
        this.postRepository.updatePost(post);
    }

    public void deletePost(long id) {
        this.postRepository.deletePost(id);
    }
}

Nesse exemplo, a classe PostService não depende diretamente da classe PostRepository. Em vez disso, a classe PostService depende da interface PostRepository. Isso significa que a classe PostService pode ser usada com qualquer implementação da interface PostRepository.

Por exemplo, podemos criar uma classe concreta chamada PostRepositoryImpl que implementa a interface PostRepository. A classe PostRepositoryImpl pode usar um banco de dados ou um arquivo para armazenar as postagens.

O código da classe PostRepositoryImpl ficaria assim:

public class PostRepositoryImpl implements PostRepository {

    private List<Post> posts;

    public PostRepositoryImpl() {
        this.posts = new ArrayList<>();
    }

    @Override
    public void addPost(Post post) {
        this.posts.add(post);
    }

    @Override
    public Post getPost(long id) {
        return this.posts.stream()
					.filter(post -> post.getId() == id)
					.findFirst().orElse(null);
	  }

    @Override
    public void updatePost(Post post) {
        this.posts.stream()
				.filter(post -> post.getId() == id)
				.findFirst()
				.ifPresent(p -> {
            p.setContent(post.getContent());
            p.setAuthor(post.getAuthor());
            p.setLikes(post.getLikes());
	       });
    }

    @Override
    public void deletePost(long id) {
        this.posts.removeIf(post -> post.getId() == id);
    }
}

Agora, a classe PostService pode ser usada com qualquer implementação da interface PostRepository. Por exemplo, podemos usar a seguinte linha de código para criar um novo objeto da classe PostService:

PostService postService = new PostService(new PostRepositoryImpl)

Isso torna o código mais flexível e reutilizável.

Renara Secchim