post

São cinco princípios da programação orientada a objetos que facilitam no desenvolvimento de softwares, tornando-os fáceis de manter e estender.

O que é SOLID?

SOLID é um acrônimo criado por Michael Feathers, após observar que cinco princípios da orientação a objetos e design de código - criados por Robert C. Martin (a.k.a. Uncle Bob) e abordados no artigo The Principles of OOD - poderiam se encaixar nesta palavra.

S.O.L.I.D: os 5 princípios da POO

  • S - Single Responsibility Principle (Princípio da Responsabilidade Única)

  • O - Open-Closed Principle (Princípio Aberto-Fechado)

  • L - Liskov Substitution Principle (Princípio da Substituição de Liskov)

  • I - Interface Segregation Principle (Princípio da Segregação da Interface)

  • D - Dependency Inversion Principle (Princípio da Inversão da Dependência)

Esses princípios ajudam o programador a escrever códigos mais limpos, separando responsabilidades, diminuindo acoplamentos, facilitando na refatoração e estimulando o reaproveitamento do código.

1. SRP - Single Responsibility Principle

Esse princípio declara que uma classe deve ser especializada em um único assunto e possuir apenas uma responsabilidade dentro do software, ou seja, a classe deve ter uma única tarefa ou ação para executar.

Exemplo prático do SRP:


class Order{


    public function calculateTotalSum(){/*...*/}

    public function getItems(){/*...*/}

    public function getItemCount(){/*...*/}

    public function addItem($item){/*...*/}

    public function deleteItem($item){/*...*/}


    public function printOrder(){/*...*/}

    public function showOrder(){/*...*/}


    public function load(){/*...*/}

    public function save(){/*...*/}

    public function update(){/*...*/}

    public function delete(){/*...*/}

}

A classe Order viola o SRP porque realiza 3 tipos distintos de tarefas. Além de lidar com as informações do pedido, ela também é responsável pela exibição e manipulação dos dados. Lembre-se, o princípio da responsabilidade única preza que uma classe deve ter um, e somente um, motivo para mudar.

A violação do SRP pode gerar alguns problemas, sendo eles:

  • Falta de coesão - uma classe não deve assumir responsabilidades que não são suas;

  • Alto acoplamento - Mais responsabilidades geram um maior nível de dependências, deixando o sistema engessado e frágil para alterações;

  • Dificuldades na implementação de testes automatizados - É difícil de “mockar” esse tipo de classe;

  • Dificuldades para reaproveitar o código;

public class Sale {

/**Atributos*/

public String getFormatToFile(int userId, int courtesyBeneficiaryId, int posCode, SparseArray map, Context context) {


   SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Constants.FORMAT_TO_FILE_DATE_PATTERN)

   StringBuilder stringBuilder = new StringBuilder(Constants.FILE_SALE_IDENTIFIER + this.id + Constants.SEPARATOR_PIPE); //cod da venda

   if (this.typeSale == TypeSale.SALE.ordinal()) {

       stringBuilder.append(posCode)

               .append(Constants.SEPARATOR_PIPE)

               .append(date).append(Constants.SEPARATOR_PIPE)

               .append(MaskEditUtil.formatDouble(this.getTotalSales(), context));//total sale

   } else if (this.typeSale == TypeSale.COURTESY.ordinal()) {

       stringBuilder.append(userId)

               .append(Constants.SEPARATOR_PIPE)

               .append(TypeSale.COURTESY.getCodFileExport()).append(Constants.SEPARATOR_PIPE)// F identicador de cortesia

               .append(date)

               .append(Constants.SEPARATOR_PIPE)//date

               .append(courtesyBeneficiaryId);//courtesyBeneficiaryId

   }

   for (SalePaymentMethodJoin joins : this.paymentMethodJoins) {

       stringBuilder.append(joins.getFormatToFile(map, context));

   }

   stringBuilder.append(Constants.SEPARATOR_PIPE).append(this.quantityItems);

   for (Ticket ticket : this.tickets) {

       stringBuilder.append(ticket.getformatToFile(context));

   }

   return stringBuilder.toString();

}

O código acima está sendo responsável por mais de uma ação, pois além de representar uma venda está fazendo a lógica de uma formatação para ser escrita em um arquivo, não que o código esteja errado mas onde se encontra não seria o melhor lugar para estar, pois é um risco, visto que, quando for realizar algum ajuste nesta classe pode acontecer algum imprevisto, como alterar algo sem necessidade na formatação.

Como proposta do tópico, é recomendado realizar a separação do código, criar uma classe que cuide dessa responsabilidade e de outras futuras caso outras formatação sejam feitas. Isolando esse trecho de código a pessoa designada a realizar a manutenção não tem a necessidade de se preocupar com a parte de vendas, e sim somente com a formatação.

public ArchiveBuilder build(Context context) {


   this.context = context;


   SettingsSharedPreferences settingsSharedPreferences = new SettingsSharedPreferences(context);


   this.shift = settingsSharedPreferences.getShift();


   this.user = settingsSharedPreferences.getManager();


   if (shift != null) {

       this.archive = new Archive(shift.getId());

   }

   return this;

}

public Archive ticketCancelation(TicketProductSale ticketProductSale) {


   StringBuilder stringBuilder = new StringBuilder();

   SimpleDateFormat simpleDateFormat = new SimpleDateFormat(Constants.FORMAT_TO_FILE_DATE_PATTERN);

   String date = simpleDateFormat.format(new Date());


   stringBuilder.append("V").append(ticketProductSale.getSaleId()).append(Constants.SEPARATOR_PIPE)

           .append("X").append(Constants.SEPARATOR_PIPE)

           .append(ticketProductSale.getSaleId()).append(Constants.SEPARATOR_PIPE)

           .append(this.user.getId()).append(Constants.SEPARATOR_PIPE)

           .append(date).append(Constants.SEPARATOR_PIPE)

           .append("0").append(Constants.SEPARATOR_PIPE)

           .append(ticketProductSale.getTicketCode());


   archive.setCreatedAt(new Date());

   archive.setCode("X" + ticketProductSale.getSaleId());

   archive.setArchiveName((new FileNameGenerator().baseName(this.context)));

   archive.setShiftOperationId(this.shift.getId());

   archive.setRowContent(stringBuilder.toString());


   return archive;

}

O princípio da responsabilidade única não se limita somente a classes, ele também pode - e deve - ser aplicado em métodos e funções, ou seja, tudo que é responsável por executar uma ação.

2. OCP - Open-Closed Principle (Princípio Aberto-Fechado)

Objetos ou entidades devem estar abertos para extensão, mas fechados para modificação, ou seja, quando novos comportamentos e recursos precisam ser adicionados no software, devemos estender e não alterar o código fonte original.

Exemplo prático do OCP:

1 - Ler login e senha do cartão NFC

2 - Fazer login com cartão

public interface CardReaderLoginListerner {

    void onWarnning(String msg);

    void onFail(String msg);

    void response(String login, String password);

}

protected void readLogin(int typeCard, LoginListerner cardReaderLoginListerner){

   try {

       String row = this.cardReader.readBlock(typeCard, WriterCardLogin.BLOCK_LOGIN);

       login = FormatForCard.extract(row, 8);

       pass = FormatForCard.extract(row, 8, 16);

       cardReaderLoginListerner.response(login, pass);

   } catch (Exception e) {

       this.listener.onFail(e.getMessage());

       e.printStackTrace();

   }

}

1 - Gravar token do evento para download

2 - Ler login e senha do cartão NFC

3 - Fazer login e download do evento

4 - Fluxo 

4.1 - somente login

4.2 - somente login e hash do evento

public class ReaderSetupEvent{ }

1 - Uso a classe leitura de login e senha

2 - Uso a classe leitura de Hash

_

1 - Vou ter duas variáveis na mesma tela

2 - Controlar estado de duas variáveis

3 - Se for login tem que ser uma se for download tem que ser outra

public interface SetupEventListener extends CardReaderLoginListerner {

   void response(String login, String password, String eventHash);

}

public class ReaderSetupEvent extends ReaderCardLogin implements CardReaderLoginListerner {

@Override

public void response(String login, String password) {

   this.login = login;

   this.password = password;

   this.readHashEvent(this.typeCard);

}

}

private void readHashEvent(int typeCard){


           this.hashEvent = this.cardReader.readBlock(typeCard, BLOCK_SETUP_EVENT);

           this.hashEvent = FormatForCard.extract(this.hashEvent, 8, 16);

           this.login = FormatForCard.clean(this.login);

           this.password = FormatForCard.clean(this.password);

           this.hashEvent = FormatForCard.clean(this.hashEvent);


((SetupEventListener)this.listener).response(this.login, this.password, this.hashEvent);

}

public class LoginFragment extends DialogFragment implements SetupEventListener {

private ReaderCardLogin cardReader;

if (LoginType.UPDATE_LOGIN == this.loginType) {

   this.cardReader = new ReaderSetupEvent(this.getContext(), this);

}else if(loginType.MANAGER_LOGIN == this.loginType){

   this.cardReader = new ReaderCardLogin(this.getContext(), this);

}

}

@Override

public void response(String login, String password) {

      this.getActivity().runOnUiThread(()->this.checkUserManager(finalLogin, finalPassword));  

}

@Override

public void response(String login, String password, String hashEvent) {

   this.checkManagerUpdate(login, password, hashEvent);

}

Quando desenvolvemos algo que contempla algum comportamento ou regra do sistema, e quando surge algo novo para ser implementado ou uma nova regra o ideal é não mexer na parte que está funcionando e sim fazer uma extensão do que está pronto para a nova implementação.

O código acima possui dois fluxos de execução que levam a retornos diferentes, o retorno de cada fluxo é feito através de uma nova instância que implementa uma INTERFACE para distinguir as regras de comportamento na leitura de NFC, this.cardReader é do tipo da interface ReaderCardLogin.

if (LoginType.UPDATE_LOGIN == this.loginType) {

   this.cardReader = new ReaderSetupEvent(this.getContext(), this);

}else if(loginType.MANAGER_LOGIN == this.loginType){

   this.cardReader = new ReaderCardLogin(this.getContext(), this);

}

Ambas implementam CardReaderLoginListerner mas somente uma implementa SetupEventListener que é responsável por ler o hash, sendo assim o código é separado por suas responsabilidades, o que facilitará a manutenção.

3. LSP - Liskov Substitution Principle (Princípio da substituição de Liskov)

Esse princípio declara que uma classe derivada deve ser substituível por sua classe base.

Esse princípio tem como objetivo trocar a instância de uma classe passada por parâmetro e o comportamento funcionará de forma transparente, o intuito é ter uma classe base com os comportamentos esperados e a partir dessa classe criar as outras e cada classe ser responsável pelas operações executadas.

class Payment{

   public void pay(long price){

       System.out.println("Pagamento executado na caderneta (Payment) "+ price);

   }

}

class Money extends Payment{

   @Override

   public void pay(long price) {

       System.out.println("Pagamento executado no Money "+ price);

   }

}

class Credit extends Payment{

   @Override

   public void pay(long price) {

       System.out.println("Pagamento executado no Credit "+ price);

   }

}

class Note extends Payment{  }

public static void main(String[] args) {

   Payment p = new Payment();

   Money m = new Money();

   Credit c = new Credit();

   Note n = new Note();

   checkout(p, 100);

   checkout(m, 250);

   checkout(c, 482);

   checkout(n, 22);

}

public static void checkout(Payment payment, long price) {

   payment.pay(price);

}

Pagamento executado na caderneta (Payment)  100

Pagamento executado no dinheiro 250

Pagamento executado no dinheiro 482

Pagamento executado na caderneta 22

No exemplo acima, a classe Payment que será estendida pela classe do método de pagamento, como: boleto, pix, cartão de crédito e débito. Cada instância de método de pagamento poderá ser utilizada no método checkout(Payment payment, long price).

4. ISP - Interface Segregation Principle (Princípio da Segregação da Interface)

Uma classe não deve ser forçada a implementar interfaces e métodos que não irão utilizar.

Esse princípio basicamente diz que é melhor criar interfaces mais específicas ao invés de termos uma única interface genérica.

public interface ProgressBarListener {


   void show(String msg);

   void progress(int actual, int max, String msg);

   void initUpdate();

   void hide();

}

public class ShowBackupFragment extends Fragment implements ProgressBarListener { 

private void sendAll() {

   if (this.syncHelper == null)

       this.syncHelper = new SyncUpdateHelper(this, getActivity());

}  


@Override

public void show(String msg) {

   LogManager.log(getClass().getSimpleName(), "Show: " + msg, "");

}

@Override

public void progress(int actual, int max, String msg) {   //Progress

   LogManager.log(getClass().getSimpleName(), "Actual: " + actual + " Max: " + max, "");

}

@Override

public void initUpdate() {//InitUpdate

   LogManager.log(getClass().getSimpleName(), "Inicio ", "");

}

@Override

public void hide() {   //Hide

   LogManager.log(getClass().getSimpleName(), "Hide: ", "");

}

}

No exemplo acima temos a implementação de uma interface um tanto quanto genérica que foi desenvolvida para atender uma funcionalidade, entretanto, acabou sendo usado em outra parte do sistema, no qual alguns do seus métodos iriam ser utilizados, dessa forma, ficou alguns métodos sem comportamento na execução, somente com a escrita de logs.

A maneira correta de fazer seria criar outra interface com os métodos que ambas iriam utilizar e fazer com que a interface atual herdasse a outra mais genérica, assim cada parte do sistema teria que implementar somente o necessário e não ficaria os métodos “vazios” só por estar.

5. DIP - Dependency Inversion Principle (Princípio da Inversão de Dependência)

De acordo com Uncle Bob:

1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender da abstração.

2. As abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Vamos entender tudo isso na prática através de exemplos:

Interface com as ações:

public interface SalesActionListener {

   void addItem(Ticket item, int numberOfTickets);

   void removeItem(long totalVaĺue, int quantityTickets);

}

Classe intermediária:

public class ListProductsFragment extends Fragment implements ItemSalesListener {


   private SalesActionListener salesActionListener;


   @Override

   public void removeItem(int position) {

       long totalTickets = this.productsFilter.get(position).getPrice() *    ticket.getQuantity();

       int quantityTickets = ticket.getQuantity();


       this.salesActionListener.removeItem(totalTickets, quantityTickets);


   }

   @Override 

   public void addItem(int position, int numberOfTickets) {

        this.ticketCreated = new Ticket(); 

        this.salesActionListener.addCart(ticketCreated, numberOfTickets);

    }

}

@Override

public void addCart(Ticket ticket, int numberOfTickets) {

     

      /** código a ser implementado */


}

@Override

public void removeItem(long totalVaĺue, int quantityTickets){

     

      /** código a ser implementado */


}

Na questão de abstração temos duas possibilidades de trabalhar: com classes abstratas, na qual podemos definir um comportamento padrão e com interfaces na qual cada implementação e detalhes serão tratados  em cada situação.

No exemplo proposto temos uma interface para adicionar e remover item da lista de compra, em um segundo momento temos uma listagem de produtos onde a interface é recebida na criação da tela e através dessa interface o item será adicionado na lista de compra, então a listagem de produto nem faz ideia de como isso deve acontecer, sendo isso abstraído pela interface, se os itens serão salvo em arquivo, no banco de dados ou em cache não cabe a listagem de produtos saber dessas informações. Com essa ideia, amanhã ou depois, por uma questão de definição, o comportamento de salvar os itens será alterado, nenhuma alteração será necessária na parte de listagem de produtos.

Conclusão

A sistemática dos princípios SOLID tornam o software mais robusto, escalável e flexível, deixando-o tolerante a mudanças, facilitando a implementação de novos requisitos para a evolução e manutenção do sistema.

Pode ser um pouco assustador no início usar todos esses princípios, nem sempre conseguiremos aplicar todos em nosso projeto, mas com a prática e constância, aos poucos vamos adquirindo a experiência necessária para escrever códigos cada vez mais maduros, já que, os princípios SOLID servem como guias para isso.

  • Análise: procure uma solução inteligente

  • Reflita sobre o código: faz parte do dia a dia, não escreva código ruim, dói no coração.

  • Escreva menos: escreva melhor, pense que amanhã você irá mexer no código

Fontes: www.medium.com

Enviar uma mensagem

Nós adoraríamos ajudar. Por favor, forneça alguns detalhes e entraremos em contato em breve.