Independentemente do que consideramos um ótimo código, ele sempre requer uma qualidade simples: o código deve ser sustentável. Indentação adequada, nomes de variáveis organizados, 100% de cobertura de teste e assim por diante só podem levar você até certo ponto. Qualquer código que não seja passível de manutenção e não possa se adaptar aos requisitos em mudança com relativa facilidade é um código apenas esperando para se tornar obsoleto. Podemos não precisar escrever um bom código quando estamos tentando construir um protótipo, uma prova de conceito ou um produto mínimo viável, mas em todos os outros casos, devemos sempre escrever um código que seja sustentável. Isso é algo que deve ser considerado uma qualidade fundamental da engenharia e design de software.
Neste artigo, discutirei como o Princípio de Responsabilidade Única e algumas técnicas que giram em torno dele podem dar a seu código essa mesma qualidade. Escrever bons códigos é uma arte, mas alguns princípios sempre podem ajudar a dar ao seu trabalho de desenvolvimento a direção necessária para produzir software robusto e sustentável.
Quase todos os livros sobre alguma nova estrutura MVC (MVP, MVVM ou outro M **) estão repletos de exemplos de códigos ruins. Esses exemplos tentam mostrar o que a estrutura tem a oferecer. Mas também acabam fornecendo conselhos ruins para iniciantes. Exemplos como 'digamos que temos este ORM X para nossos modelos, o mecanismo de modelo Y para nossas visualizações e teremos controladores para gerenciar tudo' não alcançam nada além de controladores gigantescos.
Embora em defesa desses livros, os exemplos pretendem demonstrar a facilidade com que você pode começar a usar sua estrutura. Eles não se destinam a ensinar design de software. Mas os leitores que seguem esses exemplos percebem, somente depois de anos, como é contraproducente ter pedaços de código monolíticos em seus projetos.
Os modelos são o coração do seu aplicativo. Se você tiver modelos separados do resto da lógica do aplicativo, a manutenção será muito mais fácil, independentemente de quão complicado seu aplicativo se torne. Mesmo para aplicativos complicados, uma boa implementação de modelo pode resultar em um código extremamente expressivo. E para conseguir isso, comece certificando-se de que seus modelos façam apenas o que devem fazer, e não se preocupem com o que o aplicativo desenvolvido em torno deles faz. Além disso, ele não se preocupa com o que é a camada de armazenamento de dados subjacente: seu aplicativo depende de um banco de dados SQL ou armazena tudo em arquivos de texto?
À medida que continuamos este artigo, você perceberá como o código é excelente em relação à separação de interesses.
Você provavelmente já ouviu falar sobre os princípios SOLID: responsabilidade única, aberto-fechado, substituição de liskov, segregação de interface e inversão de dependência. A primeira letra, S, representa o Princípio da Responsabilidade Única (SRP) e sua importância não pode ser exagerada. Eu até argumentaria que é uma condição necessária e suficiente para um bom código. Na verdade, em qualquer código que está mal escrito, você sempre pode encontrar uma classe que tem mais de uma responsabilidade - form1.cs ou index.php contendo alguns milhares de linhas de código não é algo tão raro de acontecer e todos nós provavelmente já viu ou fez isso.
Vamos dar uma olhada em um exemplo em C # (ASP.NET MVC e framework Entity). Mesmo se você não for um Desenvolvedor C # , com alguma experiência OOP, você será capaz de acompanhar facilmente.
public class OrderController { ... public ActionResult CreateForm() { /* * View data preparations */ return View(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } using (var context = new DataContext()) { var order = new Order(); // Create order from request context.Orders.Add(order); // Reserve ordered goods …(Huge logic here)... context.SaveChanges(); //Send email with order details for customer } return RedirectToAction('Index'); } ... (many more methods like Create here) }
Esta é uma classe OrderController comum, seu método Create é mostrado. Em controladores como este, frequentemente vejo casos em que a própria classe Order é usada como um parâmetro de solicitação. Mas eu prefiro usar classes de pedidos especiais. Novamente, SRP!
Observe no trecho de código acima como o controlador sabe muito sobre “fazer um pedido”, incluindo, mas não se limitando a, armazenar o objeto Pedido, enviar e-mails, etc. Isso é simplesmente muitos trabalhos para uma única classe. Para cada pequena mudança, o desenvolvedor precisa mudar todo o código do controlador. E apenas no caso de outro controlador também precisar criar pedidos, na maioria das vezes, os desenvolvedores recorrerão a copiar e colar o código. Os controladores devem controlar apenas o processo geral e não realmente abrigar toda a lógica do processo.
Mas hoje é o dia em que pararemos de escrever esses controladores gigantescos!
Vamos primeiro extrair toda a lógica de negócios do controlador e movê-la para uma classe OrderService:
public class OrderService { public void Create(OrderCreateRequest request) { // all actions for order creating here } } public class OrderController { public OrderController() { this.service = new OrderService(); } [HttpPost] public ActionResult Create(OrderCreateRequest request) { if (!ModelState.IsValid) { /* * View data preparations */ return View(); } this.service.Create(request); return RedirectToAction('Index'); }
Feito isso, o controlador agora faz apenas o que se pretende fazer: controlar o processo. Ele conhece apenas visualizações, classes OrderService e OrderRequest - o menor conjunto de informações necessário para fazer seu trabalho, que é gerenciar solicitações e enviar respostas.
o que é risco cambial
Dessa forma, você raramente mudará o código do controlador. Outros componentes, como visualizações, objetos de solicitação e serviços, ainda podem ser alterados, pois estão vinculados aos requisitos de negócios, mas não aos controladores.
É disso que se trata o SRP, e existem muitas técnicas para escrever código que atendem a esse princípio. Um exemplo disso é a injeção de dependência (algo que também é útil para escrever código testável )
É difícil imaginar um grande projeto baseado no Princípio de Responsabilidade Única sem Injeção de Dependência. Vamos dar uma olhada em nossa classe OrderService novamente:
public class OrderService { public void Create(...) { // Creating the order(and let’s forget about reserving here, it’s not important for following examples) // Sending an email to client with order details var smtp = new SMTP(); // Setting smtp.Host, UserName, Password and other parameters smtp.Send(); } }
Este código funciona, mas não é o ideal. Para entender como a classe OrderService do método de criação funciona, eles são forçados a entender as complexidades do SMTP. E, novamente, copiar e colar é a única maneira de replicar esse uso de SMTP onde for necessário. Mas com um pouco de refatoração, isso pode mudar:
public class OrderService { private SmtpMailer mailer; public OrderService() { this.mailer = new SmtpMailer(); } public void Create(...) { // Creating the order // Sending an email to client with order details this.mailer.Send(...); } } public class SmtpMailer { public void Send(string to, string subject, string body) { // SMTP stuff will be only here } }
Muito melhor já! Mas a classe OrderService ainda sabe muito sobre como enviar e-mail. Ele precisa exatamente da classe SmtpMailer para enviar e-mail. E se quisermos mudar isso no futuro? E se quisermos imprimir o conteúdo do e-mail enviado para um arquivo de log especial, em vez de realmente enviá-lo em nosso ambiente de desenvolvimento? E se quisermos fazer o teste de unidade de nossa classe OrderService? Vamos continuar com a refatoração criando uma interface IMailer:
public interface IMailer { void Send(string to, string subject, string body); }
SmtpMailer implementará essa interface. Além disso, nosso aplicativo usará um contêiner IoC e podemos configurá-lo para que o IMailer seja implementado pela classe SmtpMailer. OrderService pode então ser alterado da seguinte forma:
public sealed class OrderService: IOrderService { private IOrderRepository repository; private IMailer mailer; public OrderService(IOrderRepository repository, IMailer mailer) { this.repository = repository; this.mailer = mailer; } public void Create(...) { var order = new Order(); // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.) this.repository.Save(order); this.mailer.Send(, , ); } }
Agora estamos chegando a algum lugar! Aproveitei a oportunidade para também fazer outra mudança. O OrderService agora depende da interface IOrderRepository para interagir com o componente que armazena todos os nossos pedidos. Ele não se preocupa mais com a forma como essa interface é implementada e qual tecnologia de armazenamento a está alimentando. Agora a classe OrderService possui apenas código que trata da lógica de negócios do pedido.
Dessa forma, se um testador encontrar algo se comportando incorretamente ao enviar e-mails, o desenvolvedor saberá exatamente onde procurar: classe SmtpMailer. Se algo estava errado com descontos, o desenvolvedor, novamente, sabe onde procurar: OrderService (ou no caso de você ter adotado o SRP de cor, então pode ser DiscountService) código de classe.
No entanto, ainda não gosto do método OrderService.Create:
public void Create(...) { var order = new Order(); ... this.repository.Save(order); this.mailer.Send(, , ); }
Enviar um e-mail não faz parte do fluxo principal de criação do pedido. Mesmo se o aplicativo não enviar o e-mail, o pedido ainda será criado corretamente. Além disso, imagine uma situação em que você tenha que adicionar uma nova opção na área de configurações do usuário que permite que eles optem por não receber um e-mail após fazer um pedido com sucesso. Para incorporar isso em nossa classe OrderService, precisaremos introduzir uma dependência, IUserParametersService. Adicione localização à mistura e você terá ainda outra dependência, ITranslator (para produzir mensagens de e-mail corretas no idioma de escolha do usuário). Muitas dessas ações são desnecessárias, especialmente a ideia de adicionar essas muitas dependências e terminar com um construtor que não cabe na tela. Achei um ótimo exemplo disso na base de código do Magento (um CMS de comércio eletrônico popular escrito em PHP) em uma classe que tem 32 dependências!
quantos drones foram vendidos em 2016
Às vezes é difícil descobrir como separar essa lógica, e a classe do Magento é provavelmente uma vítima de um desses casos. É por isso que gosto da maneira orientada a eventos:
namespace .Events { [Serializable] public class OrderCreated { private readonly Order order; public OrderCreated(Order order) { this.order = order; } public Order GetOrder() { return this.order; } } }
Sempre que um pedido é criado, em vez de enviar um e-mail diretamente da classe OrderService, a classe de evento especial OrderCreated é criada e um evento é gerado. Em algum lugar do aplicativo, os manipuladores de eventos serão configurados. Um deles enviará um email para o cliente.
namespace .EventHandlers { public class OrderCreatedEmailSender : IEventHandler { public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator) { // this class depend on all stuff which it need to send an email. } public void Handle(OrderCreated event) { this.mailer.Send(...); } } }
A classe OrderCreated é marcada como Serializable propositalmente. Podemos tratar esse evento imediatamente ou armazená-lo serializado em uma fila (Redis, ActiveMQ ou outro) e processá-lo em um processo / thread separado daquele que trata das solicitações da web. Dentro Este artigo o autor explica em detalhes o que é a arquitetura orientada a eventos (por favor, não preste atenção à lógica de negócios dentro do OrderController).
Alguns podem argumentar que agora é difícil entender o que está acontecendo quando você cria o pedido. Mas isso não pode estar mais longe da verdade. Se você se sente assim, simplesmente aproveite a funcionalidade do seu IDE. Ao encontrar todos os usos da classe OrderCreated no IDE, podemos ver todas as ações associadas ao evento.
Mas quando devo usar injeção de dependência e quando devo usar uma abordagem orientada a eventos? Nem sempre é fácil responder a essa pergunta, mas uma regra simples que pode ajudá-lo é usar a injeção de dependência para todas as suas atividades principais no aplicativo e a abordagem orientada a eventos para todas as ações secundárias. Por exemplo, use Dependecy Injection com coisas como criar um pedido na classe OrderService com IOrderRepository e delegue o envio de e-mail, algo que não é uma parte crucial do fluxo de criação de pedido principal, para algum manipulador de eventos.
Começamos com um controlador muito pesado, apenas uma classe, e acabamos com uma coleção elaborada de classes. As vantagens dessas mudanças são bastante evidentes nos exemplos. No entanto, ainda existem muitas maneiras de melhorar esses exemplos. Por exemplo, o método OrderService.Create pode ser movido para uma classe própria: OrderCreator. Como a criação de pedidos é uma unidade independente da lógica de negócios que segue o Princípio de Responsabilidade Única, é natural que tenha sua própria classe com seu próprio conjunto de dependências. Da mesma forma, a remoção e o cancelamento do pedido podem ser implementados em suas próprias classes.
Quando escrevi um código altamente acoplado, algo semelhante ao primeiro exemplo deste artigo, qualquer pequena mudança no requisito poderia facilmente levar a muitas mudanças em outras partes do código. SRP ajuda os desenvolvedores a escrever código que é desacoplado, onde cada classe tem seu próprio trabalho. Se as especificações deste trabalho forem alteradas, o desenvolvedor fará alterações apenas nessa classe específica. É menos provável que a alteração interrompa todo o aplicativo, pois as outras classes ainda devem estar fazendo seu trabalho como antes, a menos, é claro, que tenham sido interrompidas.
Desenvolver código antecipadamente usando essas técnicas e seguindo o Princípio de Responsabilidade Única pode parecer uma tarefa assustadora, mas os esforços certamente serão recompensados conforme o projeto cresce e o desenvolvimento continua.