Refatoração de Código: Substituir Notificações Hard-Coded Por Observer
(2174)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
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:
- 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.
Comentários Facebook