S.O.L.I.D Principles

Diogo Peixoto
17 min readSep 27, 2020

--

Software Design Principles

Escrever linhas de códigos de software é uma tarefa relativamente simples, caso a pessoa domine uma linguagem de programação, especialmente uma da terceira geração. No entanto, o desenvolvimento de software de qualidade, com um bom design e que atenda as necessidades do cliente, é uma atividade de alta complexidade, onde nem todas as pessoas têm o conhecimento de ferramentas, processos e técnicas necessárias.

Em 2001, por conta desta complexidade e da observação de que diversos times de desenvolvimento de software estavam criando processos difíceis de serem seguidos e ineficientes, um grupo de 17 profissionais da área criaram o Manifesto Agile. O manifesto é um conjunto de 4 valores e 12 princípios que auxiliam no desenvolvimento de software. Analisando-os, percebo que vários deles convergem para um dos 4 valores: Responder a mudanças mais que seguir um plano.

Para mim, o desenvolvimento de software é um conjunto de atividades que requer uma adaptabilidade às mudanças que, geralmente, são provocadas pelos stakeholders ou até mesmo pelos próprios desenvolvedores. Diversos fatores ocasionam estas alterações em momentos distintos do ciclo de vida de software. Um dos mais comuns é a mudança de requisito durante a fase de desenvolvimento.

Além da mudança de requisito no desenvolvimento de software, após o seu release é possível, e muito provável, que tenha a necessidade de correções de bugs, mudanças pela equipe de negócios, ou até mesmo, a criação de novas funcionalidades. Portanto, abraçar as mudanças deve ser um dos objetivos essenciais na tomada de decisão no design de software.

Durante a história da engenharia de software, diversos engenheiros identificaram decisões ruins de design de software que dificultaram a sua manutenibilidade, legibilidade, flexibilidade e extensibilidade. Todas essas características são extremamente desejáveis e essenciais para a vida útil de um produto de software. Para auxiliar o desenvolvimento de software e alcançar essas propriedades desejadas, engenheiros de software difundiram um conjunto de princípios de design de software que quando utilizado de maneira sistemática, facilita a difícil atividade de desenvolvimento de software de qualidade.

Este artigo tem como objetivo apresentar os princípios SOLID, apresentados no livro Agile Software Development, Principles, Patterns, and Practices por Robert C Martin. Esses cinco princípios que serão apresentados servem como fundamento ou essência para o desenvolvimento de software de qualidade, habilitando o desenvolvimento de software de alta coesão e baixo acoplamento.

Usar apenas esses princípios não garante a qualidade do software, no entanto, a sua não utilização assegura que o produto de software não terá a qualidade desejada, o que pode impactar no sucesso do negócio ao qual o software está inserido.

Single Responsibility Principle. (SRP)

Ao desenvolver um software, deve-se buscar atributos importantes para cada um de seus componentes. Esses atributos guiam o design de software, criando estruturas, e relações entre os diversos componentes que fazem com que mudanças sejam fáceis de serem aplicadas.

Antes de apresentar o SRP, é importante explicar brevemente dois conceitos da engenharia de software: coesão (cohesion) e acoplamento (coupling).

Coesão é uma medida que indica o quão relacionados estão as funcionalidades dos componentes dentro de um único módulo. Um módulo altamente coeso executa uma tarefa ou um único objetivo — “Fazer uma coisa e fazer bem”.

Acoplamento é uma medida que indica o grau de interdependência entre os diversos componentes de um software. Quanto maior o acoplamento, entre eles, maior será o impacto de uma mudança em um dos componentes.

Portanto, ao criar elementos de software (módulos, classes, métodos, etc.) é importante buscar o baixo acoplamento e a alta coesão, pois, facilita as mudanças requeridas durante o ciclo de vida de um produto de software.

A class should have only one reason to change (Uma classe deve ter apenas um motivo para mudar)

Com essa afirmação, podemos perceber que o SRP dialoga com o atributo da coesão de software explicado anteriormente. No entanto, ao invés de estar relacionado com os componentes dentro de um mesmo módulo, o nível de coesão está ligado ao nível da classe.

A descrição deste princípio é simplória, no entanto, o poder de sua aplicação traz grandes benefícios para a construção do software. Mas, antes de entrar nos detalhes deste princípio é necessário responder duas perguntas: por quê é importante separar responsabilidades distintas em classes diferentes? Qual a definição de responsabilidade?

Cada responsabilidade é um possível ponto de mudança. Portanto, quanto mais responsabilidades uma classe possui, maior a chance de ter que mudá-la. E cada mudança dispara uma série de outras modificações nas classes que dependem dela. Além de ocasionar esta cascata de mudanças, uma responsabilidade pode inibir a mudança da outra, como veremos a seguir.

Exemplo 1: Retângulo

Diagrama que apresenta a relação entra a classe Retângulo e as aplicações de computação geométrica e gráfica
Imagem 1: Retângulo com mais de uma responsabilidade

No exemplo da Image 1, o retângulo possui dois métodos: um para calcular a área do retângulo e outro para pintar o retângulo na GUI. Duas aplicações, que podem, inclusive, serem mantidas por duas equipes diferentes, utilizam a classe Retângulo. A aplicação de computação geométrica, precisa da parte da modelagem geométrica de classe (o método area()), enquanto que a aplicação gráfica, precisa da parte de renderizar a figura geométrica na tela (método draw()) e da modelagem geométrica.

A classe Retângulo possui dois problemas. O primeiro é que mesmo que a aplicação de computação geométrica não utilize a GUI, ela deverá ter o conhecimento dela, pois, a classe Retângulo depende da GUI para ser renderizada. E por último, caso a aplicação gráfica necessite mudar a classe Retângulo, essa mudança pode acarretar em uma recompilação, reteste e redeploy da aplicação de computação geométrica.

Pela necessidade de um trabalho adicional, o acoplamento entre as duas responsabilidades, pode inibir a mudança nesta classe. Uma melhor abordagem seria dividir a classe em duas: uma para a modelagem geométrica e a outra para a renderização na tela.

Diagrama com a classe Retângulo dividida em duas, cada uma com sua responsabilidade
Imagem 2: Retângulo com responsabilidades divididas

Com o redesign separando as duas classes, uma mudança na forma como um retângulo é desenhado não irá afetar a aplicação de computação geométrica, pois não existe nenhuma dependência entre elas. Com essa separação, o acoplamento entre as duas responsabilidades foi reduzido (baixo acoplamento).

Exemplo 2: Modem

A definição do SRP é baseada na palavra responsabilidade, e o exemplo do retângulo utilizou a palavra responsabilidade.

Conforme dito anteriormente, responsabilidade é uma possível razão para a mudança. Portanto, se uma classe possui mais de uma razão para mudar, essa classe possui mais de uma responsabilidade. Acredito que essa definição de responsabilidade seja bem abstrata — não encontrei nenhuma definição mais concreta na literatura — portanto, mais exemplos serão apresentados para um melhor entendimento.

interface Modem {
public void dial(String pno);
public void hangup();
public void send(char c);
public void recv();
}

No exemplo do Modem, a primeira vista, pode-se pensar que a interface Modem tem apenas uma responsabilidade. No entanto, em uma análise detalhada, nota-se que das quatro operações que a interface Modem possui, duas são relativas a comunicação (send(c) e recv()) e as outras duas a conexão (dial(pno) e hangup()).

Para separar essas duas responsabilidades, a interface Modem pode ser dividida em duas interfaces: uma para a comunicação e a outra para a conexão. Com essa separação, evita que a aplicação cliente esteja acoplada a essas duas responsabilidades.

interface ModemCommunication {
public void send(char c);
public void recv();
}
interface ModemConnection {
public void dial(String pno);
public void hangup();
}

Exemplo 3: Colaborador

Um erro comum da violação do SRP é misturar as responsabilidades de regras de negócio e persistência. Geralmente, as regras de negócio mudam mais frequentemente que a forma de persistir o dado. E quando há mudanças nas duas responsabilidades são por razões completamente distintas.

Diagrama com a classe Colaborador com duas responsabilidades distintas
Imagem 3: Colaborador com duas responsabilidades

No exemplo anterior, caso a forma de calcular o pagamento mude por conta de uma mudança na legislação, a classe Colaborador será modificada e todas as aplicações cliente, devem ser recompiladas e retestadas.

Além disto, caso a forma de persistência seja modificada (troca de um framework por uma decisão técnica ou atualização de uma especificação JPA API 2.0 para JPA API 2.1), a classe Colaborador deve ser recompilada e atualizada em todas as aplicações cliente.

Portanto, existem duas razões — bem distintas, inclusive — para que a classe Colaborador seja modificada e uma cascata de mudanças seja disparada. Para resolver esse problema, a classe Colaborador deve ser divida em duas: uma para as regras de negócio e a outra para lidar com a persistência.

Diagrama com a classe Colaborador dividida em duas, cada uma com sua responsabilidade
Imagem 4: Colaborador com responsabilidades divididas

Apesar de a definição de SRP ser simples, nem sempre é possível detectar facilmente que uma classe possui mais de uma responsabilidade. No entanto, com a experiência das decisões tomadas no passado, é possível ter um melhor entendimento de quando estamos agregando duas responsabilidades em uma única classe.

The Open-Closed Principle. (OCP)

All systems change during their life cycles. This must be born in mind when developing systems are expected to last longer than the first version. (Todos os sistemas mudam durante o seu ciclo de vida. Isso deve ser levado em consideração quando se espera que os sistemas em desenvolvimento durem mais do que a primeira versão.)

Segundo Ivar Jacobson, software muda durante todo o seu ciclo de vida. Por isso, é tão importante aplicar princípios e padrões que permitam modificações no código-fonte sem que tenha muito impacto.

Uma forma de validar o quão o código está apto para ser modificado é analisar as características de rigidez (rigidity), fragilidade (fragility), imobilidade (immobility), viscosidade (viscosity), complexidade desnecessária (needless complexity), repetição desnecessária (needless repetition) e opacidade (opacity).

Para apresentar este segundo princípio, é necessário revisar antes o conceito de rigidez e o de polimorfismo.

Um módulo de software é considerado rígido quando sua modificação é uma tarefa difícil, pois, ela desencadeia uma série de outras em partes distintas do software.

Polimorfismo significa em grego muitas formas (poli = muitas, morphos = formas). Este termo é aplicado na ciência da computação através da capacidade de programar para o geral ao invés de programar para o específico através do uso de herança. Na linguagem de programação Java, a herança é possível por meio da utilização de superclass e de interfaces.

Software entities — classes, modules, functions, etc — should be open for extension, but closed for modification. (Entidades de software — classes, módulos e funções , etc— devem ser abertas para extensão, mas fechadas para modificação.)

Quando ocorre uma mudança em um módulo A que implica em mudanças em outros módulos que dependem de A, é um sinal de que o design do software é rígido. Quando o OCP é aplicado, essas mudanças são feitas através do desenvolvimento de um novo código-fonte (extensão), ao invés da mudança no código atual existente (modificação).

Abertos para extensão — Significa que o comportamento do software pode ser estendido. Isto é, caso ocorra uma mudança no requisito de software e que seja necessário adicionar um novo comportamento, isto é possível através da criação de uma nova entidade de software.

Fechado para modificação — Estender o comportamento de software não implica na mudança dos códigos já existentes. Ou seja, caso os módulos B e C dependam do A, e, houver a necessidade de estender um comportamento em A, isso não vai acarretar na mudança dos módulos B e C. Portanto, não será necessário uma recompilação dos módulos dependentes.

Em Java, é possível aplicar o OCP através do uso de abstrações e polimorfismo. Para alcançar isto, a classe ao invés de depender das classes derivadas, depende da classe base da hierarquia. Assim, ela está fechada para modificação, no entanto, aberta para extensão. Pois, através do polimorfismo uma classe pode ter mais de um comportamento, dependendo da sua classe derivada.

Exemplo 1: Colaborador

Para aplicar o OCP, tomemos como exemplo um módulo de pagamento de folha salarial de colaboradores. Os colaboradores possuem dois tipos de contratos: um tipo recebe um salário fixo e o outro recebe uma parte fixa mais um percentual relacionado a vendas.

abstract class Colaborador {
}
class ColaboradorSalarioFixo extends Colaborador {
private double salario;
}
class ColaboradorSalarioFixoEVariavel extends Colaborador {
private double fixo;
private double percentual;
private double vendas;
}
class PagamentoService {
void pagar(List<Colaborador> colaboradores) {
for (Colaborador colaborador : colaboradores) {
double salario = 0;
if (colaborador instanceof ColaboradorSalarioFixo) {
salario = ((ColaboradorSalarioFixo) colaborador).getSalario();
} else if (colaborador instanceof ColaboradorSalarioFixoEVariavel) { ColaboradorSalarioFixoEVariavel fixoEVariavel = (ColaboradorSalarioFixoEVariavel) colaborador;
salario = fixoEVariavel.getFixo() + fixoEVariavel.getVendas() * fixoEVariavel.getPercentual();
}
}
}
}

No exemplo anterior, o módulo de pagamento não está aplicando os princípios do OCP. Caso o gerente de vendas decida criar um novo tipo de remuneração para sua equipe, onde o pagamento é baseado exclusivamente num percentual de suas vendas, as mudanças devem ser na criação de uma nova classe e também na classe PagamentoService. Portanto, não é possível estender o comportamento sem ter que modificar o PagamentoService.

Para aplicar o OCP é necessário utilizar o polimorfismo, criando um método de calcular salário na abstração e chamá-lo dentro da classe PagamentoService. Antes de adicionar o novo comportamento, vamos corrigir o design da aplicação.

abstract class Colaborador {
abstract double calcularSalario();
}
class ColaboradorSalarioFixo extends Colaborador {
private double salario;

double calcularSalario() {
return salario;
}

}
class ColaboradorSalarioFixoEVariavel extends Colaborador {
private double fixo;
private double percentual;
private double vendas;
double calcularSalario() {
return fixo + percentual * vendas;
}

}
class PagamentoService {
void pagar(List<Colaborador> colaboradores) {
for (Colaborador colaborador : colaboradores) {
double salario = colaborador.calcularSalario();
}
}
}

Após o refactoring, é possível adicionar um novo comportamento ao colaborador sem a necessidade de mudar a classe PagamentoService. Portanto, nossa aplicação está aberta para extensão e fechada para modificação.

class ColaboradorVariavel extends Colaborador {
private double percentual;
private double vendas;
double calcularSalario() {
return percentual * vendas;
}
}
class PagamentoService {
void pagar(List<Colaborador> colaboradores) {
for (Colaborador colaborador : colaboradores) {
double salario = colaborador.calcularSalario();
}
}
}

ps: Para trabalhar com valores monetários em Java, utilizar a classe BigDecimal.

LSP: The Liskov Substitution Principle

Abstração e polimorfismo são os conceitos base para a aplicação do OCP. Em linguagens estáticas como Java, esses conceitos são implementados através de heranças (interfaces, classes abstratas, classes base e classes derivadas).

Vimos que para aplicar o OCP é necessário a utilização das classes base ao invés das classes especializadas ou derivadas. No entanto, o OCP não define as regras para a utilização de heranças. É aí que surge o LSP, um princípio que complementa o OCP através da seguinte restrição:

Subtypes must be substitutable for their base types. (Os subtipos devem ser substituíveis por seus tipos de base)

Imaginemos que um método m que tem como argumento uma classe base b. E que existe uma classe d derivada de b. Caso o método m tenha um comportamento distinto para as duas classes haverá uma violação do LSP.

class Base {
}
class Derivada extends Base {
}
double m(Base b) {

}

No exemplo anterior, dizemos que a classe Derivada é frágil na presença do método m. A fragilidade (fragility), é uma característica de software que indica a tendência de um código quebrar em um ou mais lugares a partir de uma mudança realizada. Geralmente, os bugs inseridos são em lugares que não tem relação conceitual com a modificação.

Exemplo 1: Quadrado e Retângulo

Segundo o Wikipedia, o quadrado é um quadrilátero regular, ou seja, uma figura geométrica que possui quatro lados de mesmo tamanho, com quatro ângulos retos. E o retângulo é um quadrilátero que possui todos os ângulos internos iguais, ou seja, todos os ângulos têm 90 graus.

Portanto, podemos concluir que um quadrado é um caso especial de um retângulo. Frequentemente, esse tipo de relacionamento É-UM (IS-A) do mundo real é mapeado em orientação à objetos a herança.

Diagrama que mostra o relacionamento É UM entre um quadrado e retângulo
Imagem 5: Diagrama - quadrado é um retângulo

A única diferença na definição de um retângulo para um quadrado é que existe uma restrição quanto à altura e largura. Onde, no quadrado elas devem possuir o mesmo valor. Portanto, para resolver essa restrição, basta ao chamar o setAltura, modificar também o tamanho de largura. E, fazer o mesmo para a chamada do setLargura.

class Quadrado extends Retangulo {
void setAltura(double altura){
super.altura = altura;
super.largura = altura;
}
void setLargura(double largura){
super.largura = largura;
super.altura = largura;
}
}

Agora, imaginemos que temos um método m que recebe um retângulo, modifica sua altura e largura, chama o método area() e compara se a área é igual a multiplicação de sua altura e largura.

void m(Retangulo r) {
double altura = 2;
double largura = 4;
r.setLargura(largura);
r.setAltura(altura);
assert(r.area() == altura * largura)
}

Caso o objeto passado seja um quadrado, a área será 16 ao invés de 8. Portanto, o método m terá comportamentos distintos a depender de qual objeto será passado, violando o LSP. Então, a classe Quadrado é frágil na presença do método m.

Exemplo 2: Colaborador

Uma das funcionalidades de um sistema de gestão de pessoas é calcular o banco de horas de um colaborador a cada semestre, pois, no fechamento das do semestre será descontado ou acrescido no salário do mês o saldo das horas. Nesta empresa existem dois tipos de colaboradores: gestor e o colaborador.

No entanto, há duas condicionais: os gestores não tem banco de horas e apenas eles recebem um bônus. O diagrama a seguir mostra uma possível modelagem.

Diagrama que mostra o relacionamento É UM entre um gestor e colaborador
Imagem 6: Diagrama - gestor é um colaborador

O problema da modelagem anterior é que o gestor não tem horas extras. Uma forma simples de resolvê-lo é lançar uma exceção ao chamar o método calcularHorasExtras.

class Gestor extends Colaborador {
double calcularComissao(){
}
double calcularHorasExtras(){
throw new MethodNotSupportedException();
}
}

Uma rotina é executada a meia-noite dos dias 30 de Junho e 31 de Dezembro de cada ano para calcular as horas extras de todos os funcionários.

class HorasExtrasService {
double calcularHorasExtras(List<Colaborador> colaboradores){
for(Colaborador colaborador: colaboradores){
colaborador.calcularHorasExtras();
}
}
}

Caso exista um colaborador do tipo gestor, o método calcularHorasExtras lançará uma exceção. Assim, o comportamento deste método varia segundo o tipo de classe passado como argumento. Portanto, a classe Gestor é frágil na presença do método calcularHorasExtras.

A primeira vista, as duas soluções propostas pareciam estar corretas. Um quadrado é um retângulo e um gestor é um colaborador. No entanto, por quê ambos apresentaram inconsistências nas chamadas dos métodos?

Para os métodos calcularHorasExtras() e m(), gestor e colaborador possuem comportamentos distintos, assim como quadrado e retângulo. Ou seja, em termos de comportamento, um gestor não é um colaborador, e um quadrado não é o retângulo. Por isso, a hierarquia de ambos deve ser analisada novamente (Solução no princípio a seguir).

The Interface Segregation Principle. (ISP)

Em algumas ocasiões, uma classe cliente pode necessitar de um grupo de métodos que não sejam estritamente relacionados, ou ainda, clientes distintos dependem de um subconjunto dos métodos de uma interface.

O ISP reconhece que essa é uma necessidade real dos clientes ter uma interface não coesa. No entanto, ele sugere que os clientes não tenham conhecimento dessa interface grande. Ao invés disso, ele tenha conhecimento de diversas interfaces coesas.

Clients should not be forced to depend on methods that they do not use. (Clientes não devem ser forçados a depender de métodos que eles não usam)

Quando clientes são forçados a depender de métodos que eles não utilizam, estão sujeitos a mudanças. Portanto, estão acoplados a métodos que sequer deveriam ter o conhecimento, assim sendo, ocasionando um alto acoplamento.

Exemplo 1: Colaborador

Voltemos ao nosso exemplo do LSP, onde um gestor não possui horas extras e possui um bônus, enquanto que um colaborador não gestor possui horas extras, mas não possui bônus. Neste caso, podemos modelar o nosso problema com a criação de interfaces, segregando os métodos.

Imagem 7: Diagrama das classes da folha de pagamento

Nesta solução, o Gestor não precisa degenerar a função calcularHorasExtras do Colaborador, pois, ele não mais herda do colaborador. Além disto, as classes Gestor e Colaborador agora possuem conhecimentos de suas interfaces especializadas e coesas.

E caso existe uma mudança na interface que declara o método calcularHorasExtras, não irá mais afetar o Gestor que não deve ter conhecimento deste método. Assim, fica mais fácil alterar esta interface.

É importante ressaltar que o ISP e o LSP dialogam e que em alguns casos, uma degeneração de uma função que viola o LSP pode ser resolvida com a segregação de interface ISP. E por fim, em Java, é possível aplicar o ISP através de implementação de múltiplas interfaces, inclusive, algumas classes nativas utilizam este recurso. (ArrayList, Connection, File)

The Dependency Inversion Principle. (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. (Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações)

Abstractions should not depend on details. Details should depend on abstractions. (Abstrações não devem depender de detalhes. Os detalhes devem depender das abstrações)

Acredito que esse seja um dos princípios mais utilizados pelas pessoas que desenvolvem em uma linguagem de programação orientada à objetos.

A primeira afirmação está relacionada com as dependências entre os módulos. Ela fica clara, principalmente, numa arquitetura em camadas (layered architecture) onde cada camada é responsável por uma parte da aplicação. Apesar deste padrão não especificar o número de camadas, a maioria das aplicações que utiliza essa arquitetura possui 4 camadas: apresentação (presentation), regras de negócio (business), persistência (persistence) e banco (database).

Imagem que mostra a arquitetura em camadas
Imagem 8: Arquitetura em camadas

A primeira parte do DIP afirma que a camada de business não deve depender da camada de persistence. Ao invés disto, deve depender de uma abstração (interface ou uma classe abstrata).

class ColaboradorService {
private ColaboradorPersistence persistence;
void editar(Colaborador colaborador){
// Validar as regras de negócio
persistence.editar(colaborador);
}
}
class ColaboradorPersistence {
private EntityManager entitymanager;
void editar(Colaborador colaborador){
entitymanager.getTransaction().begin();
entitymanager.persist(colaborador);
entitymanager.getTransaction().commit();
entitymanager.close();
}
}

O código anterior está violando o DIP, pois, um componente da camada superior (business) depende de um componente da camada inferior (persistence). Isto não é desejado, pois, uma mudança num componente da camada inferior, impactará em uma mudança no componente da camada superior, devido ao acoplamento entre essas camadas. Portanto, essa decisão deixa o nosso software rígido (rigidity).

Para exemplificar: imaginemos que surgiu uma forma mais eficiente ou mais fácil de implementar a camada de persistência (Spring JPA, por exemplo). Com isso, queremos trocar a camada de persistência. Neste cenário, teríamos que mudar a camada de persistência e a camada de regras de negócio. Onde, a mudança da camada superior seria motivada por uma mudança da camada inferior, o que não é desejável. Neste caso, a camada cliente (regras de negócio) sofreria uma mudança por conta da camada servidor (persistência).

Antes de apresentar uma solução que esteja em conformidade com o DIP, vamos destrinchar um pouco a segunda parte do DIP.

A segunda parte do DIP é complementar à primeira. Onde é recomendado que nenhuma classe deve depender de uma classe concreta, ao invés disto depender apenas de abstrações (interfaces ou classes abstratas). Dito isto, existe um problema na nossa classe ColaboradorService, pois, ela depende da classe concreta ColaboradorPersistence. Para estar em conformidade com o DIP, precisamos que as classes dependam apenas de abstrações.

interface IColaboradorPersistence {
void editar(Colaborador colaborador);
}
class ColaboradorService {
private IColaboradorPersistence persistence;
void editar(Colaborador colaborador){
// Validar as regras de negócio
persistence.editar(colaborador);
}
}
class ColaboradorPersistence implements IColaboradorPersistence {
private EntityManager entitymanager;
void editar(Colaborador colaborador){
entitymanager.getTransaction().begin();
entitymanager.persist(colaborador);
entitymanager.getTransaction().commit();
entitymanager.close();
}
}

Com essas modificações, ColaboradorService e ColaboradorPersistence passam a depender da abstração IColaboradorPersistence. Assim, caso haja uma mudança nos detalhes da implementação, não é necessário uma mudança no componente da camada superior.

Diagrama com as dependências da abstração
Imagem 9: Diagrama com as dependências das classes apenas à abstração

É importante observar que o “dono” da interface IColaboradorPersistence não é a sua implementação, e sim a classe cliente ColaboradorService. Portanto, uma mudança na abstração deve ser motivada apenas pela classe cliente.

A tarefa de desenvolver software de qualidade, que consiga atender a todas as características que facilitem a sua extensibilidade, é bem difícil. Portanto, é importante seguirmos princípios e padrões de projetos descobertos e validados pela comunidade de engenharia de software. Para assim, deixarmos aptos nosso software para quando as mudanças vierem.

REFERÊNCIAS

https://agilemanifesto.org/

Agile Software Development, Principles, Patterns, and Practices by Robert C. Martin

Object Oriented Software Engineering: A Use Case Driven Approach by Ivar Jacobson

Wikipedia — Polimorfismo

Java How to Program, Early Objects (Deitel: How to Program) 11th Edition

Wikipedia — Quadrado

Wikipedia — Retângulo

Layered Architecture

--

--

Diogo Peixoto
Diogo Peixoto

Written by Diogo Peixoto

Apaixonado por compartilhar, errar, aprender e um pouco de engenharia de software

Responses (1)