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.

O uso de códigos de terceiros

  • 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 do Map. Mas logo no início da lista dos métodos do Map está o método clear(). E assim qualquer usuário do Map 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 um Map de sensors da seguinte forma:
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.

Explorando e aprendendo sobre limites

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

Aprendendo sobre log4j

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 o ConsoleAppender. Então criamos o ConsoleAppender:
@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âmetro ConsoleAppender.SYSTEM_OUT e ainda é exibido “hello”. Mas quando retiramos o PatternLayout temos a mensagem de falta de fluxo de saída. Lendo a documentação novamente, vimos que o construtor ConsoleAppender 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.

Os testes de aprendizagem são melhores que de graça

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

O uso de código que não existe ainda

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

Limites limpos

  • 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