Princípios SOLID: Single Responsability Principle

O artigo Introdução aos princípios SOLID aqui no blog da TreinaWeb trouxe uma breve introdução aos princípios SOLID. Nesta breve introdução, o foco era na definição do que são os princípios SOLID, abordando quais são os cinco princípios envolvidos e as vantagens que eles podem trazer quando adotados corretamente em um projeto no que diz respeito à qualidade de manutenibilidade do código.

A partir deste artigo, vamos analisar cada um dos princípios SOLID de maneira mais detalhada. Este artigo irá focar no SRP: o Single Responsability Principle.

Uma breve revisão: o que são os princípios SOLID?

Os princípios SOLID são cinco princípios de design de código orientado a objeto que basicamente tem os seguintes objetivos:

  • Tornar o código mais entendível, claro e conciso;
  • Tornar o código mais flexível e tolerante a mudanças;
  • Aumentar a adesão do código aos princípios da orientação a objetos.

SOLID é um acrônimo para cada um dos cinco princípios que fazem parte desse grupo:

  • Single Responsability Principle (Princípio da Responsabilidade Única);
  • Open/Closed Principle (Princípio do “Aberto para Extensão/Fechado para Implementação);
  • Liskov Substitution Principle (Princípio da Substituição de Liskov);
  • Interface Segregation Principle (Princípio da Segregação de Interfaces);
  • Dependency Inversion Principle (Princípio da Inversão de Dependências)

O que é o Single Responsability Principle (ou SRP)?

O SRP é um princípio definido por Robert C. Martin da seguinte maneira:

A class should have only one reason to change.

Ou seja:

Uma classe deveria ter uma única razão para ser alterada.

Quando criamos uma classe e/ou um método, esta estrutura precisa ter uma única responsabilidade, muito clara e bem definida. Isso centraliza as responsabilidades e pontos de mudança em lugares centrais e fáceis de serem identificados, tornando a manutenção do código muito mais simples. Lembre-se sempre do seguinte pensamento: o que meu trecho de código é passível de sofrer alterações drásticas no decorrer do tempo? Se sim, ele é um “eixo de mudança” e, portanto, deveria estar em uma estrutura à parte.

Vamos a um exemplo: vamos considerar um trecho de código Java em uma classe que é responsável por extrair registros a partir de uma tabela de clientes e exibi-los para o usuário. Poderíamos escrever assim:

// Imports omitidos somente para clareza
public class Cliente {

    private int id;
    private String nome;
    // Gets e sets omitidos por clareza

}

public class ListaClientes {

    public void listarClientes() throws Exception {
        List<Cliente> clientes = new ArrayList<Cliente>();
        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/srp", "root", "root")) {
            Statement st = con.createStatement();
            ResultSet rs = st.executeQuery("SELECT * FROM clientes");
            while (rs.next()){
                Cliente cliente = new Cliente();
                cliente.setId(rs.getInt("id"));
                cliente.setNome(rs.getInt("nome"));
                clientes.add(cliente)
            }
            rs.close();
            st.close();
        }
        clientes.forEach(cliente -> {
            System.out.println(cliente.toString()); // Considerando que "toString()" está devidamente sobrescrito
        });
    }

}

Veja a classe ListaClientes… Ela está extremamente sobrecarregada, pois ela é responsável por:

  • Lidar com os aspectos de banco de dados (conexão, statement, resultset e resolução de impedância ao extrair os objetos Cliente do resultset);
  • Fazer a exibição dos clientes na interface.

A nossa classe ListaClientes passa a ser chamada de God class, ou classe Deus, pois ela está fazendo tudo… Ela está concentrando todos os pontos de mudança em um único local:

  • Se a conexão do banco de dados precisar ser alterada, essa classe precisará ser alterada;
  • Se a consulta para obtenção dos clientes precisar mudar, essa classe precisará ser alterada;
  • Se você precisar implementar um pool de conexões JDBC, essa classe precisará ser alterada;
  • Se os nomes das colunas da tabela de clientes forem alterados, essa classe precisará ser alterada;
  • Se precisarmos alterar a maneira como os clientes são exibidos, essa classe precisará ser alterada…

Ou seja: se nossa classe concentra muitos pontos de mudança, ela também está acumulando muitas responsabilidades.

Os impactos disso são terríveis:

  • A classe ListaClientes tem um grau de legibilidade baixíssimo;
  • A classe ListaClientes é complexa para ser testada adequadamente, principalmente se estivermos utilizando testes automatizados. Por exemplo: você pode ter um caso de teste que testa a exibição dos clientes na interface. Porém, no código que criamos, este caso de teste pode simplesmente falhar porque houve um erro na hora de abrir a conexão com o banco de dados. Nese caso, teríamos um falso negativo: o teste falha não porque os clientes não são exibidos, mas sim porque não foi possível abrir a conexão com o banco de dados. Isso acontece porque nosso método e nossa classe possuem um alto acoplamento, ou seja: suas funções estão todas “agarradas” umas nas outras;
  • O reaproveitamento de código fica impossibilitado. Se precisarmos da lista de clientes do banco de dados em outros pontos de nossa aplicação, precisaremos duplicar o código de listagem do método listarClientes();

Desse modo, reparamos que a classe acima claramente viola o Single Responsability Principle.

Aplicando o Single Responsability Principle

Se quisermos melhorar o nosso código de listagem de clientes, precisamos pensar em separar as responsabilidades, ou seja: os pontos de mudança.

Desenvolvedor ASP.NET Full-Stack
Formação: Desenvolvedor ASP.NET Full-Stack
A formação Desenvolvedor ASP.NET Full Stack da TreinaWeb tem como objetivo abordar as duas principais plataformas dentro do ASP.NET: o ASP.NET MVC, para criação de aplicações web seguindo o padrão MVC/MVW; e o ASP.NET WebAPI, para criação de APIs RESTful que sigam os padrões mais atuais da indústria.
CONHEÇA A FORMAÇÃO

Um possível ponto de mudança é a conexão com o banco de dados… O IP do servidor ou as credenciais de conexão podem ser alteradas em algum momento. Por isso, vamos isolar esta responsabilidade em uma classe especializada em criação de conexões.

public class FabricaConexao {

    public static Connection criarConexao() throws SQLException {
        Connection conexao = DriverManager.getConnection("jdbc:mysql://localhost:3306/srp", "root", "root");
        return conexao;
    }

}

Esta classe respeita o SRP: ela possui uma única responsabilidade, concentrando um único ponto de mudança: a conexão com o banco de dados.

Outra responsabilidade clara é a leitura dos clientes a partir da tabela correspondente. Podemos também isolar este processo em uma classe separada. Nesse caso, é comum vermos a adoção de um design pattern chamado DAO – Data Access Object. Trata-se de uma classe para justamente lidar com as fontes de dados de objetos na aplicação. No nosso caso, temos uma tabela do MySQL como fonte de dados para obtenção de objetos Cliente. Essa classe poderia ser implementada da seguinte maneira:

public class ClienteDAO {

    public ArrayList<Cliente> lerClientes() throws Exception {
        List<Cliente> clientes = new ArrayList<Cliente>();
        try (Connection conexao = FabricaConexao.criarConexao()) {
            Statement comando = conexao.createStatement();
            ResultSet rs = comando.executeQuery("SELECT * FROM clientes");
            while (rs.next()) {
                contatos.add(this.extrairClienteDoResultSet(rs));
            }
        }
        return clientes;
    }

    private Cliente extrairClienteDoResultSet(ResultSet rs) {
        Contato contato = new Contato();
        contato.setId(rs.getInt("id"));
        contato.setNome(rs.getString("nome"));
        return contato;
    }

}

Veja que nossa classe ClienteDAO também possui uma única responsabilidade: ler clientes a partir de uma tabela clientes no banco de dados, utilizando a conexão criada pela classe FabricaConexao criada anteriormente. Veja que até separamos a responsabilidade de análise do ResultSet para extração do cliente em um método à parte, método este que pode ser reutilizado por qualquer outro método do ClienteDAO sem prejuízos. Caso alguma coluna da tabela de clientes tenha seu nome alterado, bastará a nós alterar o método extrairClienteDoResultSet(): automaticamente, todos os métodos de ClienteDAO já saberão a nova forma de extrair os clientes do banco de dados.

Agora, temos mais uma responsabilidade: a interface, para exibição dos clientes:

public class UIExibicaoClientes {

    public void listarClientes() {
          try {
            ClienteDAO clienteDAO = new ClienteDAO();
            ArrayList<Cliente> clientes = clienteDAO.lerClientes();
            clientes.forEach(cliente -> {
                System.out.println(cliente.toString());
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Veja que, agora, nosso código é muito mais claro, conciso e coeso. As responsabilidades também estão muito bem separadas: se tivermos que lidar com conexões, mexemos só na classe FabricaConexao; se precisarmos alterar a maneira como os clientes são lidos do banco de dados, mexemos só em ClienteDAO; se precisarmos alterar a maneira como os clientes são exibidos, mexemos só em UIExibicaoClientes. Veja também que o grau de reusabilidade do nosso código aumentou drasticamente: as classes FabricaConexao e ClienteDAO também podem ser utilizadas por qualquer outro ponto em nosso projeto que precise da lista de clientes do banco de dados ou precise lidar diretamente com conexões ao banco de dados. Por fim, nosso novo código é bem mais fácil de ser testado, já que agora consigo testar cada responsabilidade de maneira isolada uma das outras. Nossas classes agora estão mais próximas do Princípio da Responsabilidade Única.

Resumindo…

Podemos resumir o SRP nos seguintes tópicos:

  • SRP é um dos princípios SOLID, tendo sido definido por Robert C. Martin;
  • O SRP visa separar os pontos de alteração de nossas estruturas de código: uma classe deveria ter uma única razão para ser alterada;
  • Cada unidade de código acaba assumindo uma única e clara responsabilidade quando aplicamos o SRP;
  • O SRP traz muitos benefícios, como facilidade na manutenção do código, melhoria da legibilidade do código e facilidade para a execução de testes (principalmente testes automatizados).
Deixe seu comentário

Líder de Conteúdo/Inovação. Pós-graduado em Projeto e Desenvolvimento de Aplicações Web. Certificado Microsoft MCSD (Web).