Refatoração de Código: Substituir Notificações Hard-Coded Por Observer

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Refatoração de Código: Substituir Notificações Hard-Coded Por Observer

Refatoração de Código: Substituir Notificações Hard-Coded Por Observer

Vinícius Thiengo
(1273)
Go-ahead
"Quando o passado chamá-lo, deixe ir para a caixa postal. Não tem nada de novo para lhe dizer."
Mandy Hale
Kotlin Android
Capa do livro Desenvolvedor Kotlin Android - Bibliotecas para o dia a dia
TítuloDesenvolvedor Kotlin Android - Bibliotecas para o dia a dia
CategoriasAndroid, Kotlin
AutorVinícius Thiengo
Edição
Capítulos19
Páginas1035
Acessar Livro
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas186
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas936
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Capítulos46
Páginas599
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, blz?

Nesse artigo continuamos com a série Refatoração de Código, com o propósito de construirmos códigos de maior performance. Dessa vez abordando o método de refatoração: Substituir Notificações Hard-Coded por Observer.

O que é Hard-Coded? 

Já vamos chegar nessa explicação.

Lembrando que todos os artigos dessa série são úteis para qualquer tipo de software, não somente Android. Mas todos no paradigma orientado a objetos.

Para o melhor entendimento do método de refatoração proposto aqui é preciso primeiro que você tenha conhecimento do padrão de projeto Observer.

Tópicos presentes no artigo:

Motivação

Você tem uma classe em que a instância dela notifica (notificadora) uma outra instância de uma outra classe (receptora). Essa notificação acontece sempre que há atualização no estado da instância notificadora, ou seja, sempre que algum atributo tem o valor alterado.

Por ter somente uma classe observadora nesse esquema, sua classe notificadora têm parte da implementação, dos métodos de notificação, com código específico da classe receptora. Esse código específico é também conhecido como Hard-Coded e, acredite, isso não é um bom sinal e é na evolução do projeto de software que o problema aparece.

Código Hard-Coded é o grande inimigo de ao menos um princípio de orientação a objetos: "Programe para interface e não para implementação".

Quando (e onde) enxergamos o problema em nossa implementação de classe notificadora e classe receptora?

Simples. Assuma que agora você descobre que precisará colocar mais uma instância como receptora de sua instância notificadora. E essa nova instância não é do mesmo tipo da primeira instância receptora já existente. Crash!

O código terá de ser muito atualizado caso não busque uma implementação mais genérica, uma solução já conhecida. O projeto será muito atualizado por causa do código específico presente, ele é pouco flexível e implica em mais atributos e métodos na classe notificadora. Esse é um clássico exemplo de Hard-Coded.

E note que código muito atualizado por causa de evoluções simples é quase sinônimo de "bug". O padrão Observer seria a implementação genérica e conhecida, nesse caso. Implementado pelo método Substituir Notificações Hard-Coded por Observer.

Código de exemplo

Nosso código de exemplo para esse post é um trecho do código do framework JUnit, mais precisamente um trecho da versão 2.x.

Nesse código vamos refatorar uma implementação onde temos as classes UITestResult e TextTestResult trabalhando como Parâmetros Coletores (saiba mais sobre no artigo: Mover Acumulação Para Parâmetro Coletor) de objetos de casos de teste, obtendo e relatando os dados. No esquema atual, somente as instâncias de UITestResult é que relatam os dados para instâncias de TestRunner, entidade responsável por imprimir o conteúdo na tela.

Segue códigos das classes citadas. Começando por UITestResult:

public class UiTestResult extends TestResult {
private TestRunner testRunner;

public UiTestResult( TestRunner testRunner ){
this.testRunner = testRunner;
}

public void addFailure( Test test, Throwable t ){
super.addFailure( test, t );
this.testRunner.addFailure( this, test, t ); /* NOTIFICANDO TEST RUNNER */
}

public void endTest( Test test ){
super.endTest(test);
this.testRunner.endTest( this, test ); /* NOTIFICANDO TEST RUNNER */
}
}

 

Então a classe TextTestResult:

public class TextTestResult extends TestResult {
public void startTest( Test test ){
super.startTest(test);
}

public void addError( Test test, Throwable t ){
super.addError( test, t );
System.out.print("E");
}

public void addFailure( Test test, Throwable t ){
super.addFailure( test, t );
System.out.print("F");
}
}

 

E por fim a atual classe ouvinte de UITestResult, TestRunner:

public class TestRunner extends Frame {
private TestResult testResult;

protected TestResult createTestResult(){
return( new UiTestResult( this ) );
}

public void runSuite(){
testResult = createTestResult();
testSuite.run( testResult );
}

public void startTest( TestResult testResult, Test test ){
runStartTest(testResult, test);
}

public void addFailure( TestResult testResult, Test test, Throwable t ){
/* APRESENTA A FALHA EM UMA TELA GRAFICA */
}
...
}

 

Com um tempo, os usuários do JUnit começaram a solicitar a possibilidade de terem mais classes receptoras além de somente TestRunner. As classes receptoras deles.

Mecânica

Nosso primeiro passo é mover todos os códigos que estão dentro de métodos notificadores, mas que não são específicos da lógica de notificação, ou seja, não trabalham com a tarefa: "enviar dados para instâncias de classes receptoras". Vamos mover esses códigos não notificadores para a classe receptora. Em nosso caso a classe receptora é a TestRunner.

Note que caso houvesse mais de uma classe como tipo receptor (código muito Hard-Coded) você moveria os métodos para ambas as classes. Logo, nesse início, você estaria criando código duplicado. Não há problemas, isso será corrigido, mais precisamente a partir do passo dois.

Antes de prosseguir com esse primeiro passo é importante ressaltar que nossa classe notificadora, TextTestResult, passará a utilizar também uma instância da classe TestRunner como instância receptora.

Por que isso?

Bom, nós temos as classes UITestResult e TextTestResult que são responsáveis por imprimirem dados, respectivamente de forma indireta e direta. A UITestResult imprimi os dados na tela via TestRunner. A TextTestResult imprimi os dados no console diretamente de seu código, sem instância receptora.

Colocando TestRunner como receptora de dados em TextTestResult podemos delegar a tarefa de imprimir somente para TestRunner, deixando o código de TextTestResult ainda mais limpo e bem dividido:

Como visto na implementação da classe TextTestResult, ela tem dois System.out.print() em métodos que serão notificadores. São eles: addError() e addFailure().

Vamos então fazer essas duas atualizações: colocar o TestRunner como entidade receptora de TextTestResult e mover os códigos System.out.print() para a classe TestRunner.

Segue TextTestResult atualizada:

public class TextTestResult extends TestResult {
private TestRunner testRunner;

public TextTestResult( TestRunner testRunner ){
this.testRunner = testRunner;
}

public void startTest( Test test ){
super.startTest( test );
this.testRunner.startTest( this, test ); /* NOTIFICANDO TEST RUNNER */
}

public void addError( Test test, Throwable t ){
super.addError( test, t );
this.testRunner.addError( this, test, t ); /* NOTIFICANDO TEST RUNNER */
}

public void addFailure( Test test, Throwable t ){
super.addFailure( test, t );
this.testRunner.addFailure( this, test, t ); /* NOTIFICANDO TEST RUNNER */
}
}

 

E então TestRunner:

public class TestRunner extends Frame{
...

/* MÉTODO ADICIONADO */
public void addError( TestResult testResult, Test test, Throwable t ){
System.out.print("E");
}
}

 

Os System.out que eram irrelevantes para o script de notificação em TextTestResult foram movidos para a entidade receptora TestRunner. Mais precisamente o System.out.print("E"), pois o System.out.print("F") não foi necessário, tendo em mente que esse método imprimi os dados na tela com classes do pacote AWT (isso no código do JUnit).

O startTest() também é um método notificador, mas esse não imprimia nada.

Estudando a classe UITestResult não encontramos esse problema de código irrelevante em método de notificação.

No passo dois temos de criar uma Interface que será nosso tipo observador.

Tipo observador?

Sim, nossas classes receptoras, em nosso caso TestRunner, deverão implementá-la e consequentemente no padrão Observer elas passam a se chamar observers.

Nessa Interface devemos colocar todos os métodos que são das classes receptoras e que são invocados nas instâncias das classes notificadoras.

Primeiro estudamos a classe TextTestResult. Então criamos a Interface com os métodos de TestRunner que são invocados nela:

public interface TestListener {
public void addError( TestResult testResult, Test test, Throwable t );
public void addFailure( TestResult testResult, Test test, Throwable t );
public void startTest( TestResult testResult, Test test );
}

 

Então partimos para o estudo da classe notificadora UITestResult, na qual descobrimos mais um método, endTest(). Logo atualizamos a Interface TestListener:

public interface TestListener {
public void addError( TestResult testResult, Test test, Throwable t );
public void addFailure( TestResult testResult, Test test, Throwable t );
public void startTest( TestResult testResult, Test test );
public void endTest( TestResult testResult, Test test ); /* ADICIONADO */
}

 

Nosso terceiro passo é fazer com que todas as classes receptoras implementem a interface TestListener. No caso temos somente a classe TestRunner como receptora:

public class TestRunner extends Frame implements TestListener{
...
}

 

Ainda no terceiro passo temos de atualizar as classes notificadoras para trabalharem somente com o tipo TestListener ao invés de TestRunner:

public class UiTestResult extends TestResult {
private TestListener testRunner;

public UiTestResult( TestListener testRunner ){
this.testRunner = testRunner;
}

...
}
public class TextTestResult extends TestResult {
private TestListener testRunner;

public TextTestResult( TestListener testRunner ){
this.testRunner = testRunner;
}

...
}

 

No quarto passo devemos obter todos os métodos notificadores de nossas classes notificadoras e então movê-los para a superclasse dessas classes notificadoras.

Note que se em seu código não houver uma superclasse comum as classes notificadoras, você deverá criá-la. A referência a Interface observadora, ou seja, nossa variável de instância do tipo TestListener, também deve ser movida para a superclasse.

Segue novo código de TestResult, a superclasse das entidades notificadoras de nosso exemplo:

public class TestResult {
protected TestListener testListener;
...

public TestResult(TestListener testListener){
this();
this.testListener = testListener;
}

public TestResult(){
failures = new Vector(10);
errors = new Vector(10);
runTests = 0;
stop = false;
}

public void addError( Test test, Throwable t ){
errors.addElement( new TestFailure(test, t) );
this.testRunner.addError( this, test, t ); /* NOTIFICANDO TEST LISTENER */
}

public void addFailure( Test test, Throwable t ){
failures.addElement( new TestFailure(test, t) );
this.testRunner.addFailure( this, test, t ); /* NOTIFICANDO TEST LISTENER */
}

public void startTest( Test test ){
runTests++;
this.testListener.startTest(this, test); /* NOTIFICANDO TEST LISTENER */
}

public void endTest( Test test ){
this.testListener.endTest(this, test); /* NOTIFICANDO TEST LISTENER */
}
...
}

 

Com a atualização do passo quatro nossas classes notificadoras UITestResult e TextTestResult ficaram vazias e a classe TestResult se tornou a entidade Subject do padrão Observer.

No passo cinco devemos fazer com que todos os códigos que trabalhavam com tipos específicos (Hard-Coded) de classes notificadoras dentro das classes receptoras, todos esses algoritmos devem começar a trabalhar com o tipo de nossa classe Subject, TestResult.

Em nosso exemplo há um método em TestRunner a ser atualizado. Segue:

public class TestRunner extends Frame implements TestListener{
...

protected TestResult createTestResult(){
return( new TestResult( this ) ); /* AQUI ERA new UITestResult() */
}
...
}

 

Ainda no quinto passo devemos destruir as antigas classes notificadoras: UITestResult e TextTestResult. Elas não mais serão úteis.

Nosso sexto e último passo é atualizar o código de nosso Subject, a classe TestResult, para que ele trabalhe com uma coleção de observadores e não somente um observador. Vamos criar uma variável de instância que seja uma coleção. Criaremos também um método para adicionar observadores a essa coleção:

public class TestResult {
protected List<TestListener> observers;
...

public void addObserver(TestListener testListener){
observers.add( testListener );
}

public TestResult(){
failures = new Vector(10);
errors = new Vector(10);
runTests = 0;
stop = false;
}

public void addError( Test test, Throwable t ){
errors.addElement( new TestFailure(test, t) );
for( TestListener testListener : observers ){ /* NOTIFICANDO TODOS OBSERVERS */
testListener.addError( this, test, t );
}
}

public void addFailure( Test test, Throwable t ){
failures.addElement( new TestFailure(test, t) );
for( TestListener testListener : observers ){ /* NOTIFICANDO TODOS OBSERVERS */
testListener.addFailure( this, test, t );
}
}

public void startTest( Test test ){
runTests++;
for( TestListener testListener : observers ){ /* NOTIFICANDO TODOS OBSERVERS */
testListener.startTest( this, test );
}
}

public void endTest( Test test ){
for( TestListener testListener : observers ){ /* NOTIFICANDO TODOS OBSERVERS */
testListener.endTest( this, test );
}
}

...
}

 

Note que o construtor que tinhamos: public TestResult( TestListener testListener ). Não era mais necessário, logo, o removemos.

Para finalizar devemos fazer com que as classes observadoras trabalhem com o novo método addObserver(). Em nosso caso a classe observadora é TestRunner:

public class TestRunner extends Frame implements TestListener{
private TestResult testResult;

protected TestResult createTestResult(){
TestResult testResult = new TestResult();
testResult.addObserver( this );
return( testResult );
}

...
}

 

Assim finalizamos nossa refatoração. Os developers clientes do JUnit provavelmente ficaram satisfeitos com a possibilidade de trabalhar como observadores proprietários somente implementando uma Interface comum, Observer.

Digo “… ficaram satisfeitos”, pois essa refatoração realmente ocorreu nesse framework de testes.

Conclusão

Como informado no início do artigo: o padrão Observer é um daqueles que uma hora você terá de utilizá-lo, ainda mais se seu código tem muitos listeners para eventos dos mais diversos.

Além das vantagens já mencionadas no artigo do padrão, ele é bem simples de entender e implementar e junto com o método de refatoração Substituir Notificadores Hard Coded Por Observer, você consegue configurá-lo em seu projeto orientado a objetos já existente e consequentemente obter algoritmos mais intuitivos.

Outros artigos da série

Segue lista dos artigos da série refatoração de código:

Internalizar Singleton

Mover Embelezamento Para Decorator

Substituir Condicionais que Alteram Estado por State

Introduzir Objeto Nulo

Unificar Interfaces Com Adapter

Extrair Adapter

Mover Conhecimento de Criação Para Factory

Substituir Código de Tipo Por Classe

Extrair Parâmetro

Unificar Interfaces

Limitar Instanciação Com Singleton

Mover Acumulação Para Parâmetro Coletor

Compor Method

Formar Template Method

Substituir Lógica Condicional Por Strategy

Introduzir Criação Polimórfica com Factory Method

Encapsular Classes Com Factory

Encadear Construtores

Substituir Construtores Por Métodos de Criação

Fontes

Refatoração para Padrões

Vlw.

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Padrões de Implementação - Um Catálogo de Padrões Indispensável Para o Dia a Dia do ProgramadorPadrões de Implementação - Um Catálogo de Padrões Indispensável Para o Dia a Dia do ProgramadorLivros
Persistência Com Firebase Android - Parte 1Persistência Com Firebase Android - Parte 1Android
Refatoração Para PadrõesRefatoração Para PadrõesLivros
Padrão de Projeto: ObserverPadrão de Projeto: ObserverAndroid

Compartilhar

Comentários Facebook

Comentários Blog

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...