22
Anotações Capítulo 8: Boundaries
- Sempre precisamos utilizar módulos, códigos fornecidos por terceiros.
- E devemos integrar de forma limpa esse código externo ao nosso.
Há uma tensão natural entre o fornecedor de uma interface e seu usuário.
Os fornecedores de pacotes (aqui no sentido de módulos) e frameworks de outros fabricantes visam a uma maior aplicabilidade de modo que possam trabalhar com diversos ambientes e atender a um público maior.
Já os usuários desejam uma interface voltada para suas próprias necessidades.
Essa tensão pode causar problemas nos limites de nossos sistemas.
-
Exemplo:
java.util.Map
, é uma interface bastante ampla com diversas capacidades. O que nos dá flexibilidade, isso é útil, mas também pode ser uma desvantagem:- Nosso aplicativo pode construir um
Map
e passá-lo adiante. - E nosso objetivo talvez seja que nenhum dos recipientes de nosso
Map
não exclua nada doMap
. Mas logo no início da lista dos métodos doMap
está o métodoclear()
. E assim qualquer usuário doMap
pode apagá-lo. - Ou talvez, segundo a convenção que adotamos, o
Map
pode armazenar apenas certos tipos de objetos, mas ele não restringe os tipos que podemos armazenar, assim qualquer usuário pode adicionar itens de qualquer tipo. - Exemplo
sensors
: Se temos umMap
desensors
da seguinte forma:
- Nosso aplicativo pode construir um
Map sensors = new HashMap();
E quando alguma parte do código precisar acessar o Sensor, terá que fazer:
Sensor s = (Sensor) sensors.get(sensorId);
E isso aparece várias vezes ao longo do código. E com isso o cliente fica com a responsabilidade de obter um Object
do Map
e atribuí-lo ao tipo certo. Apesar de funcionar, não é um código limpo. E esse código não explica muito bem o que ele faz.
E poderíamos melhorar a legibilidade com o uso de tipos genéricos, como:
Map<Sensor> sensors = new HashMap<Sensor>();
…
Sensor s = sensors.get(sensorId );
Observação: O Map
deveria ter a chave e o tipo do valor.
Porém, isso ainda não resolve o problema de que o Map<Sensor>
oferece mais capacidade do que precisamos ou queremos.
Passar adiante pelo sistema uma instância de Map<Sensor>
significa que haverá vários lugares para mexer se a interface Map
mudar.
Uma forma limpa de usar o Map
:
public class Sensors {
private Map sensors = new HashMap();
public Sensor getById(String id) {
return (Sensor) sensors.get(id);
}
//snip
}
Dessa forma a interface no limite (Map) está oculta, e é possível alterá-la causando muito pouco impacto no resto do aplicativo. Essa classe também pode forçar regras de modelo e de negócios.
Não estamos sugerindo para sempre usar dessa forma.
Mas é bom não passar os
Maps
ou qualquer outra interface num limite por todo o sistema.Se for usar, mantenha numa classe ou próxima a uma família de classes em que ela possa ser usada. Evite retorná-la ou aceitá-la como parâmetro de APIs públicas.
Códigos de terceiros nos ajudam a obter mais funcionalidade em menos tempo.
Não é tarefa nossa testá-los, mas pode ser melhor para nós criar testes para os códigos externos que formos usar.
Isso quando não é claro como usar uma biblioteca de terceiros. Podemos gastar um tempo lendo a documentação e decidindo como usá-la. E então escreveremos nosso código para usar o código externo e vemos que não é o que achávamos. E poderíamos passar muito tempo depurando o código e tentando saber se o bug é no nosso código ou no deles.
Entender códigos de terceiros é difícil. Integrá-lo ao seu também é. Fazer ambos ao mesmo tempo dobra a dificuldade, e se adotássemos outra abordagem?
Se criarmos testes para explorar nosso conhecimento sobre ele. O que o Jim Newkirk chama isso de testes de aprendizagem.
Nesses testes, chamamos a API do código externo como faríamos ao usá-la em nosso aplicativo. Assim estaríamos controlando os experimentos que verificam nosso conhecimento daquela API.
O teste foca no que desejamos saber sobre a API.
Para usar o pacote log4j
do Apache, baixaremos o pacote e abriremos a documentação inicial.
- Sem ler muito, criamos nosso primeiro teste:
@Test
public void testLogCreate() {
Logger logger = Logger.getLogger("MyLogger");
logger.info("hello");
}
- Quando executamos, temos um erro dizendo que precisamos de algo chamado
Appender
. Após ler um pouco mais, descobrimos oConsoleAppender
. Então criamos oConsoleAppender
:
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger("MyLogger");
ConsoleAppender appender = new ConsoleAppender();
logger.addAppender(appender);
logger.info("hello");
}
- Agora descobrimos que o
Appender
não possui fluxo de saída. Depois de pesquisar tentamos o seguinte:
@Test
public void testLogAddAppender() {
Logger logger = Logger.getLogger("MyLogger");
logger.removeAllAppenders();
logger.addAppender(new ConsoleAppender(
new PatternLayout("%p %t %m%n"), ConsoleAppender.SYSTEM_OUT));
logger.info("hello");
}
- E funcionou. Uma mensagem “hello” apareceu no console! Porém:
Parece estranho ter que dizer ao
ConsoleAppender
o que ele precisa escrever no console, e mais interessante é quando removemos o parâmetroConsoleAppender.SYSTEM_OUT
e ainda é exibido “hello”. Mas quando retiramos oPatternLayout
temos a mensagem de falta de fluxo de saída. Lendo a documentação novamente, vimos que o construtorConsoleAppender
padrão vem “desconfigurado”, o que não parece óbvio ou prático. Parece um bug. Lendo mais detalhadamente sobre o pacote chegamos a seguinte série de testes de unidade:
public class LogTest {
private Logger logger;
@Before
public void initialize() {
logger = Logger.getLogger("logger");
logger.removeAllAppenders();
Logger.getRootLogger().removeAllAppenders();
}
@Test
public void basicLogger() {
BasicConfigurator.configure();
logger.info("basicLogger");
}
@Test
public void addAppenderWithStream() {
logger.addAppender(new ConsoleAppender(new PatternLayout("%p %t %m%n"), ConsoleAppender.SYSTEM_OUT));
logger.info("addAppenderWithStream");
}
@Test
public void addAppenderWithoutStream() {
logger.addAppender(new ConsoleAppender(new PatternLayout("%p %t %m%n")));
logger.info("addAppenderWithoutStream");
}
}
Agora sabemos como obter um console simples e inicializado de logs, com isso podemos encapsular essa lógica nas classes de logs para que o resto do código fique isolado da interface limite do log4j
.
Esses testes acabam não custando nada. Porque tivemos que aprender sobre a API mesmo, e escrever eles foi uma forma fácil de aprender.
“Os testes de aprendizagem são experimentos precisos que ajudam a aumentar nosso entendimento”.
Esses testes também geram um retorno positivo. Quando houver novas versões daquele pacote, podemos executar os testes para ver se há diferenças nas atividades.
Cada distribuição vem com um novo risco. Porém com os testes de aprendizagem podemos saber se o pacote ficou incompatível com os testes.
Também pode ser mais fácil a atualização com os testes, assim podemos ir para novas versões sem grandes problemas desde que os testes continuem passando.
- Outro tipo de limite, que separa o conhecido do desconhecido.
- Às vezes, o que está do outro lado nos é desconhecido. Às vezes, optamos por não olhar além do limite.
- Pense num caso onde você vai ter que utilizar uma API externa mas que você não conhece ela e não sabe como ela funciona:
- Digamos que você usará um subsistema chamado “Transmissor”.
- Nesse caso a melhor abordagem é criar nossa própria interface “Transmitter” que será responsável por se comunicar com o subsistema "Transmissor".
- Nela podemos criar um método “transmit” que pega a frequência e o fluxo de dados. Nesse caso, essa é a interface que gostaríamos que o subsistema "Transmissor" tivesse, que não sabemos como realmente é a sua interface.
- Assim deixamos o código do nosso lado organizado e centralizado.
- E podemos até utilizar testes com um “Transmissor fake” para testarmos o funcionamento do nosso lado.
- Bem como usar “Adapter Transmitter” para fazer a comunicação no futuro com a API externa.
- Coisas interessantes ocorrem nos limites. A alteração é uma delas.
- Bons projetos de software acomodam modificações sem muito investimento ou trabalho.
- Quando usamos códigos que estão fora de controle, devemos dar uma atenção ao nosso investimento e garantir que uma mudança futura não seja muito custosa.
- O código nos limites precisa de uma divisão clara e testes que definem o que se deve esperar. Evitar que grande parte do nosso código enxergue as particularidades de terceiros.
- Melhor depender de algo que você controle do que pegar algo que acabe controlando você.
- Lidamos com os limites de códigos externos através de alguns poucos lugares em nosso código que fazem referência a eles.
- Podemos usar uma interface, ou um adapter para converter nossa interface perfeita na que nos for fornecida.
- Nosso código se comunica melhor conosco, provê uso consistente interno e possui poucos pontos para serem mexidos quando o código externo mudar.
22