Android

7 abr, 2017

Como usar S.O.L.I.D no Android

Publicidade

Nesse artigo, vou explicar de maneira objetiva como utilizar o acrônimo SOLID no seu projeto Android.

O SOLID possui 5 princípios de programação orientada a objetos:

  • # S :SRP – Single responsibility principle (principio da responsabilidade única)

Single responsibility principle

  • # O : OCP – Open/closed principle: Princípio Aberto-Fechado
  • # L : LSP – Liskov substitution principle: Princípio da Substituição de Liskov
  • # I :ISP – Interface segregation principle: Princípio da Segregação da Interface
  • # D :DIP – Dependency inversion principle: Princípio da inversão da dependência

SRP: Princípio da responsabilidade única

Uma classe deve ter um, e somente um, motivo para mudar

Vamos utilizar o preenchimento de uma lista como exemplo.

Muitas vezes, colocamos regra de negócio no método onBindViewHolder do nosso Adapter, quando, na verdade, ele é somente responsável pelo preenchimento dos items do nosso RecyclerView.

Vejamos um exemplo de violação desse primeiro princípio na prática:

public class BudgetAdapter extends RecyclerView.Adapter<BudgetAdapter.ViewHolder> {

    private ArrayList<IntervalDataBase> dataBases;
    private Context context;
    public BudgetAdapter(ArrayList<IntervalDataBase> dataBases, Context context) {
        this.dataBases = dataBases;
        this.context = context;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recycler, parent,false);;
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        IntervalDataBase intervalDataBase = dataBases.get(position);
        String date = "";
        
        if (intervalDataBase.date != null) {
            String[] split = intervalDataBase.date.split("-");
            date = split[1] + " " + Util.getMonthLittle(Integer.parseInt(split[0]));
        }

        holder.date.setText(date);
        holder.name.setText(intervalDataBase.description);
        holder.value.setText(context.getString(R.string.money, String.valueOf(intervalDataBase.amount)));
    }

    @Override
    public int getItemCount() {
        return dataBases.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.layout)
        LinearLayout layout;

        @BindView(R.id.date)
        TextView date;

        @BindView(R.id.name)
        TextView name;

        @BindView(R.id.value)
        TextView value;

        public ViewHolder(View view) {
            super(view);
            ButterKnife.bind(this, view);
        }
    }

}

A violação ocorre no método onBindViewHolder, onde estamos formatando a string “date” para setar no nosso holder.date da maneira correta.

Esse método é responsável por popular os nossos items do RecyclerView, e não executar regras de negócio e formatação de dados.

Para corrigirmos o nosso código e manter esse princípio, temos que retirar a formatação dos dados desse método, como podemos ver no exemplo:

package com.budgetedonline.adapter;

import android.content.Context;
import android.graphics.Color;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.budgetedonline.R;
import com.budgetedonline.Util.Util;
import com.budgetedonline.entity.DataBase;
import com.budgetedonline.entity.IntervalDataBase;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

/**
 * Created by iagomendesfucolo on 06/12/16.
 */

public class BudgetAdapter extends RecyclerView.Adapter<BudgetAdapter.ViewHolder> {

    private ArrayList<IntervalDataBase> dataBases;
    private Context context;
    public BudgetAdapter(ArrayList<IntervalDataBase> dataBases, Context context) {
        this.dataBases = dataBases;
        this.context = context;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recycler, parent,false);;
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        IntervalDataBase intervalDataBase = dataBases.get(position);

        holder.date.setText(Util.formateDate(intervalDataBase.date));
        holder.name.setText(intervalDataBase.description);
        holder.value.setText(context.getString(R.string.money, String.valueOf(intervalDataBase.amount)));
    }

    @Override
    public int getItemCount() {
        return dataBases.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.layout)
        LinearLayout layout;

        @BindView(R.id.date)
        TextView date;

        @BindView(R.id.name)
        TextView name;

        @BindView(R.id.value)
        TextView value;

        public ViewHolder(View view) {
            super(view);
            ButterKnife.bind(this, view);
        }
    }

}

Como podemos ver, acabamos com a violação retirando a formatação do campo “date” para o método Util.formateDate(intervalDataBase.date), que faz a o que precisamos sem violar o princípio.

Extra: um outro caso onde podemos verificar a violação desse princípio é quando utilizamos o padrão MVP (Model View Presenter) no nosso código e colocamos regras de negócio na nossa classe que implementa a nossa View, que é responsável apenas por mostrar os dados na tela e não executará qualquer regra de negócio, pois isso é responsabilidade do Presenter.

Obs: caso de dúvidas sobre MVP, acesse.

OCP: princípio aberto-fechado

Você deve ser capaz de estender um comportamento de uma classe, sem modificá-la

Como o título já nos diz, não podemos modicar o comportamento de um classe, mas sim estendê-la. Vamos utilizar um exemplo de cálculo de área de objetos diferentes.

Neste primeiro exemplo, vamos violar esse princípio:

public class Rectangle {
    private double length;
    private double height; 
    // getters/setters ... 
}

public class Circle {
    private double radius; 
    // getters/setters ...
}

public class AreaFactory {
    public double calculateArea(ArrayList<Object>... shapes) {
        double area = 0;
        for (Object shape : shapes) {
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle)shape;
                area += (rect.getLength() * rect.getHeight());                
            } else if (shape instanceof Circle) {
                Circle circle = (Circle)shape;
                area += 
                (circle.getRadius() * cirlce.getRadius() * Math.PI);
            } else {
                throw new RuntimeException("Shape not supported");
            }            
        }
        return area;
    }
}

Caso tenhamos a necessidade de adicionar novos objetos para calcular a área, temos que adicionar um bloco de código a nossa classe AreaFactory, violando o princípio, já que não podemos modificar a nossa classe para cada objeto novo que adicionarmos.

Para solucionar esse problema que acabamos de criar, temos que retirar essa lógica de adicionar a cada novo objeto um novo bloco de código, que deixe a nossa classe cada vez maior e confusa.

Segue a solução:

public interface Shape {
    double getArea(); 
}

public class Rectangle implements Shape{
    private double length;
    private double height; 
    // getters/setters ... 
    @Override
    public double getArea() {
        return (length * height);
    }
}

public class Circle implements Shape{
    private double radius; 
    // getters/setters ...
   @Override
    public double getArea() {
        return (radius * radius * Math.PI);
    }
}
public class AreaFactory {
    public double calculateArea(ArrayList<Shape>... shapes) {
        double area = 0;
        for (Shape shape : shapes) {
            area += shape.getArea();
        }
        return area;
    }
}

Criamos uma interface chamada Shape, que possui o método getArea() que é implementado em todos os nosso objetos (Circle e Rectangle).

Podemos adicionar novos objetos sem ter a necessidade de modificar a nossa AreaFactory para calcular o total da soma das áreas, simplificando o nosso código e não modificando a nossa classe principal.

Extra: um exemplo mais prático no Android desse princípio é a capacidade que temos de criar customViews sem modificar as Views de origem.

public class CustomEditText extends EditText {
    public CustomEditText(Context context) {
        super(context);

    }

    public CustomEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        Util.setCustomFont(this,context,attrs,
                R.styleable.CustomTextView,
                R.styleable.CustomTextView_font);
    }

    public CustomEditText(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        Util.setCustomFont(this,context,attrs,
                R.styleable.CustomTextView,
                R.styleable.CustomTextView_font);
    }

    public void setFont(String font) {
        Util.setCustomFont(this, getContext(), font);
    }
}

Nesse caso só estendemos a classe EditText para criar a nossa própria, onde nesse exemplo já podemos setar a font direto no arquivo xml.

<com.example.Widgets.CustomEditText
                        android:id="@+id/txtFirst"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        app:font="ScopeOne-Regular.ttf"/>

LSP: Princípio da substituição de Liskov

As classes derivadas devem ser substituíveis por suas classes base

No princípio da substituição de Liskov, as classes filhas nunca devem romper com as definições de tipos da classe pai.

No exemplo abaixo temos a violação, onde temos duas classes que implementam a classe pai Car que são respectivamente Ferrari e Tesla.

No método letStartEngine(), passamos o objeto Car para iniciar o carro, porém se o carro for Tesla e estiver descarregado, a chamada car.statEngine() não ira funcionar, ocasionando um bug no nosso código.

public interface Car {
  public void startEngine();
}

public Ferrari implements Car {
  ...
  @Override
  public double startEngine() {
         //logic ...
  }
}
=public Tesla implements Car{
  ...
  @Override
  public double startEngine() {
         if (!IsCharged)
            return;
       //logic ...
  }
}

public void letStartEngine(Car car) {
   car.startEngine();
}

Uma possível solução seria dentro do método letStartEngine verificar se nosso car é um Tesla.

Em seguida, chamamos o método car.TurnOnCar() para ligá-lo, caso ele tenha bateria.

Fazendo isso, acabamos violando o OCP, pois estamos modificando nossa classe para que ela possa se adaptar com os possíveis objetos, ou seja esta solução não serve, pois devemos manter intacta à nossa classe que executa nossa tarefa de ligar o carro.

Fazer o cast para verificar qual é a classe é um code smell também.

public void LetStartEngine(Car car) {
  if (car instanceof Tesla) 
       ((Tesla)car).TurnOnCar();  
   car.startEngine();
}

A solução para esse caso é colocar dentro da nossa classe Tesla a verificação se o carro está sem bateria, para depois ligá-lo. Assim, as duas classes filhas de Car retornam a mesma solução para o método statEngine().

public interface Car {
  public void startEngine();
}

public Ferrari implements Car {
  ...
  @Override
  public double startEngine() {
         //logic ...
  }
}

public Tesla implements Car{
  ...
  @Override
  public double startEngine() {
         if (!IsCharged)
            TurnOnCar();
       //logic ...
  }
}

public void letStartEngine(Car car) {
   car.startEngine();
}

ISP: Princípio da segregação da interface

Muitas interfaces específicas são melhores do que uma interface única

Nesse caso, vamos falar da implementação de varias interfaces que nem se quer utilizamos.

No caso da classe abaixo, OnClickListener, na sua utilização temos que implementar os 3 métodos no nosso click, sendo que não vamos usar todos os métodos.

Obs: A interface OnClickListener na prática não nos obriga a implementar os 3 métodos, mas para exemplificar, criamos a nossa própria, onde nos obriga, para fins de estudo.

public interface OnClickListener { 
    void onClick(View v);
    void onLongClick(View v); 
    void onTouch(View v, MotionEvent event);
}

A violação ocorre no código abaixo, onde precisamos somente do método onClick(), mas como utilizamos a nossa interface OnClickListener, somos obrigados a implementar onLongClick e onTouch sem nem mesmo utilizá-los.

Button valid = (Button)findViewById(R.id.valid);
valid.setOnClickListener(new View.OnClickListener {
    public void onClick(View v) {
       // TODO: do some stuff...
       
    }
    
    public void onLongClick(View v) {
        // we don't need to it
    }

    public void onTouch(View v, MotionEvent event) {
        // we don't need to it
    } 
});

A solução para manter esse princípio é separar cada método da nossa interface em diferentes interfaces, assim, só vamos implementar aquilo que realmente vamos utilizar, evitando o desperdício de código e mantendo o princípio na nossa implementação.

public interface OnClickListener { 
    void onClick(View v);
}
public interface OnLongClickListener { 
    void onLongClick(View v);
}
public interface OnTouchListener { 
    void onTouch(View v, MotionEvent event);
}

valid.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                // TODO: 
            }
});

DIP: Princípio da inversão da dependência

Dependa de uma abstração e não de uma implementação

No último princípio do SOLID, e não menos importante, temos a inversão de dependência como padrão, ou seja, classes de alto nível não podem depender de classes de baixo nível e, sim, ambas dependerem de uma abstração.

Vamos ver o exemplo da violação:

class Program {

	public void work() {
		// ....code
	}
}

class Engineer{

	Program program;

	public void setProgram(Program p) {
		program = p;
	}

	public void manage() {
		program.work();
	}
}

O problema com essa implementação é que a classe Engineer depende da classe Program.

Engineer é a nossa classe de alto nível e não deve adaptar-se a mudança de outras classes para continuar funcionando, ou seja, não depender de ninguém.

Program é de baixo nível, pois deve se adaptar a classe de alto nível.

Caso seja adicionado um novo SuperProgram, teríamos que mudar a lógica da nossa classe de alto nível Engineer e considerando que ela possa ser complexa, a mudança poderia afetar o nosso código e causar outros problemas. Para evitar que isso aconteça, invertemos as dependência das classes como no exemplo a seguir:

interface IProgram {
	public void work();
}

class Program implements IProgram{
	public void work() {
		// ....code
	}
}

class SuperProgram implements IProgram{
	public void work() {
		//....code
	}
}

class Engineer{
	IProgram program;

	public void setProgram(IProgram p) {
		program = p;
	}

	public void manage() {
		program.work();
	}
}

No código acima, adicionamos uma abstração que é a interface IProgram que nossos objetos a implementam, deixando a relação entre os objetos e a nossa classe de alto nível totalmente distinta.

Para retirar a dependência da classe Enginner, não foi preciso modificar nada no código, pois nossa abstração resolveu esse problema.

Conclusão

Sempre que podermos utilizar SOLID, estaremos ganhando tempo no futuro, para dar manutenção e realizar testes. Além de ser uma ótima oportunidade de aplicar algo novo no seu código, o que te tornara um programador melhor.

Por que escrevi esse artigo?

A ideia surgiu no lugar onde trabalho, para apresentar algo “novo” para o time de mobile e também para que eu pudesse melhorar meu código utilizando SOLID.

Então recomendo que estudem coisas novas, escrevam sobre elas e compartilhem.