Refatoração S.R.P. - ContactsDisplay
A class should have one, and only one, reason to change
Talvez as duas primeiras palavras que um instrutor diria em uma turma de redação seriam: Coesão e Coerência.
Coesão significa ligação. Uma coisa coesa é uma coisa focada em um determinado contexto e era justamente sobre isso que Tom DeMarco e Meilir Page-Jones estavam falando quando descreveram o Princípio da responsabilidade única.
Quanto mais coesa for uma classe, menor será seu acoplamento. Por outro lado, uma classe com mais do que uma responsabilidade terá maiores chances de sofrer com alguma modificação.
Mas para compreender realmente S.R.P. precisamos compreender o que é responsabilidade e, para isso, vamos dar uma olhada na Wikipédia:
Os motivos das ações de um indivíduo responsável devem fazer sentido e este deve fazer conhecer suas opiniões sem causar transtorno, ao resto da comunidade.
Com um pequeno ajuste, esse fragmento define praticamente tudo o que precisamos para compreender SRP:
Os motivos dos métodos de uma classe devem fazer sentido e esta deve fazer conhecer seus comportamentos sem causar transtorno ao resto da aplicação.
Ao contrário do que muitos pensam, uma classe coesa não significa que ela deva ter apenas um método público, mas que todos seus métodos públicos, independentemente da quantidade, tenham uma forte ligação com o seu propósito.
O código original:
<?php class ContactsDisplay { /** * @var array */ private $contacts = array(); public function __construct( $filename ) { $fh = fopen( $filename , 'r' ); while ( !feof( $fh ) ) { $data = fgetcsv( $fh , 1024 , ',' ); $this->contacts[] = array( 'email' => $data[ 0 ], 'name' => $data[ 1 ] ); } fclose( $fh ); } public function show() { foreach ( $this->contacts as $contact ) { echo '<dl>'; echo '<dt>' , $contact[ 'email' ] , '</dt>'; echo '<dd>' , $contact[ 'name' ] , '</dd>'; echo '</dl>'; } } }
O problema:
Como o nome sugere, ContactsDisplay tem como objetivo mostrar contatos, então, qual o problema dela ?
O problema, é que podemos identificar claramente 2 razões para ela possa ser modificada:
A origem dos dados mudou.
A forma de exibição mudou.
Basicamente ContactsDisplay recebe dados de uma origem e exibe esses dados, tudo estaria bem se a classe não assumisse para si a responsabilidade de manipular o arquivo.
Se tivermos que modificar a entrada, seja para um banco de dados, webservice ou qualquer que seja a nova origem, teremos que modificar a classe ContactsDisplay e, com isso, refazer testes mesmo em classes relacionadas com exibição, que não deveriam ser afetadas pela mudança na origem de dados.
O Teste:
<?php require_once 'ContactsDisplay.php'; require_once 'PHPUnit/Framework/TestCase.php'; /** * ContactsDisplay test case. */ class ContactsDisplayTest extends PHPUnit_Framework_TestCase { /** * @var ContactsDisplay */ private $ContactsDisplay; /** * Prepares the environment before running a test. */ protected function setUp() { parent::setUp(); /** * The file filecsv.csv should contain the following lines: * * [email protected],Fulano de tal * [email protected],Beltrano * [email protected],Exemplo */ $data = array(); $data[] = '[email protected],Fulano de tal'; $data[] = '[email protected],Beltrano'; $data[] = '[email protected],Exemplo'; file_put_contents( 'filecsv.csv' , implode( "\n" , $data ) ); $this->ContactsDisplay = new ContactsDisplay( 'filecsv.csv' ); } /** * Cleans up the environment after running a test. */ protected function tearDown() { $this->ContactsDisplay = null; parent::tearDown(); } /** * Tests ContactsDisplay->show() */ public function testShow() { ob_start(); $this->ContactsDisplay->show(); $expected = '<dl><dt>[email protected]</dt><dd>Fulano de tal</dd></dl>'; $expected .= '<dl><dt>[email protected]</dt><dd>Beltrano</dd></dl>'; $expected .= '<dl><dt>[email protected]</dt><dd>Exemplo</dd></dl>'; $actual = ob_get_contents(); ob_clean(); $this->assertEquals( $expected , $actual ); } }
Refatoração:
O primeiro passo na refatoração é a definição de uma entidade Contact:
<?php class Contact { private $email; private $name; public function __construct( $email , $name ) { $this->email = $email; $this->name = $name; } public function getEmail() { return $this->email; } public function getName() { return $this->name; } }
A criação da entidade Contact permitirá que a classe ContactsDisplay saiba trabalhar com os contatos independentemente da origem. Isso é importante porque a forma que os dados virão precisam ser conhecidos. Bancos de dados, arquivos CSV, webservices, cada fonte de dados pode entregar os contatos de uma forma diferente, porém a classe Contact permitirá que esses dados cheguem sempre da mesma forma para a ContactsDisplay.
Com isso, a ContactsDisplay ficará assim:
<?php require_once 'Contact.php'; class ContactsDisplay { /** * @var array */ private $contacts = array(); public function __construct( $filename ) { $fh = fopen( $filename, 'r' ); while ( !feof( $fh ) ) { $data = fgetcsv( $fh, 1024, ',' ); $this->contacts[] = new Contact( $data[ 0 ], $data[ 1 ] ); } fclose( $fh ); } public function show() { foreach ( $this->contacts as $contact ) { echo '<dl>'; echo '<dt>', $contact->getEmail(), '</dt>'; echo '<dd>', $contact->getName(), '</dd>'; echo '</dl>'; } } }
Executando novamente o teste, teremos sinal verde. Isso significa que nossa classe continua com o mesmo funcionamento de antes. Passamos para a próxima etapa, a separação da recuperação dos dados.
A separação dos dados se dará em duas partes:
Definição da ContactsReader.
Ajuste na ContactsDisplay para usar a ContactsReader.
<?php require_once 'Contact.php'; class ContactsReader { /** * @var string */ private $filename; public function __construct( $filename ) { $this->filename = $filename; } public function getContacts() { $fh = fopen( $this->filename, 'r' ); $contacts = array(); while ( !feof( $fh ) ) { $data = fgetcsv( $fh, 1024, ',' ); $contacts[] = new Contact( $data[ 0 ], $data[ 1 ] ); } fclose( $fh ); return $contacts; } }
<?php require_once 'ContactsReader.php'; class ContactsDisplay { /** * @var ContactsReader */ private $contactsReader; public function __construct( $filename ) { $this->contactsReader = new ContactsReader( $filename ); } public function show() { foreach ( $this->contactsReader->getContacts() as $contact ) { echo '<dl>'; echo '<dt>', $contact->getEmail(), '</dt>'; echo '<dd>', $contact->getName(), '</dd>'; echo '</dl>'; } } }
Novamente executamos o teste e novamente temos sinal verde, as modificações feitas não interferiram nem no funcionamento da classe, como também não precisamos fazer nenhum ajuste no uso dela.
Porém, criamos um novo problema: A classe ContactsDisplay tem a responsabilidade de criar a instância de ContactsReader, o que é muito ruim já que toda a refatoração iniciou-se justamente para que pudéssemos variar a origem.
Vamos remover o acoplamento passando a ContactsReader como parâmetro na construção da ContactsDisplay:
<?php require_once 'ContactsReader.php'; class ContactsDisplay { /** * @var ContactsReader */ private $contactsReader; public function __construct( ContactsReader $contactsReader ) { $this->contactsReader = $contactsReader; } public function show() { foreach ( $this->contactsReader->getContacts() as $contact ) { echo '<dl>'; echo '<dt>', $contact->getEmail(), '</dt>'; echo '<dd>', $contact->getName(), '</dd>'; echo '</dl>'; } } }
Vamos também ajustar o nosso teste para a nova realidade: (passar ContactsReader como parâmetro)
<?php require_once 'ContactsDisplay.php'; require_once 'PHPUnit/Framework/TestCase.php'; /** * ContactsDisplay test case. */ class ContactsDisplayTest extends PHPUnit_Framework_TestCase { /** * @var ContactsDisplay */ private $ContactsDisplay; /** * Prepares the environment before running a test. */ protected function setUp() { parent::setUp(); /** * The file filecsv.csv should contain the following lines: * * [email protected],Fulano de tal * [email protected],Beltrano * [email protected],Exemplo */ $data = array(); $data[] = '[email protected],Fulano de tal'; $data[] = '[email protected],Beltrano'; $data[] = '[email protected],Exemplo'; file_put_contents( 'filecsv.csv' , implode( "\n" , $data ) ); $this->ContactsDisplay = new ContactsDisplay( new ContactsReader( 'filecsv.csv' ) ); } /** * Cleans up the environment after running a test. */ protected function tearDown() { $this->ContactsDisplay = null; parent::tearDown(); } /** * Tests ContactsDisplay->show() */ public function testShow() { ob_start(); $this->ContactsDisplay->show(); $expected = '<dl><dt>[email protected]</dt><dd>Fulano de tal</dd></dl>'; $expected .= '<dl><dt>[email protected]</dt><dd>Beltrano</dd></dl>'; $expected .= '<dl><dt>[email protected]</dt><dd>Exemplo</dd></dl>'; $actual = ob_get_contents(); ob_clean(); $this->assertEquals( $expected , $actual ); } }
Executando o teste, teremos sinal verde novamente. Isso significa que o funcionamento da classe ContactsDisplay permanece como antes. Para finalizar a refatoração precisamos remover a dependência da classe concreta ContactsReader da ContactsDisplay.
Podemos fazer isso de uma forma bem simples criando uma interface chamada ContactsReader que possui o método de interface getContacts(). Após a criação dessa interface, modificamos o nome da classe que faz leitura de contatos atualmente para alguma coisa que indique o que ela faz, por exemplo: CSVContactsReader.
<?php interface ContactsReader { public function getContacts(); }
<?php require_once 'Contact.php'; require_once 'ContactsReader.php'; class CSVContactsReader implements ContactsReader { /** * @var string */ private $filename; public function __construct( $filename ) { $this->filename = $filename; } public function getContacts() { $fh = fopen( $this->filename, 'r' ); $contacts = array(); while ( !feof( $fh ) ) { $data = fgetcsv( $fh, 1024, ',' ); $contacts[] = new Contact( $data[ 0 ], $data[ 1 ] ); } fclose( $fh ); return $contacts; } }
Ajustando o teste para a nova realidade:
<?php require_once 'ContactsDisplay.php'; require_once 'CSVContactsReader.php'; require_once 'PHPUnit/Framework/TestCase.php'; /** * ContactsDisplay test case. */ class ContactsDisplayTest extends PHPUnit_Framework_TestCase { /** * @var ContactsDisplay */ private $ContactsDisplay; /** * Prepares the environment before running a test. */ protected function setUp() { parent::setUp(); /** * The file filecsv.csv should contain the following lines: * * [email protected],Fulano de tal * [email protected],Beltrano * [email protected],Exemplo */ $data = array(); $data[] = '[email protected],Fulano de tal'; $data[] = '[email protected],Beltrano'; $data[] = '[email protected],Exemplo'; file_put_contents( 'filecsv.csv' , implode( "\n" , $data ) ); $this->ContactsDisplay = new ContactsDisplay( new CSVContactsReader( 'filecsv.csv' ) ); } /** * Cleans up the environment after running a test. */ protected function tearDown() { $this->ContactsDisplay = null; parent::tearDown(); } /** * Tests ContactsDisplay->show() */ public function testShow() { ob_start(); $this->ContactsDisplay->show(); $expected = '<dl><dt>[email protected]</dt><dd>Fulano de tal</dd></dl>'; $expected .= '<dl><dt>[email protected]</dt><dd>Beltrano</dd></dl>'; $expected .= '<dl><dt>[email protected]</dt><dd>Exemplo</dd></dl>'; $actual = ob_get_contents(); ob_clean(); $this->assertEquals( $expected , $actual ); } }
Com o sinal verde na execução do teste, temos a classe ContactsDisplay refatorada, mantendo a funcionalidade anterior, mas agora sem excesso de responsabilidade: Finalizamos a refatoração da ContactsDisplay.
O código final:
<?php /** * Representação de um contato. */ class Contact { /** * @var string */ private $email; /** * @var string */ private $name; /** * Constroi uma nova instância da entidade Contact * @param string $email Email do contato * @param string $name Nome do contato */ public function __construct( $email, $name ) { $this->email = $email; $this->name = $name; } /** * Recupera o email do contato. * @return string */ public function getEmail() { return $this->email; } /** * Recupera o nome do contato. * @return string */ public function getName() { return $this->name; } }
<?php /** * Interface para definição de um leitor de contatos. */ interface ContactsReader { /** * Recupera uma lista de contatos de uma origem qualquer. * @return array Um array contendo instâncias de Contact */ public function getContacts(); }
<?php require_once 'Contact.php'; require_once 'ContactsReader.php'; /** * Implementação da interface ContactsReader para leitura de * contatos armazenados em arquivos CSV. */ class CSVContactsReader implements ContactsReader { /** * @var string */ private $filename; /** * Constroi o leitor de contatos de arquivos CSV passando o * nome do arquivo que contém esses contatos. * @param string $filename Nome do arquivo CSV que contém * os contatos que serão lidos. */ public function __construct( $filename ) { $this->filename = $filename; } /** * Recupera a lista de contatos. * @return array Lista de contatos que estão armazenadas no * arquivo CSV. * @see ContactsReader::getContacts() */ public function getContacts() { $fh = fopen( $this->filename, 'r' ); $contacts = array(); while ( !feof( $fh ) ) { $data = fgetcsv( $fh, 1024, ',' ); $contacts[] = new Contact( $data[ 0 ], $data[ 1 ] ); } fclose( $fh ); return $contacts; } }
<?php require_once 'ContactsReader.php'; /** * Classe responsável pela exibição de contatos */ class ContactsDisplay { /** * @var ContactsReader */ private $contactsReader; /** * Constroi a instância de ContactsDisplay para a exibição de * uma lista de contatos. * @param ContactsReader $contactsReader Instância de ContactsReader * responsável por recuperar os contatos de uma origem qualquer. */ public function __construct( ContactsReader $contactsReader ) { $this->contactsReader = $contactsReader; } /** * Exibe os contatos. */ public function show() { foreach ( $this->contactsReader->getContacts() as $contact ) { echo '<dl>'; echo '<dt>', $contact->getEmail(), '</dt>'; echo '<dd>', $contact->getName(), '</dd>'; echo '</dl>'; } } }
Consequências:
Do ponto de vista do princípio da responsabilidade única, responsabilidade é a razão pela qual uma classe deve ser modificada. Se tivermos mais do que uma razão para modificar uma classe então temos uma classe com mais do que uma responsabilidade.
Como podemos ver no código final, a responsabilidade pela recuperação dos dados foi removida da classe ContactsDisplay. Com essa remoção, passamos a poder variar a origem dos dados sem afetar a exibição deles.
Com a criação da interface ContactsReader temos a ContactsDisplay dependendo de uma abstração e poderemos ter contatos de qualquer origem sem que a exibição seja afetada.










