[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!


 

10 comentários

  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!

  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!

Deixe uma resposta

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

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.