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

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

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
(2174)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloDomain-driven Design Destilado
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Neste artigo daremos continuidade à série Refatoração de Código, com o propósito de construirmos códigos de maior performance.

Desta 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.

Antes de prosseguir, não esqueça de se inscrever na ðŸ“« lista de e-mails do Blog para receber em primeira mão todos os conteúdos exclusivos sobre desenvolvimento e codificação limpa.

A seguir os tópicos que estaremos abordando em 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 GRÁFICA */
}
...
}

 

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 tínhamos: 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.

Então é isso.

Por fim, não deixe de se inscrever na 📩 lista de e-mails do Blog para receber os conteúdos de desenvolvimento e codificação limpa exclusivos, em primeira mão e também...

... na versão em PDF (versão liberada somente para os inscritos da lista de e-mails).

Abraço.

Outros artigos da série

A seguir a lista de todos os artigos aula já liberados desta série do Blog sobre Refatoração de Código:

Fonte

Refatoração para Padrões

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
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...