Programando com boas práticas – Parte 1: Este artigo apresenta algumas boas práticas de programação a serem levadas em consideração com a finalidade de incrementar a manutenibilidade e extensibilidade de códigos orientados a objetos. Boas práticas, já testadas e aprovadas, certamente irão tornar seu código mais robusto e flexível. Uma boa implementação deve não somente alcançar seus objetivos funcionais, mas também ser de fácil aprimoramento e compreensão.


Em que situação o tema é útil:
Este tema é útil para desenvolvedores que almejam aperfeiçoar seus conhecimentos em boas práticas de programação, como a abstração, reutilização, extensão e desacoplamento de objetos e suas referências. Tais princípios facilitam tanto a criação de novos códigos como a manutenção de outros já existentes, muitas vezes complexos de serem alterados por sua complexidade, forte acoplamento e falta de coesão.

Boas práticas de desenvolvimento são técnicas criadas ao longo do tempo para aprimorar a qualidade de um código, facilitando sua manutenção e reuso. O uso de boas práticas, já testadas e aprovadas pela comunidade, reduz a probabilidade de um desenvolvedor eventualmente escrever códigos difíceis de terem suas funcionalidades estendidas, alteradas ou reutilizadas.

Algumas dessas práticas e princípios pregam que um código deve ser escrito uma única vez, de forma definitiva e sempre de acordo com seu objetivo funcional. Objetivo este que deve ser único, claro e preciso. Uma classe deve ser alterada somente caso haja alguma extensão em sua funcionalidade. Um código bem escrito, isolado, abstraído e coeso não precisará ser alterado por consequência de efeitos adversos ocasionados por uma ou mais modificações que tenham sido realizadas em outras partes da aplicação.

Mas e a refatoração? A refatoração tem como objetivo principal refazer algo que não foi feito da melhor maneira na primeira tentativa. A real necessidade de uma refatoração precisa ser avaliada caso a caso, levando em consideração seu custo e viabilidade. A refatoração tem sua finalidade e seu propósito. Códigos com determinadas lógicas repetidas em diferentes partes da aplicação podem precisar de refatoração para que tais duplicidades sejam eliminadas, proporcionando melhor manutenibilidade. Se ao desenvolver uma classe, por algum motivo o desenvolvedor não se satisfez com certa lógica, seja por sua complexidade, seja pela interdependência gerada entre diferentes partes da aplicação ou mesmo por sua dificuldade de reutilização, a refatoração é um bom caminho para o aperfeiçoamento. Códigos mal escritos geralmente requerem, em algum momento, refatoração para aprimorar sua estrutura interna (sua codificação), sem que se altere seu comportamento externo (o requisito funcional).

Uma eficiente refatoração certamente deve introduzir boas práticas de desenvolvimento e padrões de projeto, eliminando a necessidade de futuras novas refatorações. Se bem realizada, tanto uma melhor manutenibilidade como extensibilidade podem ser adquiridas com a refatoração. Contudo, o ideal é sempre iniciar um novo código já fazendo uso de boas práticas, incluindo padrões de projeto, evitando-se assim uma possível refatoração ou qualquer tipo de alteração que não esteja relacionada ao objetivo funcional do código (a razão de ser de uma determinada lógica, classe ou componente).

Alterações posteriores devem se restringir ao desenvolvimento de novas funcionalidades ou ampliar as já existentes, sempre de forma abstraída e desacoplada. Refatoração é sinônimo de retrabalho, e tem como foco principal refazer algo que não foi bem feito na primeira tentativa. Reescrever um código, mesmo que parcialmente, é algo que pode e deve ser evitado. Uma classe que faça referências apenas a interfaces, que esteja desacoplada de todo o resto, com objetivo claro, singular, e que faça uso de composição e abstração, dificilmente precisará de refatoração.

Como exemplo de ampliação de uma determinada funcionalidade sem a necessidade de refatoração ou reescrita, imaginemos uma classe, ou mesmo um componente de uma aplicação, responsável por realizar cálculos de contabilidade bastante complexos, que precisa ter o valor retornado por um de seus métodos alterado devido a uma nova lei trabalhista qualquer, por exemplo. Agora suponhamos que esse código já se encontre em ambiente de produção, por várias décadas, e que ao longo desse tempo tenha sofrido alterações pontuais em sua lógica por diversos times de programadores. Somando-se a isso, sabe-se que inúmeras dessas alterações foram implementadas de forma incorreta, não levando em conta futuras necessidades de alteração de sua lógica de negócio, e assim dificultando ainda mais sua compreensão e consequentemente sua manutenção. São em casos como esse que encontramos vantagens no uso de boas práticas de desenvolvimento para a extensão de funcionalidades, como na utilização do padrão Decorator, que cria uma camada superior à da função inicial, adicionando mais funcionalidades à mesma sem qualquer reescrita de código.

Ademais, como já brevemente mencionado e baseando-se nos princípios de coesão, reutilização e fraco acoplamento de funções, uma determinada classe deve representar um único propósito ou responsabilidade. Deste modo, evitam-se códigos confusos e com objetivos variados, que por sua vez dificultam a reutilização, manutenção e compreensão. Imagine como seria difícil reaproveitar algo que não possua uma funcionalidade específica e bem definida, ao contrário de algo com função singular e fortemente determinada. À vista disso, cada responsabilidade que uma classe possui torna-se uma área em potencial para futuras alterações de lógica. Quanto maior for o número de responsabilidades de uma classe, maior é a abrangência de código passível de alterações. E se cada uma dessas múltiplas responsabilidades estiver fortemente conectada, mais complexo será sua manutenção e menos óbvio sua clareza. Enfim, uma classe Bola deve ser responsável apenas por criar objetos bola, e não também chuteiras e uniformes.

Os princípios já elencados também nos ensinam que diferentes componentes, ou mesmo simples classes, devem conhecer o mínimo possível de todo o resto da aplicação. Uma classe Bola não precisa saber ou ter acesso à classe Uniforme, uma vez que a única função da classe Bola é criar bolas, que nada tem a ver com a classe Uniforme. Ainda, se um código somente tem acesso a outros códigos de forma abstrata (via interface), ele não será impactado se um ou outro código, ao qual ele faz uso, mudar. Tendo dito isso, é importante que uma classe apenas tenha referências abstratas e jamais faça uso de referências concretas. Objetos que interagem entre si devem fazê-lo de forma desacoplada.

Consequentemente, quando aplicável, prefira composição ao invés de herança, e torne seu código capaz de referenciar diferentes objetos sem a necessidade de recodificação, tanto em tempo de compilação quanto em tempo de execução. Igualmente, evite instâncias de objetos concretos, através do uso de interfaces.

Adicionalmente, separe e encapsule toda e qualquer funcionalidade que possa vir a variar ao longo do tempo. Assim será possível modificá-las separadamente sem a necessidade de modificar outras partes do código de forma desnecessária.

Abstraia, singularize e reutilize

Classes que referenciam outras classes devem realizar tais referências de forma abstraída, utilizando interfaces, sem a dependência de uma instanciação concreta. Quando se passa uma instância concreta de uma classe para outra – por exemplo, uma classe qualquer referenciando um objeto HashSet ao invés de referenciar sua interface Set –, o que se está fazendo é um acoplamento forte entre ambas, uma interdependência mútua. Isto deve ser evitado, do contrário a alteração em uma classe poderá ocasionar a necessidade de alteração em outra. Assim, uma classe que faz uso de uma HashSet deve referenciá-la através de sua interface Set, a fim de evitar o que chamamos de acoplamento forte entre objetos.

Apresentando a aplicação SysEmpresa, e seu modelo de classes

Antes de darmos prosseguimento ao artigo, vamos primeiro analisar o projeto SysEmpresa, criado com o propósito de exemplificar os princípios de abstração, singularização e reutilização. O projeto SysEmpresa é constituído basicamente de um conjunto de interfaces e suas respectivas classes. Neste cenário, IEmpresa é a interface para todas as classes do tipo “empresa” criadas no projeto. Tendo dito isso, a classe EmpresaA será a utilizada em nossos exemplos, porém outras classes empresas também poderiam ser implementadas, como EmpresaB, EmpresaC, entre outras. Cada uma dessas empresas, por sua vez, pode ter um ou mais departamentos, e todos os departamentos implementam a interface IDepartamento.

Assim sendo, em nossa EmpresaA, temos dois departamentos: DepartamentoA e DepartamentoB. E cada um desses departamentos possui instâncias das demais classes que compõem o projeto, a saber: Funcionario (que implementa a interface IFuncionario), PagamentoPJ e PagamentoCLT (que implementam a interface IPagamento) e PromoA (que implementa a interface IPromo). A interface e a classe referentes aos funcionários são responsáveis pelas funcionalidades inerentes ao cadastro de funcionários. Já a interface e suas duas classes pertinentes ao pagamento de funcionários serão utilizadas pelos departamentos da empresa para o cálculo dos salários nos regimes CLT ou PJ. Também há a interface e as implementações relativas a possíveis promoções que cada funcionário pode estar elegível. Mais à frente, no decorrer dos exemplos, criaremos outras classes, que estenderão a aplicação, referentes aos respectivos tópicos que serão abordados. Como complementação do que foi dito, vejamos o diagrama de classes, na Figura 1, referente ao projeto ...

Quer ler esse conteúdo completo? Tenha acesso completo