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?
Introdução
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.
Contexto
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.
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.
Exemplo prático
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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
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:
1 2 3 4 5 6 7 8 9 10 |
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?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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.
Aplicando o SRP
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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. 🙂
Conclusão
É 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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!
Boa noite, André,
Parabéns… essa série vai ser TOP hein… ótimo artigo.
Obrigadão
Grande abraço.
Opa, obrigado, Daniel!
Espero que a série fique boa mesmo! 😉
Abraço!
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!
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!
Show, aguardando o próximo!
Parabéns!!!
Obrigado, Cleiton!
Em breve publico o próximo princÃpio!
Abraço!
Fala, André! Mais um excelente artigo.
Continuamos prestigiando seu trabalho.
Obrigado.
Obrigado pelo feedback, Carlos!
Agradeço por acompanhar o blog!
Abração!
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.
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!
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?
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!
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
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!
Sinceramente esse rapaz chamado André Celestino ! me ajudou muito a resolver uma problema numa aplicação em delphi
7 que conseguir concluir graças ao SR Deus em 1 lugar e depois a ele. pois prometi algo a ele que no momento ainda nao pode cumprir. e peco aos demais do blog se poderem ajudar ele a bancar o blog dele cada um cooperando com 10 ou 20 pelo menos e uma opiniao minha porque com o conhecimento que ele tem e ainda a ajudar agente na pragramacao nao qualquer um que faz isso que ele faz nao.
Obrigado pelo comentário, Carlos.
Fico feliz em poder ter lhe ajudado!
Abração!
Muito bom André.
Quando tiver tempo e se for da sua vontade, poderia escrever artigos sobre criptografia no Delphi.
que seja em arquivos ou em um login feito de uma aplicação.
Abraço.
Muito bom André.
Quando tiver tempo e se for da sua vontade, poderia escrever artigos sobre criptografia no Delphi.
que seja em arquivos ou em um login feito de uma aplicação.
Abraço.
Olá, Willian, boa noite!
Rapaz, obrigado pela sugestão de tema! 🙂
Já faz um tempo que estou tentando retomar os trabalhos aqui no blog, mas a falta de tempo ainda está sendo a minha maior inimiga.
Mesmo assim, pretendo retomar ainda esse ano, e o tema sobre criptografia certamente estará entre os artigos!
Abraço.