Tudo que você precisa saber sobre princípios sólidos em Java



Neste artigo, você aprenderá em detalhes sobre o que são princípios sólidos em java com exemplos e sua importância com exemplos da vida real.

No mundo de (OOP), existem muitas diretrizes, padrões ou princípios de design. Cinco desses princípios são geralmente agrupados e são conhecidos pela sigla SOLID. Embora cada um desses cinco princípios descreva algo específico, eles também se sobrepõem, de modo que a adoção de um deles implica ou leva à adoção de outro. Neste artigo, entenderemos os princípios SOLID em Java.

História dos Princípios SOLID em Java

Robert C. Martin deu cinco princípios de design orientado a objetos, e a sigla “S.O.L.I.D” é usada para isso. Quando você usa todos os princípios de S.O.L.I.D de uma maneira combinada, torna-se mais fácil desenvolver software que pode ser gerenciado facilmente. Os outros recursos do uso de S.O.L.I.D são:





  • Evita odores de código.
  • Código do refrator rapidamente.
  • Pode fazer desenvolvimento de software adaptável ou ágil.

Quando você usa o princípio de S.O.L.I.D em sua codificação, você começa a escrever o código que é eficiente e eficaz.



Qual é o significado de S.O.L.I.D?

Solid representa cinco princípios de java, que são:

  • S : Princípio de responsabilidade única
  • OU : Princípio aberto-fechado
  • eu : Princípio de substituição de Liskov
  • Eu : Princípio de segregação de interface
  • D : Princípio de inversão de dependência

Neste blog, discutiremos todos os cinco princípios SOLID do Java em detalhes.



Princípio de responsabilidade única em Java

O que isso quer dizer?

Robert C. Martin o descreve como uma classe deve ter apenas uma e única responsabilidade.

De acordo com o princípio da responsabilidade única, deve haver apenas um motivo pelo qual uma classe deve ser alterada. Isso significa que uma classe deve ter uma tarefa a fazer. Este princípio é freqüentemente denominado como subjetivo.

O princípio pode ser bem compreendido com um exemplo. Imagine que existe uma classe que realiza as seguintes operações.

  • Conectado a um banco de dados

  • Leia alguns dados das tabelas do banco de dados

  • Finalmente, grave-o em um arquivo.

Você já imaginou o cenário? Aqui a classe tem vários motivos para mudar, e poucos deles são a modificação da saída do arquivo, a adoção de um novo banco de dados. Quando falamos sobre responsabilidade de princípio único, diríamos que há muitas razões para a classe mudar, portanto, não se encaixa adequadamente no princípio de responsabilidade única.

Por exemplo, uma classe Automóvel pode iniciar ou parar sozinha, mas a tarefa de lavá-la pertence à classe CarWash. Em outro exemplo, uma classe Book tem propriedades para armazenar seu próprio nome e texto. Mas a tarefa de imprimir o livro deve pertencer à classe Book Printer. A classe Book Printer pode imprimir em um console ou outro meio, mas tais dependências são removidas da classe Book

Por que este princípio é necessário?

Quando o princípio de responsabilidade única é seguido, o teste é mais fácil. Com uma única responsabilidade, a turma terá menos casos de teste. Menos funcionalidade também significa menos dependências para outras classes. Isso leva a uma melhor organização do código, já que classes menores e bem definidas são mais fáceis de pesquisar.

Um exemplo para esclarecer este princípio:

Suponha que você seja solicitado a implementar um serviço UserSetting no qual o usuário pode alterar as configurações, mas antes disso, o usuário precisa ser autenticado. Uma maneira de implementar isso seria:

public class UserSettingService {public void changeEmail (User user) {if (checkAccess (user)) {// Concede opção para alterar}} public boolean checkAccess (User user) {// Verifique se o usuário é válido. }}

Tudo parece bem até que você queira reutilizar o código checkAccess em algum outro lugar OU queira fazer alterações na forma como o checkAccess está sendo feito. Em todos os 2 casos, você acabaria alterando a mesma classe e, no primeiro caso, teria que usar UserSettingService para verificar o acesso também.
Uma maneira de corrigir isso é decompor o UserSettingService em UserSettingService e SecurityService. E mova o código checkAccess para SecurityService.

public class UserSettingService {public void changeEmail (User user) {if (SecurityService.checkAccess (user)) {// Concede opção para alterar}}} public class SecurityService {public static boolean checkAccess (User user) {// verifique o acesso. }}

Princípio aberto e fechado em Java

Robert C. Martin o descreve como componentes de software devem ser abertos para extensão, mas fechados para modificação.

Para ser preciso, de acordo com este princípio, uma classe deve ser escrita de tal maneira que execute seu trabalho perfeitamente, sem a suposição de que as pessoas no futuro simplesmente virão e a mudarão. Portanto, a classe deve permanecer fechada para modificação, mas deve ter a opção de ser estendida. As formas de estender a aula incluem:

  • Herdando da classe

  • Substituindo os comportamentos necessários da classe

  • Estendendo certos comportamentos da classe

Um excelente exemplo de princípio aberto-fechado pode ser entendido com a ajuda de navegadores. Você se lembra de instalar extensões em seu navegador Chrome?

A função básica do navegador chrome é navegar em sites diferentes. Deseja verificar a gramática ao escrever um e-mail usando o navegador Chrome? Se sim, você pode simplesmente usar a extensão Grammarly, que fornece a verificação gramatical do conteúdo.

Este mecanismo onde você adiciona coisas para aumentar a funcionalidade do navegador é uma extensão. Portanto, o navegador é um exemplo perfeito de funcionalidade que está aberta para extensão, mas fechada para modificação. Em palavras simples, você pode aprimorar a funcionalidade adicionando / instalando plug-ins em seu navegador, mas não pode construir nada novo.

Por que esse princípio é necessário?

O OCP é importante, pois as classes podem chegar até nós por meio de bibliotecas de terceiros. Devemos ser capazes de estender essas classes sem nos preocupar se essas classes básicas podem suportar nossas extensões. Mas a herança pode levar a subclasses que dependem da implementação da classe base. Para evitar isso, o uso de interfaces é recomendado. Essa abstração adicional leva a um acoplamento fraco.

Digamos que precisamos calcular áreas de várias formas. Começamos criando uma classe para a nossa primeira forma Retânguloque tem 2 atributos de comprimento& largura.

public class Rectangle {public double length public double width}

Em seguida, criamos uma classe para calcular a área deste retânguloque tem um método calcularRectangleAreaque leva o retângulocomo parâmetro de entrada e calcula sua área.

public class AreaCalculator {public double calculRectangleArea (retângulo retângulo) {return rectangle.length * rectangle.width}}

Por enquanto, tudo bem. Agora, digamos que tenhamos nosso segundo círculo de forma. Então, prontamente criamos uma nova classe Circlecom um único raio de atributo.

public class Circle {public double radius}

Então nós modificamos o Areacalculatorclasse para adicionar cálculos de círculo por meio de um novo método calcularCircleaArea ()

public class AreaCalculator {public double calculRectangleArea (retângulo retângulo) {return rectangle.length * rectangle.width} public double ComputCircleArea (Circle circle) {return (22/7) * circle.radius * circle.radius}}

No entanto, observe que houve falhas na maneira como projetamos nossa solução acima.

Vamos dizer que temos um novo pentágono de forma. Nesse caso, acabaremos novamente modificando a classe AreaCalculator. Conforme os tipos de formas aumentam, isso se torna mais confuso, pois AreaCalculator continua mudando e quaisquer consumidores dessa classe terão que continuar atualizando suas bibliotecas que contêm AreaCalculator. Como resultado, a classe AreaCalculator não terá a linha de base (finalizada) com segurança, pois toda vez que uma nova forma vier, ela será modificada. Portanto, este design não está fechado para modificações.

AreaCalculator precisará continuar adicionando sua lógica de computação em métodos mais novos. Não estamos realmente expandindo o escopo das formas, em vez disso, estamos simplesmente fazendo uma solução individualizada (bit a bit) para cada forma adicionada.

Modificação do projeto acima para cumprir o princípio aberto / fechado:

Vamos agora ver um design mais elegante que resolve as falhas no design acima, aderindo ao princípio aberto / fechado. Em primeiro lugar, tornaremos o design extensível. Para isso, precisamos primeiro definir um tipo de base Forma e fazer com que Círculo e Retângulo implementem a interface Forma.

interface pública Forma {public double calculArea ()} public class Rectangle implementa Shape {double comprimento double width public double calculArea () {return length * width}} public class Circle implementa Shape {public double radius public double calculArea () {return (22 / 7) * raio * raio}}

Existe uma interface de base Shape. Todas as formas agora implementam a interface básica Forma. A interface de forma tem um método abstrato calculArea (). O círculo e o retângulo fornecem sua própria implementação substituída do método calculArea () usando seus próprios atributos.
Trouxemos um grau de extensibilidade, pois as formas agora são uma instância das interfaces de formas. Isso nos permite usar a forma em vez de classes individuais
O último ponto acima mencionado consumidor dessas formas. Em nosso caso, o consumidor será a classe AreaCalculator que agora se pareceria com isso.

public class AreaCalculator {public double calculShapeArea (Shape shape) {return shape.calculateArea ()}}

Este AreaCalculatorA classe agora remove totalmente as falhas de design observadas acima e oferece uma solução limpa que segue o princípio aberto-fechado. Vamos prosseguir com outros Princípios SOLID em Java

Princípio de substituição de Liskov em Java

Robert C. Martin o descreve como tipos derivados devem ser completamente substituíveis por seus tipos básicos.

O princípio de substituição de Liskov assume q (x) como uma propriedade, demonstrável sobre entidades de x que pertence ao tipo T. Agora, de acordo com este princípio, o q (y) deve ser agora demonstrável para objetos y que pertencem ao tipo S, e o S é na verdade um subtipo de T. Agora você está confuso e não sabe o que o princípio de substituição de Liskov realmente significa? A definição disso pode ser um pouco complexa, mas, na verdade, é bem fácil. A única coisa é que cada subclasse ou classe derivada deve ser substituída por seu pai ou classe base.

Você pode dizer que é um princípio orientado a objetos único. O princípio pode ainda ser simplificado por um tipo de filho de um tipo particular de pai, sem fazer nenhuma complicação ou explodir as coisas, deve ter a capacidade de substituir aquele pai. Este princípio está intimamente relacionado ao princípio de Substituição de Liskov.

Por que esse princípio é necessário?

Isso evita o uso indevido da herança. Ajuda-nos a conformar-nos com a relação “é um”. Também podemos dizer que as subclasses devem cumprir um contrato definido pela classe base. Nesse sentido, está relacionado aProjeto por contratoque foi descrito pela primeira vez por Bertrand Meyer. Por exemplo, é tentador dizer que um círculo é um tipo de elipse, mas os círculos não têm dois focos ou eixos maior / menor.

O LSP é popularmente explicado usando o exemplo do quadrado e do retângulo. se assumirmos uma relação ISA entre Quadrado e Retângulo. Assim, chamamos de “Quadrado é um Retângulo”. O código abaixo representa o relacionamento.

public class Rectangle {private int length private int breadth public int getLength () {return length} public void setLength (int length) {this.length = length} public int getBreadth () {return breadth} public void setBreadth (int breadth) { this.breadth = breadth} public int getArea () {return this.length * this.breadth}}

Abaixo está o código do Square. Observe que Square estende Rectangle.

public class Square extends Rectangle {public void setBreadth (int breadth) {super.setBreadth (breadth) super.setLength (breadth)} public void setLength (int length) {super.setLength (length) super.setBreadth (length)}}

Nesse caso, tentamos estabelecer uma relação ISA entre Square e Rectangle de forma que chamar “Square is a Rectangle” no código a seguir começaria a se comportar inesperadamente se uma instância de Square fosse passada. Um erro de afirmação será lançado no caso de verificação de “Área” e verificação de “Largura”, embora o programa seja encerrado quando o erro de afirmação é lançado devido à falha na verificação de Área.

public class LSPDemo {public void calculArea (Rectangle r) {r.setBreadth (2) r.setLength (3) assert r.getArea () == 6: printError ('area', r) assert r.getLength () == 3: printError ('comprimento', r) assert r.getBreadth () == 2: printError ('largura', r)} private String printError (String errorIdentifer, Rectangle r) {return 'Valor inesperado de' + errorIdentifer + ' por exemplo de '+ r.getClass (). getName ()} public static void main (String [] args) {LSPDemo lsp = new LSPDemo () // Uma instância de Rectangle é passada lsp.calculateArea (new Rectangle ()) // Uma instância de Square é passada lsp.calculateArea (new Square ())}}

A classe demonstra o Princípio de Substituição de Liskov (LSP) De acordo com o princípio, as funções que usam referências às classes base devem ser capazes de usar objetos de classe derivada sem saber.

Assim, no exemplo mostrado a seguir, a função calculArea que usa a referência de “Retângulo” deve ser capaz de usar os objetos de classe derivada como Quadrado e cumprir o requisito imposto pela definição de Retângulo. Deve-se observar que, de acordo com a definição de Retângulo, o seguinte deve sempre ser verdadeiro, dados os dados abaixo:

  1. O comprimento deve ser sempre igual ao comprimento passado como entrada para o método, setLength
  2. A largura deve ser sempre igual à largura passada como entrada para o método, setBreadth
  3. A área deve ser sempre igual ao produto de comprimento e largura

No caso, tentamos estabelecer uma relação ISA entre o Quadrado e o Retângulo de tal forma que chamamos de “Quadrado é um Retângulo”, o código acima começaria a se comportar inesperadamente se uma instância de Quadrado fosse passada. Erro de afirmação será lançado em caso de verificação de área e verificação para amplitude, embora o programa seja encerrado quando o erro de asserção for lançado devido à falha na verificação de área.

A classe Square não precisa de métodos como setBreadth ou setLength. A classe LSPDemo precisaria saber os detalhes das classes derivadas de Rectangle (como Square) para codificar apropriadamente para evitar o lançamento de erros. A mudança no código existente quebra o princípio aberto-fechado em primeiro lugar.

Princípio de Segregação de Interface

Robert C. Martin o descreve como os clientes não devem ser forçados a implementar métodos desnecessários que eles não usarão.

De acordo comPrincípio de segregação de interfaceum cliente, não importa o que nunca deve ser forçado a implementar uma interface que não usa ou o cliente nunca deve ser obrigado a depender de qualquer método, que não seja usado por eles. Então, basicamente, os princípios de segregação de interface como você prefere o interfaces, que são pequenas, mas específicas do cliente em vez de interfaces monolíticas e maiores. Em suma, seria ruim para você forçar o cliente a depender de uma determinada coisa, da qual ele não precisa.

Por exemplo, uma única interface de registro para gravar e ler registros é útil para um banco de dados, mas não para um console. Ler logs não faz sentido para um logger de console. Continuando com este artigo Princípios SOLID em Java.

Por que esse princípio é necessário?

Digamos que existe uma interface de Restaurante que contém métodos para aceitar pedidos de clientes online, clientes de discagem ou telefone e clientes que entram. Ele também contém métodos para lidar com pagamentos online (para clientes online) e pessoalmente (para clientes que entram na loja, bem como clientes por telefone quando o pedido é entregue em casa).

exemplo de bloco estático em java

Agora vamos criar uma interface Java para Restaurant e nomeá-la como RestaurantInterface.java.

interface pública RestaurantInterface {public void acceptOnlineOrder () public void takeTelephoneOrder () public void payOnline () public void walkInCustomerOrder () public void payInPerson ()}

Existem 5 métodos definidos no RestaurantInterface que são para aceitar pedidos online, receber pedidos por telefone, aceitar pedidos de um cliente que entrou na loja, aceitar pagamento online e aceitar o pagamento pessoalmente.

Vamos começar implementando o RestaurantInterface para clientes online como OnlineClientImpl.java

public class OnlineClientImpl implementa RestaurantInterface {public void acceptOnlineOrder () {// lógica para fazer pedido on-line} public void takeTelephoneOrder () {// Não aplicável para pedido on-line lançar novo UnsupportedOperationException ()} public void payOnline () {// lógica para pagar online} public void walkInCustomerOrder () {// Não aplicável para pedido online lançar novo UnsupportedOperationException ()} public void payInPerson () {// Não aplicável para pedido online lançar novo UnsupportedOperationException ()}}
  • Como o código acima (OnlineClientImpl.java) é para pedidos online, lance UnsupportedOperationException.

  • Clientes online, telefônicos e presenciais usam a implementação RestaurantInterface específica para cada um deles.

  • As classes de implementação para cliente telefônico e cliente walk-in terão métodos sem suporte.

  • Como os 5 métodos fazem parte do RestaurantInterface, as classes de implementação devem implementar todos os 5 deles.

  • Os métodos que cada uma das classes de implementação lança UnsupportedOperationException. Como você pode ver claramente - implementar todos os métodos é ineficiente.

  • Qualquer mudança em qualquer um dos métodos do RestaurantInterface será propagada para todas as classes de implementação. A manutenção do código então começa a se tornar realmente complicada e os efeitos de regressão das mudanças continuarão aumentando.

  • RestaurantInterface.java quebra o Princípio de Responsabilidade Única porque a lógica para pagamentos, bem como para a colocação de pedidos, é agrupada em uma única interface.

Para superar os problemas mencionados acima, aplicamos o Princípio de Segregação de Interface para refatorar o projeto acima.

  1. Separe as funcionalidades de pagamento e colocação de pedido em duas interfaces enxutas separadas, PaymentInterface.java e OrderInterface.java.

  2. Cada um dos clientes usa uma implementação de PaymentInterface e OrderInterface. Por exemplo - OnlineClient.java usa OnlinePaymentImpl e OnlineOrderImpl e assim por diante.

  3. O Princípio de responsabilidade única agora está anexado como interface de pagamento (PaymentInterface.java) e interface de pedido (OrderInterface).

  4. A alteração em qualquer uma das interfaces de pedido ou pagamento não afeta a outra. Eles são independentes agora. Não haverá necessidade de fazer nenhuma implementação fictícia ou lançar uma UnsupportedOperationException, pois cada interface tem apenas métodos que sempre usará.

Depois de aplicar o ISP

Princípio de Inversão de Dependência

Robert C. Martin o descreve como ele depende de abstrações, não de concreções. De acordo com ele, o módulo de alto nível nunca deve depender de nenhum módulo de baixo nível. por exemplo

Você vai a uma loja local para comprar algo e decide pagar com seu cartão de débito. Então, quando você dá o seu cartão ao balconista para fazer o pagamento, o balconista não se preocupa em verificar que tipo de cartão você deu.

Mesmo que você tenha dado um cartão Visa, ele não vai lançar uma máquina Visa para passar seu cartão. O tipo de cartão de crédito ou débito que você tem para pagar não importa, eles vão simplesmente passá-lo. Portanto, neste exemplo, você pode ver que você e o balconista dependem da abstração do cartão de crédito e você não está preocupado com as especificações do cartão. Isso é o que é um princípio de inversão de dependência.

Por que esse princípio é necessário?

Ele permite que um programador remova dependências codificadas para que o aplicativo se torne fracamente acoplado e extensível.

classe pública Aluno {endereço privado endereço aluno público () {endereço = novo endereço ()}}

No exemplo acima, a classe Aluno requer um objeto Endereço e é responsável por inicializar e usar o objeto Endereço. Se a classe de endereço for alterada no futuro, teremos que fazer alterações na classe do aluno também. Isso torna o acoplamento estreito entre os objetos Aluno e Endereço. Podemos resolver esse problema usando o padrão de design de inversão de dependência. isto é, o objeto de endereço será implementado de forma independente e fornecido ao Aluno quando o Aluno for instanciado usando a inversão de dependência baseada no construtor ou no setter.

Com isso, chegamos ao fim deste SOLID Principles in Java.

Confira o pela Edureka, uma empresa de aprendizagem online confiável com uma rede de mais de 250.000 alunos satisfeitos espalhados por todo o mundo. O curso de certificação e treinamento Java J2EE e SOA da Edureka é projetado para estudantes e profissionais que desejam ser um desenvolvedor Java. O curso foi elaborado para dar a você uma vantagem inicial na programação Java e treiná-lo para os conceitos básicos e avançados de Java, juntamente com várias estruturas Java como Hibernate e Spring.

Tem alguma questão para nós? Mencione isso na seção de comentários deste blog “SOLID Principles in Java” e entraremos em contato com você o mais breve possível.