Passo a passo para desenvolver uma aplicação usando o Spring Framework MVC – Capitulo 3 – Desenvolvendo a lógica de negócio

Essa é a terceira parte do tutorial de como desenvolver uma aplicação com Spring. Nessa parte, iremos adotar uma abordagem pragmática do Desenvolvimento Orientado ao Teste (TDD) para a criação dos objetos do domínio e implementar a lógica de negócio para nosso sistema de gerenciamente de inventário. Isso significa que iremos codificar um pouco, testar um pouco, codificar um pouco mais e testar mais. Na Parte 1, configuramos o ambiente e uma aplicação básica. Na Parte 2, refinamos a aplicação pela desacoplação das visões do controlador.

O Spring tem como principal objetivo fazer coisas simples de forma fácil, e coisas difíceis possíveis. O item fundamental que torna isso possível é o uso pelo Spring dos POJOs (Plain Old Java Objects). Os POJOs são essencialmente classes antigas do Java que são livres de qualquer contrato que normalmente são forçadas por um framework ou arquitetura através de sub-classes ou implementação de interfaces. Os POJOs são objetos livres de qualquer amarras, tornando a programação orientada à objeto possível novamente. Quando estiver trabalhando com o Spring, os objetos do domínio e os serviços que você implementa serão POJOs. De fato, quase tudo que você implementa deve ser um POJO. Se não é, você deve certifica-se de perguntar a você mesmo por quê não é. Nessa seção, começaremos a ver a simplicidade e o poder do Spring.

3.1. Revisão do caso de negócio do Sistema de gerenciamento de Inventário

No nosso sistema de gerenciamento de inventário, temos o conceito de um produto e um serviço que manipula-o. Em particular, o negócio pede a habilidade de aumentar os preços por todos os produtos. Qualquer diminuição deverá ser feito por produto, individualmente, mas esse comportamento está fora do escopo dessa aplicação. As regras de validação para aumentos de preços são:

  • O aumento máximo é limitado a 50%.
  • O aumento minimo precisa ser maior que 0%.

Abaixo está o diagrama de classe de nosso sistema de inventário.

3.2. Adicionando algumas classes para a lógica de negócio

Vamos agora adicionar alguma lógica de negócio na forma de uma classe Product e um serviço chamado ProductManager que gerenciará todos os produtos. Para que possamos separar a lógica dependente da web da lógica de negócios, colocaremos as classes relacionadas a web no pacote ‘web’ e criaremos dois novos pacotes: um para objetos de serviço chamado ‘service’ e outro para objetos de domínio chamado ‘domain’.

Primeiro implementaremos a classe Product como um POJO com um construtor padrão (automaticamente fornecido se não especificarmos um construtor) e getter e setters para suas propriedades ‘description’ e ‘price’. Vamos também tornar essa classe ‘Serializable’, não necessariamente para nossa aplicação, maspode vir a ser útil mais tarde quando precisarmos armazenar o seu estado. A classe é um objeto de domínio, então pertence ao pacote ‘domain’.

'springapp/src/springapp/domain/Product.java':

package springapp.domain;

import java.io.Serializable;

public class Product implements Serializable {

    private String description;
    private Double price;

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public String toString() {
        StringBuffer buffer = new StringBuffer();
        buffer.append("Description: " + description + ";");
        buffer.append("Price: " + price);
        return buffer.toString();
    }
}

Agora vamos escrever as unidades de teste para nossa classe Product. Alguns desenvolvedores não se incomodam em escrever testes para métodos getter e setter ou código auto-gerado. Normalmente isso toma muito tempo em debates (como esse páragrafo demonstra) em definir se os getter e setter precisam ter unidades de teste já que eles são bastante comuns. Nós vamos escrever essas unidades porque: a) São triviais de escrever; b) Ter testes é útil para não perder tempo uma vez ou outra, quando se tem um método getter ou setter mais difícil; e c) eles reforçam a cobertura dos testes. Nós criamos um Product e testamos cada getter e setter como um par em um único teste. Normalmente, você escreverá um ou mais métodos de teste por método de classe, como cada método testando um condição particular de um método da classe como a checagem por um valor null de um argumento passado para o método.

'springapp/test/springapp/domain/ProductTests.java':

package springapp.domain;

import junit.framework.TestCase;

public class ProductTests extends TestCase {

    private Product product;

    protected void setUp() throws Exception {
        product = new Product();
    }

    public void testSetAndGetDescription() {
        String testDescription = "aDescription";
        assertNull(product.getDescription());
        product.setDescription(testDescription);
        assertEquals(testDescription, product.getDescription());
    }

    public void testSetAndGetPrice() {
        double testPrice = 100.00;
        assertEquals(0, 0, 0);
        product.setPrice(testPrice);
        assertEquals(testPrice, product.getPrice(), 0);
    }

}

Em seguida, criamos o ProductManager. Esse é o serviço responsável por manipular os produtos. Contém dois métodos: um método de negócio increasePrice() que aumenta o preço de todos os produtos e um método getter getProducts() para recuperar todos os produtos. Nós vamos implementar esse serviço como uma interface ao invés de uma classe concreta por várias razões. Em primeiro lugar, torna a escrita de unidades de teste para o Controlador mais fácil (como veremos no próximo capítulo). Em segundo lugar, o uso de interfaces significa que o recurso de Proxying do JDK (uma característica de Java) pode ser usado para tornar o serviço transacional ao invés do CGLIB (uma biblioteca de geração de código).

'springapp/src/springapp/service/ProductManager.java':

package springapp.service;

import java.io.Serializable;
import java.util.List;

import springapp.domain.Product;

public interface ProductManager extends Serializable{

    public void increasePrice(int percentage);

    public List<Product> getProducts();

}

Vamos criar a classe SimpleProductManager que implementa a interface ProductManager.

'springapp/src/springapp/service/SimpleProductManager.java':

package springapp.service;

import java.util.List;

import springapp.domain.Product;

public class SimpleProductManager implements ProductManager {

    public List<Product> getProducts() {
        throw new UnsupportedOperationException();
    }

    public void increasePrice(int percentage) {
        throw new UnsupportedOperationException();
    }

    public void setProducts(List<Product> products) {
        throw new UnsupportedOperationException();
    }

}

Antes de implementar os métodos de SimpleProductManager, iremos definir alguns testes primeiro. A definição estrita de TDD (Desenvolvimento direcionado aos Testes) é sempre escrever os testes primeiro, depois o código. Uma interpretação mais moderada disso é mais parecida com o TOD – Desenvolvimento Orientado aos Testes, onde nós alternamos entre escrever códigos e escrever testes como parte do processo de desenvolvimento. A coisa mais importante para um base de código é ter um conjunto de unidade de testes o mais completa possível, de modo que o você consegue torne-se de alguma forma acadêmico. Muitos desenvolvedores TDD, todavia, concordam que a qualidade dos testes é sempre maior quando eles são escritos por volta do mesmo tempo em que são escritos os códigos são desenvolvidos, que é a abordagem que nós iremos usar.

Para escrever testes efetivos, você tem que considerar todas as pré e pós condições possíveis de um método que esteja sendo testado assim como o que acontece dentro do método. Vamos começar pelo teste para quando o getProduct() retorna null.

'springapp/test/springapp/service/SimpleProductManagerTests.java':

package springapp.service;

import junit.framework.TestCase;

public class SimpleProductManagerTests extends TestCase {

    private SimpleProductManager productManager;

    protected void setUp() throws Exception {
        productManager = new SimpleProductManager();
    }

    public void testGetProductsWithNoProducts() {
        productManager = new SimpleProductManager();
        assertNull(productManager.getProducts());
    }

}

Re-execute todos os testes do Ant e o teste deve resultar em erro por getProduct() ainda ter que ser implementado. Normalmente é uma boa idéia marcar métodos não implementados para que emitam uma exceção UnsupportedOperationException.

Em seguida implementamos um teste para recuperar a lista de produtos dos dados de teste. Sabemos que precisaremos popular a lista de produtos na maioria de nossos testes em SimpleProductManager, assim definimos a lista em setUp(), do JUnit, um método que é invocado antes de que cada método de teste seja chamado.

'springapp/test/springapp/service/SimpleProductManagerTests.java':

package springapp.service;

import java.util.ArrayList;
import java.util.List;

import springapp.domain.Product;

import junit.framework.TestCase;

public class SimpleProductManagerTests extends TestCase {

    private SimpleProductManager productManager;
    private List<Product> products;

    private static int PRODUCT_COUNT = 2;

    private static Double CHAIR_PRICE = new Double(20.50);
    private static String CHAIR_DESCRIPTION = "Chair";

    private static String TABLE_DESCRIPTION = "Table";
    private static Double TABLE_PRICE = new Double(150.10);

    protected void setUp() throws Exception {
        productManager = new SimpleProductManager();
        products = new ArrayList<Product>();

        // stub up a list of products
        Product product = new Product();
        product.setDescription("Chair");
        product.setPrice(CHAIR_PRICE);
        products.add(product);

        product = new Product();
        product.setDescription("Table");
        product.setPrice(TABLE_PRICE);
        products.add(product);

        productManager.setProducts(products);
    }

    public void testGetProductsWithNoProducts() {
        productManager = new SimpleProductManager();
        assertNull(productManager.getProducts());
    }

    public void testGetProducts() {
        List<Product> products = productManager.getProducts();
        assertNotNull(products);
        assertEquals(PRODUCT_COUNT, productManager.getProducts().size());

        Product product = products.get(0);
        assertEquals(CHAIR_DESCRIPTION, product.getDescription());
        assertEquals(CHAIR_PRICE, product.getPrice());

        product = products.get(1);
        assertEquals(TABLE_DESCRIPTION, product.getDescription());
        assertEquals(TABLE_PRICE, product.getPrice());
    }
}

Re-execute todos os teste do Ant e nosso dois teste resultarão em erro.

Voltando a SimpleProductManager, vamos implementar os métodos getter e setter para a propriedade products.

'springapp/src/springapp/service/SimpleProductManager.java':

package springapp.service;

import java.util.ArrayList;
import java.util.List;

import springapp.domain.Product;

public class SimpleProductManager implements ProductManager {

    private List<Product> products;

    public List<Product> getProducts() {
        return products;
    }

    public void increasePrice(int percentage) {
        // TODO Auto-generated method stub
    }

    public void setProducts(List<Product> products) {
        this.products = products;
    }

}

Re-execute os teste do Ant e agora todos os teste passarão.

Agora vamos implementar os seguintes testes para o método increasePrice():

  • A lista de produtos é nula e o método executa normalmente.
  • A lista de produtos está vazia e o método executa normalmente.
  • Comandar um aumento de preço de 10% e checar se o aumento é refletido em todos os produtos da lista.

'springapp/test/springapp/service/SimpleProductManagerTests.java':

package springapp.service;

import java.util.ArrayList;
import java.util.List;

import springapp.domain.Product;

import junit.framework.TestCase;

public class SimpleProductManagerTests extends TestCase {

    private SimpleProductManager productManager;

    private List<Product> products;

    private static int PRODUCT_COUNT = 2;

    private static Double CHAIR_PRICE = new Double(20.50);
    private static String CHAIR_DESCRIPTION = "Chair";

    private static String TABLE_DESCRIPTION = "Table";
    private static Double TABLE_PRICE = new Double(150.10);         

    private static int POSITIVE_PRICE_INCREASE = 10;

    protected void setUp() throws Exception {
        productManager = new SimpleProductManager();
        products = new ArrayList<Product>();

        // stub up a list of products
        Product product = new Product();
        product.setDescription("Chair");
        product.setPrice(CHAIR_PRICE);
        products.add(product);

        product = new Product();
        product.setDescription("Table");
        product.setPrice(TABLE_PRICE);
        products.add(product);

        productManager.setProducts(products);
    }

    public void testGetProductsWithNoProducts() {
        productManager = new SimpleProductManager();
        assertNull(productManager.getProducts());
    }

    public void testGetProducts() {
        List<Product> products = productManager.getProducts();
        assertNotNull(products);
        assertEquals(PRODUCT_COUNT, productManager.getProducts().size());

        Product product = products.get(0);
        assertEquals(CHAIR_DESCRIPTION, product.getDescription());
        assertEquals(CHAIR_PRICE, product.getPrice());

        product = products.get(1);
        assertEquals(TABLE_DESCRIPTION, product.getDescription());
        assertEquals(TABLE_PRICE, product.getPrice());
    }   

    public void testIncreasePriceWithNullListOfProducts() {
        try {
            productManager = new SimpleProductManager();
            productManager.increasePrice(POSITIVE_PRICE_INCREASE);
        }
        catch(NullPointerException ex) {
            fail("Products list is null.");
        }
    }

    public void testIncreasePriceWithEmptyListOfProducts() {
        try {
            productManager = new SimpleProductManager();
            productManager.setProducts(new ArrayList<Product>());
            productManager.increasePrice(POSITIVE_PRICE_INCREASE);
        }
        catch(Exception ex) {
            fail("Products list is empty.");
        }
    }

    public void testIncreasePriceWithPositivePercentage() {
        productManager.increasePrice(POSITIVE_PRICE_INCREASE);
        double expectedChairPriceWithIncrease = 22.55;
        double expectedTablePriceWithIncrease = 165.11;

        List<Product> products = productManager.getProducts();
        Product product = products.get(0);
        assertEquals(expectedChairPriceWithIncrease, product.getPrice());

        product = products.get(1);
        assertEquals(expectedTablePriceWithIncrease, product.getPrice());
    }
    
}

Retornamos agora para SimpleProductManaget para implementar increaasePrice().

'springapp/src/springapp/service/SimpleProductManager.java':

package springapp.service;

import java.util.List;

import springapp.domain.Product;

public class SimpleProductManager implements ProductManager {

    private List<Product> products;

    public List<Product> getProducts() {
        return products;
    }

    public void increasePrice(int percentage) {
        if (products != null) {
            for (Product product : products) {
                double newPrice = product.getPrice().doubleValue() *
                                    (100 + percentage)/100;
                product.setPrice(newPrice);
            }
        }
    }

    public void setProducts(List<Product> products) {
        this.products = products;
    }

}

Re-execute os testes do Ant e todos os nossos testes passarão. Agora estamos prontos para voltar a camada web para colocar uma lista de produtos em nosso modelo de Controlador.

3.3. Sumário

Vamos dar uma olhada no que fizemos nesse capítulo 3:

  1. Implementamos um objeto de domínio Product e uma interface de um serviço ProductManager e a classe concreta SimpleProductManager, todas como POJOs.
  2. Escrevemos unidades de teste para todas as classes implementadas.
  3. Não escrevemos nenhuma linha de código com o Spring. Este é um exemplo de quão não invasivo o framework Spring realmente é. Um dos objetivos centrais do Spring é permitir que os desenvolvedores foquem na tarefa mais importante de todas: entregar valor pela modelagem e implementação dos requisitos do negócio. Outro dos objetivos é permitir seguir boas práticas de forma fácil, como a implementação de serviços como interfaces e unidades de testes tão pragmáticas quanto possível. Durante o decorrer desse tutorial, você verá os benefícios do desenho de interfaces vindo à tona.

Abaixo segue a tela de como a estrutura de diretório do projeto precisa ficar depois das instruções acima.