[S]OLID – Single Responsibility Principle (SRP)

SOLID - Single Responsibility Principle - SRP

Boa noite, leitores! Como estão?
Hoje iniciaremos uma nova série de apenas 5 artigos abordando os princípios SOLID. Pretendo enfatizar o objetivo de cada um deste princípios devido à sua extrema importância na arquitetura de um software.
O primeiro dos princípios é a letra “S”, que corresponde ao Single Responsibility Principle. Vamos conhecê-lo?

 

Os princípios SOLID foram introduzidos por um especialista em Engenharia de Software chamada Robert C. Martin, ou “Uncle Bob”, bastante conhecido pela autoria dos livros Clean Code“, “Clean Coder” e “Clean Archicteture. Estes princípios estão intimamente associados à programação Orientada a Objetos e apresentam uma série de técnicas e mecanismos para construir uma arquitetura de classes mais flexível e sustentável.
O termo SOLID, embora seja uma palavra em inglês com tradução de “sólido”, neste contexto é um acrônimo. Cada letra corresponde a um dos princípios, exigindo que todo termo seja escrito em maiúsculas.

Neste primeiro artigo, estudaremos o primeiro deles, chamado Single Responsibility Principle, ou simplesmente SRP.
Pela tradução – Princípio da Responsabilidade Única – já podemos ter uma noção do que se trata: o SRP declara que cada classe no projeto deve possuir apenas uma única responsabilidade, e nada mais do que isso.

 

Como assim?
Para iniciar a explicação, imagine “responsabilidade” como uma “habilidade” de uma classe. Dito isso, o fato de uma classe ter mais de uma habilidade indica que o SRP não foi cumprido. Por exemplo, considere uma classe que tenha métodos para calcular estatísticas de venda e gerar um relatório gerencial. Nota-se que ela possui duas habilidades, certo? Uma delas é o cálculo das estatísticas e a outra é a geração do relatório. Neste caso, dizemos que a classe quebra o princípio de responsabilidade única.

Uma forma simples de verificar se uma classe possui mais de uma responsabilidade é atentar-se à presença da conjunção “e” ao descrever o seu papel no projeto. Portanto, se a classe calcula estatísticas e produz um relatório, significa que existem duas responsabilidades.
Outro fator que revela várias responsabilidades em uma classe é a quantidade de linhas. Classes grandes, com vários métodos, normalmente tendem a executar mais de uma tarefa, ao menos que a regra de negócio realmente seja complexa e justifique o tamanho.

A definição teórica do SRP, na realidade, traz a seguinte frase:

“A class should have one, and only one, reason to change”
(Uma classe deve ter um, e somente um, motivo para mudar)

O “motivo para mudar”, citado na frase, é o que conduz a ideia do SRP. A quantidade de motivos para mudar equivale ao número de responsabilidades que uma classe carrega. Logo, um único motivo para mudar é o que devemos almejar ao desenhar a nossa arquitetura.
Na classe de exemplo deste artigo, que calcula estatísticas e gera o relatório, observe o impacto circular:

  • Se uma modificação for realizada no cálculo de estatísticas, o relatório provavelmente será alterado para refletir essa alteração;
  • Se uma modificação for realizada no relatório, o cálculo de estatísticas provavelmente será alterado para calcular os dados requeridos que serão exibidos.

Verifica-se, então, dois motivos para mudar, quebrando o princípio de responsabilidade única.

 

E a solução?
Para satisfazer o SRP, basta extrair cada responsabilidade para uma classe separada. Dessa forma, as classes terão um objetivo único, exclusivo e apenas um motivo para mudar. Utilizando o exemplo anterior, a classe seria fragmentada em duas: uma exclusiva para o cálculo de estatísticas e outra exclusiva para a geração do relatório. Com essa ação, pode-se constatar vários benefícios:

  • Facilidade na manutenção;
  • Arquitetura com responsabilidades bem definidas;
  • Redução de impacto na arquitetura ao alterar o código;
  • Possibilidade de reaproveitamento de código, já que cada classe possui apenas uma função.

 

Para fixar ainda mais o conceito do SRP, partiremos para um exemplo prático.
Imagine que uma classe foi modelada para criar algumas informações de log e enviar para um endereço de e-mail. Opa, pela conjunção “e”, já sabemos que essa classe provavelmente tem mais de uma responsabilidade!

type
  TLogToMail = class
  private
    FsLog: string;
    procedure WriteLog;
    procedure SendMail;
  public
    procedure SendLog;
  end;
 
implementation
 
{ TLogToMail }
 
procedure TLogToMail.SendLog;
begin
  WriteLog;
  SendMail;
end;
 
procedure TLogToMail.WriteLog;
var
  Log: TStringList;
begin
  Log := TStringList.Create;
  try
    Log.Add('Data/Hora: ' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now));
    Log.Add('Versão do Windows: ' + TOSVersion.Name);
    Log.Add('Última mensagem do Sistema: ' + SysErrorMessage(GetLastError));
 
    FsLog := Log.Text;
  finally
    Log.Free;
  end;
end;
 
procedure TLogToMail.SendMail;
var
  IdSMTP: TIdSMTP;
  IdMessage: TIdMessage;
  IdText: TIdText;
begin
  IdSMTP := TIdSMTP.Create(nil);
  IdMessage := TIdMessage.Create(nil);
  try
    { as configurações dos componentes foram omitidas }
 
    IdText := TIdText.Create(IdMessage.MessageParts);
    // usa o texto de log criado no método anterior
    IdText.Body.Add(FsLog);
 
    IdSMTP.Connect;
    IdSMTP.Authenticate;
    IdSMTP.Send(IdMessage);
  finally
    IdSMTP.Disconnect;
    IdMessage.Free;
    IdSMTP.Free;
  end;
end;

 

A instância da classe é consumida dessa forma:

var
  LogToMail: TLogToMail;
begin
  LogToMail := TLogToMail.Create;
  try
    LogToMail.SendLog;
  finally
    LogToMail.Free;
  end;
end;

 

Eis que, após um tempo, foi solicitada uma alteração no comportamento dessa classe. Ao invés de escrever o log temporariamente, devemos salvá-lo em disco para manter um histórico. Bom, já que estamos trabalhando com TStringList, basta alterar o método WriteLog e substituir uma das linhas por SaveToFile, certo?

procedure TLogToMail.WriteLog;
var
  Log: TStringList;
begin
  Log := TStringList.Create;
  try
    Log.Add('Data/Hora: ' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now));
    Log.Add('Versão do Windows: ' + TOSVersion.Name);
    Log.Add('Última mensagem do Sistema: ' + SysErrorMessage(GetLastError));
 
    // Alteração
    Log.SaveToFile(GetCurrentDir + '\Log.txt');
  finally
    Log.Free;
  end;
end;

 

A rotina irá funcionar?
Hmm… acho que não! O método de envio de e-mail usa a variável FsLog para compor o corpo da mensagem, porém, essa variável não é mais utilizada. Ao executar a rotina, o e-mail será enviado com a mensagem vazia.
Para ajustar esse comportamento, devemos alterar também o método SendMail para anexar o arquivo salvo:

procedure TLogToMail.SendMail;
var
  IdSMTP: TIdSMTP;
  IdMessage: TIdMessage;
begin
  IdSMTP := TIdSMTP.Create(nil);
  IdMessage := TIdMessage.Create(nil);
  try
    { as configurações de conexão e autenticação foram omitidas }
 
    // Alteração
    TIdAttachmentFile.Create(IdMessage.MessageParts, GetCurrentDir + '\Log.txt');
 
    IdSMTP.Connect;
    IdSMTP.Authenticate;
    IdSMTP.Send(IdMessage);
  finally
    IdSMTP.Disconnect;
    IdMessage.Free;
    IdSMTP.Free;
  end;
end;

Resultado: duas alterações, que revelam dois motivos para mudar, ou seja, duas responsabilidades.

Para que essa classe atenda o princípio de responsabilidade única, é preciso extrair cada responsabilidade – escrita de log e envio de e-mail – para classes separadas.

  • TLogger
TLogger:
type
  TLogger = class
  public
    function WriteLog: string;
  end;
 
implementation
 
{ TLogger }
 
function TLogger.WriteLog: string;
var
  Log: TStringList;
  Arquivo: string;
begin
  Log := TStringList.Create;
  try
    Log.Add('Data/Hora: ' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now));
    Log.Add('Versão do Windows: ' + TOSVersion.Name);
    Log.Add('Última mensagem do Sistema: ' + SysErrorMessage(GetLastError));
 
    Arquivo := GetCurrentDir + '\Log.txt';
    Log.SaveToFile(Arquivo);
    result := Arquivo;
  finally
    Log.Free;
  end;
end;

 

  • TMailService
type
  TMailService = class
  public
    procedure SendMail(const Attachment: string);
  end;
 
implementation
 
{ TMailService }
 
procedure TMailService.SendMail(const Attachment: string);
var
  IdSMTP: TIdSMTP;
  IdMessage: TIdMessage;
begin
  IdSMTP := TIdSMTP.Create(nil);
  IdMessage := TIdMessage.Create(nil);
  try
    { as configurações de conexão e autenticação foram omitidas }
 
    // Alteração
    TIdAttachmentFile.Create(IdMessage.MessageParts, Attachment);
 
    IdSMTP.Connect;
    IdSMTP.Authenticate;
    IdSMTP.Send(IdMessage);
  finally
    IdSMTP.Disconnect;
    IdMessage.Free;
    IdSMTP.Free;
  end;
end;

 

Por fim, para enviar o log por e-mail, deve-se utilizar instâncias das duas classes em conjunto:

var
  Logger: TLogger;
  MailService: TMailService;
  Arquivo: string;
begin
  Logger := TLogger.Create;
  MailService := TMailService.Create;
  try
    Arquivo := Logger.WriteLog;
    MailService.SendMail(Arquivo);
  finally
    MailService.Free;
    Logger.Free;
  end;
end;

 

Pronto, pessoal! Efetuamos os ajustes necessários para atender o Single Responsibility Principle. A classe foi dividida em duas, cada qual recebendo uma única responsabilidade. Observem que as classes ficaram pequenas e fáceis de compreender. Além disso, a classe TMailService tornou-se “genérica” e poderá ser utilizada por outras rotinas, já que não faz mais referência ao log. 🙂

É importante esclarecer que SRP não é aplicado somente para classes. Este mesmo princípio também se destina a métodos, seguindo basicamente o mesmo conceito – se o método realiza duas ou mais funções, cada uma delas deve ser extraída para um método exclusivo. Veja um exemplo bastante simples:

public
  procedure OpenFileInMemo;
 
...
 
procedure TClasse.OpenFileInMemo;
var
  OpenDialog: TOpenDialog;
begin
  OpenDialog := TOpenDialog.Create(nil);
  try
    OpenDialog.Execute;
 
    if OpenDialog.FileName = EmptyStr then
      Exit;
 
    Memo1.Lines.LoadFromFile(OpenDialog.FileName);
  finally
    OpenDialog.Free;
  end;
end;

 

O método exibe um diálogo para selecionar o arquivo e o carrega em um componente TMemo. Duas responsabilidades. Ao aplicar o SRP, o método é fracionado para separá-las:

public
  function OpenFile: string;
  procedure LoadToMemo(const FileName: string);
 
...
 
function TClasse.OpenFile: string;
var
  OpenDialog: TOpenDialog;
begin
  OpenDialog := TOpenDialog.Create(nil);
  try
    OpenDialog.Execute;
    result := OpenDialog.FileName;
  finally
    OpenDialog.Free;
  end;
end;
 
procedure TClasse.LoadToMemo(const FileName: string);
begin
  if FileName.Trim = EmptyStr then
    Exit;
 
  Memo1.Lines.LoadFromFile(FileName);
end;

Embora o código tenha ficado um pouco maior, garanto que as manutenções serão bem menos custosas.
Uma das maiores vantagens é que estes novos métodos podem ser reaproveitados em outros locais do código, assim como acontece com as classes.

 

Por hoje é só, leitores!
Continuem acompanhando o blog! Um grande abraço!


 

14 comentários

    1. Opa, obrigado, Daniel!
      Espero que a série fique boa mesmo! 😉
      Abraço!

  1. Show!! Essa série vai ser boa!
    Além da técnica que vc mostrou pra identificar classes que estão violando o principio do SRP, eu uso essas aqui:
    – Classes que são modificadas com frequência;
    – Classes que não param nunca de crescer;
    Caso alguma seja afirmativa, considero que a classe está violando o SRP.

    Aguardando o artigo sobre o OCP!

    Abraços!

    1. Olá, Giquieu!
      Ótimas técnicas! Realmente são indicadores fortes de violação do SRP, principalmente o fato de crescerem constantemente.
      Continue acompanhando! Grande abraço!

    1. Obrigado, Cleiton!
      Em breve publico o próximo princípio!
      Abraço!

    1. Obrigado pelo feedback, Carlos!
      Agradeço por acompanhar o blog!
      Abração!

  2. Excelente artigo André, gostei bastante.
    Os exemplos práticos que você coloca, ajuda muito no entendimento do problema. Parabéns pelo empenho e dedicação que tens para manter o blog sempre com conteúdos interessantes.
    Abraço.

    1. Muito obrigado, Lucas! Obrigado mesmo!
      Comentários como o seu me motivam a continuar este trabalho e fazê-lo cada vez melhor.
      Continue acompanhando. Abraço!

  3. Olá André,
    Parabéns!
    Uma dúvida, se fosse uma classe CRUD, a responsabilidade da classe é fazer a manutenção de um cadastro, se pensar por esse lado tem uma unica responsabilidade, mas se analisar por métodos (Consultar, Incluir, Alterar e Excluir), temos 4 métodos com responsabilidades distintas, nesse caso é considerado que seguiu o padrão SRP ou precisaria mudar algo?

    1. Olá, Paulo, boa tarde! Desculpe-me pela demora.
      Ótima pergunta! Bom, no meu ponto de vista, a responsabilidade da classe é a manutenção do cadastro, logo, isso inclui todo o CRUD. Podemos encontrar essa situação nas camadas de persistência (DAO) em alguns padrões de arquitetura. Por exemplo, uma classe chamada TClienteDAO seria responsável pela manutenção do cadastro de clientes.
      Em resumo, acredito que a definição de responsabilidades deve ser observada a nível de contexto (classe) ao invés de ser a nível de métodos. No caso do CRUD, eu particularmente escreveria tudo na mesma classe.

      Abraço!

  4. Obrigado pelo retorno André!
    Tenho me aprofundado no assunto e inclusive estou tentando montar um projeto com um “estrutura robusta”, DDD, Solid, TDD e CQRS, encontro muito assunto dessa estrutura em outras linguagens, fico feliz de ter iniciado, gostaria muito se incluísse esses padrões em futuros posts ou até mesmo em um curso pago, por mais que programe em Delphi a 20 anos sempre estou me aperfeiçoando, só gostaria que o conhecimento na comunidade Delphi crescesse em um ritmo próximo das outras linguagens.
    Aproveitando Feliz Natal e um ano novo abençoado por Deus!
    Abraços

    1. Legal, Paulo!
      Você está de parabéns com essa iniciativa de conhecer e aplicar estes conceitos da Engenharia de Software! Na minha opinião, todo programador deveria ter conhecimento dessas técnicas de arquitetura.
      Ainda não escrevi artigos sobre DDD e TDD, mas os 5 artigos sobre SOLID já estão disponíveis!
      Continue firme nos estudos! Isso será muito importante para a sua carreira como programador.
      Grande abraço e feliz ano novo!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *