Há uma dica na comunidade do TDD que diz
que não devemos usar objetos simulados para tipos que não possuímos . Penso que este é um bom conselho e tento segui-lo. Claro, existem pessoas que dizem que não devemos usar objetos simulados. Independentemente de sua opinião, o conselho de "não imitar o que não é seu" também contém um significado oculto. As pessoas geralmente passam por ele, vendo a palavra "zombar" e se enfurecendo.
Esse significado oculto é que você deve criar interfaces, clientes, pontes, adaptadores entre nosso aplicativo e o código de terceiros que usamos. Se criaremos objetos simulados dessas interfaces em nossos testes não é tão importante. O importante é que criamos e usamos interfaces que separam melhor nosso código do código de terceiros. Um exemplo clássico disso no mundo PHP é criar e usar um cliente HTTP em nosso aplicativo que usa o
cliente HTTP Guzzle , em vez de usar o Guzzle diretamente.
Porque Bem, para iniciantes, o Guzzle possui uma API muito mais poderosa do que a que seu aplicativo precisa (na maioria dos casos). Criar seu próprio cliente HTTP, que fornece apenas o conjunto necessário de APIs do Guzzle, limitará os desenvolvedores de aplicativos ao que eles podem fazer com esse cliente. Se a API do Guzzle mudar no futuro, precisaremos fazer alterações em um só lugar, em vez de corrigir suas chamadas em todo o aplicativo, na esperança de que nada ocorra. Duas boas razões, e eu nem mencionei objetos simulados!
Eu não acho que isso seja difícil de alcançar. O código de terceiros geralmente fica em uma pasta separada do nosso aplicativo, geralmente é
vendor/
ou
library/
. Ele também reside em um espaço para nome diferente e possui uma convenção de nomenclatura diferente daquela usada em nosso aplicativo. O código de terceiros é bastante fácil de identificar e, com um pouco de disciplina, podemos tornar nosso código de aplicativo menos dependente de peças de terceiros.
E se aplicarmos as mesmas regras ao código herdado?
E se olharmos para o nosso código legado e também para terceiros? Isso pode ser difícil, ou até contraproducente se o código desatualizado for usado exclusivamente no modo de suporte, quando apenas corrigimos bugs e ajustamos um pouco suas pequenas partes. Mas se escrevermos um novo código que (re) use desatualizado, acredito que vale a pena considerá-lo da mesma maneira que o código de terceiros. Pelo menos do ponto de vista do novo código.
Se possível, códigos desatualizados e novos devem estar localizados em diferentes pastas e espaços para nome. Já faz muito tempo desde a última vez que vi o sistema
sem inicialização, então isso é bastante factível. Mas, em vez de usar cegamente o código legado no novo código, e se criarmos interfaces para ele e usá-las?
Código desatualizado geralmente está cheio de objetos "divinos" que fazem muitas coisas. Eles usam um estado global, têm propriedades públicas ou métodos mágicos que dão acesso a propriedades privadas como se fossem públicas, têm métodos estáticos que são
muito convenientes para ligar
para qualquer pessoa e em qualquer lugar. Portanto, essa conveniência nos levou à situação em que estamos.
Outro problema, talvez ainda mais sério, com código desatualizado é que estamos prontos para alterá-lo, corrigi-lo e decifrá-lo porque não o consideramos como código de terceiros. O que fazemos quando vemos um bug ou queremos adicionar um novo recurso ao código de terceiros? Descrevemos o problema e / ou criamos uma solicitação de recebimento. O que
não fazemos não é ir para o
vendor/
pasta e não editar o código lá. Por que estamos fazendo isso com código legado? E então cruzamos os dedos e esperamos que nada tenha quebrado.
Em vez de usar cegamente código obsoleto no novo código, vamos tentar escrever interfaces que incluirão apenas o subconjunto necessário da API do antigo objeto "divino". Digamos que tenhamos um objeto
User
no código legado que saiba tudo sobre tudo. Ele sabe como alterar e-mail e senha, como atualizar usuários do fórum para moderadores, como atualizar perfis de usuários públicos, definir preferências de notificação, salvar-se no banco de dados e muito mais.
src / Legacy / User.php <?php namespace Legacy; class User { public $email; public $password; public $role; public $name; public function promote($newRole) { $this->role = $newRole; } public function save() { db_layer::save($this); } }
Este é um exemplo grosseiro, mas reflete o problema: cada propriedade é pública e pode ser facilmente alterada para qualquer valor, precisamos lembrar de chamar explicitamente o método
save
após qualquer alteração para salvar, etc.
Vamos nos limitar e proibir o acesso a essas propriedades públicas e tentar adivinhar como um sistema desatualizado funciona enquanto aumenta os direitos do usuário:
src / LegacyBridge / Promoter.php <?php namespace LegacyBridge; interface Promoter { public function promoteTo(Role $role); }
src / LegacyBridge / LegacyUserPromoter.php <?php namespace LegacyBridge; class LegacyUserPromoter implements Promoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function promoteTo(Role $newRole) { $newRole = (string) $newRole;
Agora, quando queremos aumentar os direitos do
User
no novo código, usamos a interface
LegacyBridge\Promoter
, que lida com todas as sutilezas de aumentar um usuário em um sistema desatualizado.
Alterar idioma de herança
A interface para código desatualizado nos dá a oportunidade de melhorar o design do sistema e pode nos salvar de possíveis erros de nomenclatura cometidos há muito tempo. O processo de mudar a função de um usuário de um moderador para um participante não é "promoção" (aumento), mas sim "rebaixamento" (diminuição). Ninguém está nos impedindo de criar duas interfaces para essas coisas diferentes, mesmo que o código legado pareça o mesmo.
src / LegacyBridge / Promoter.php <?php namespace LegacyBridge; interface Promoter { public function promoteTo(Role $role); }
src / LegacyBridge / LegacyUserPromoter.php <?php namespace LegacyBridge; class LegacyUserPromoter implements Promoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function promoteTo(Role $newRole) { if ($newRole->isMember()) { throw new \Exception("Can't promote to a member."); } $legacyMemberRole = 2; $this->legacyUser->promote($legacyMemberRole); $this->legacyUser->save(); } }
src / LegacyBridge / Demoter.php <?php namespace LegacyBridge; interface Demoter { public function demoteTo(Role $role); }
src / LegacyBridge / LegacyUserDemoter.php <?php namespace LegacyBridge; class LegacyUserDemoter implements Demoter { private $legacyUser; public function __construct(Legacy\User $user) { $this->legacyUser = $user; } public function demoteTo(Role $newRole) { if ($newRole->isModerator()) { throw new \Exception("Can't demote to a moderator."); } $legacyModeratorRole = 1; $this->legacyUser->promote($legacyModeratorRole); $this->legacyUser->save(); } }
Não é uma mudança tão grande, mas o objetivo do código se tornou muito mais claro.
Agora, da próxima vez que você precisar chamar alguns métodos de código desatualizado, tente criar uma interface para eles. Pode ser impossível, pode ser muito caro. Eu sei que o método estático desse objeto "divino" é realmente muito simples de usar e, com sua ajuda, você pode fazer o trabalho muito mais rapidamente, mas pelo menos considere esta opção. Você pode melhorar um pouco o design do novo sistema que está criando.