Página Inicial > .NET, Arquitetura > Cheeseburgers, Decorators e Mocks

Cheeseburgers, Decorators e Mocks

Em São Paulo, eu sempre comi cheeseburgers feitos com pão, hamburguer e queijo. Mas quando eu fui para Itararé, cidade do interior do estado de São Paulo, descobri que eles também colocavam milho no sanduíche.

Para exemplificar, vamos imaginar que o cheeseburger de Ilhéus-BA, venha com molho de pimenta. Só para constar, eu nunca fui para Ilhéus, apesar de ser a cidade natal de meu pai. Então na verdade não tenho a mínima idéia de como seja o cheeseburger de lá.

Imagem original de MarketFare Foods, Inc.

Imagem original de MarketFare Foods, Inc.

Vamos transportar esses três tipos de cheeseburgers para objetos e fazer alguns testes com eles. Usarei como plataforma .NET, linguagem C#, a ferramenta de testes unitários que vem com o Visual Studio 2008 e o Rhino Mocks como framework de criação de mocks.

.
Cheeseburgers com herança
Primeiramente criamos a classe Cheeseburger, que representa o cheeseburger comum de São Paulo. Nessa classe teremos uma propriedade para descrição e um método que retorna a quantidade de calorias do sanduíche.

public class Cheeseburger
{
    public Cheeseburger()
    {
        this.Description = "Bread, Hamburger, Cheese";
    }

    public string Description { get; protected set; }

    public virtual int Calories()
    {
        return 300;
    }
}

.
Os testes para a classe Cheeseburger são bem simples:

[TestClass]
public class CheeseburgerTest
{
    private Cheeseburger cheeseburger;

    [TestInitialize]
    public void Init()
    {
        this.cheeseburger = new Cheeseburger();
    }

    [TestMethod]
    public void Description_Of_Cheeseburger()
    {
        Assert.AreEqual("Bread, Hamburger, Cheese", this.cheeseburger.Description);
    }

    [TestMethod]
    public void Calories_Of_Cheeseburger()
    {
        Assert.AreEqual(300, this.cheeseburger.Calories());
    }
}

.
Agora vamos criar a classe CheeseburgerItarare que possuirá os mesmos membros de Cheeseburger. Como nós aprendemos na faculdade ou em algum curso que herança é um dos pilares da orientação a objetos e serve para reutilização de código, CheeseburgerItarare irá herdar de Cheeseburger.

public class CheeseburgerItarare : Cheeseburger
{
    public CheeseburgerItarare() : base()
    {
        this.Description += ", Corn";
    }

    public override int Calories()
    {
        return base.Calories() + 70;
    }
}

A diferença no cheeseburger de Itararé é o milho, então acrescentamos um texto para milho na descrição e sobrescrevemos o método Calories para somar 70 calorias ao valor que é retornada do método Calories da classe base Cheeseburger.

Fazemos o mesmo para o cheeseburger de Ilhéus: criamos uma classe CheeseburgerIlheus que herda de Cheeseburger e adicionamos a descrição e calorias equivalentes ao molho de pimenta. Veja o diagrama de classes:

Cheeseburgers com herança

Cheeseburgers com herança

.
Os testes para a classe CheeseburgerItarare ficam assim:

[TestClass]
public class CheeseburgerItarareTest
{
    private CheeseburgerItarare cheeseburgerItarare;

    [TestInitialize]
    public void Init()
    {
        this.cheeseburgerItarare = new CheeseburgerItarare();
    }

    [TestMethod]
    public void Description_Of_Cheeseburger_Itarare()
    {
        Assert.AreEqual("Bread, Hamburger, Cheese, Corn", this.cheeseburgerItarare.Description);
    }

    [TestMethod]
    public void Calories_Of_Cheeseburger_Itarare()
    {
        Assert.AreEqual(370, this.cheeseburgerItarare.Calories());
    }
}

Em princípio muito simples. Os testes unitários asseguram o comportamento de CheeseburgerItarare, ou seja, os valores de retorno da descrição e da quantidade de calorias, mas não testamos implementação. Em outras palavras, não conseguimos assegurar que quando o método Calories de CheeseburgerItarare é executado, uma chamada ao método Calories da classe base Cheeseburger é realizada.

Para testar essa chamada de método da classe base, precisamos usar mocks. Com isso, temos um problema de acoplamento, pois CheeseburgerItarare depende de Cheeseburger e com herança não conseguimos substituir a classe base por um mock. Podemos resolver isso usando composição ao invés de herança.

Antes de passarmos para composição, quero alertar que você precisa avaliar se testar chamadas de outras classes agregam valor aos testes que você está fazendo. Às vezes somente o comportamento da classe já é o suficiente para considerar uma classe testada. Você precisa ponderar o que é importante e necessário testar nos pedaços de código do seu sistema. Nesse pequeno exemplo em específico, estou considerando necessário testar as chamadas de método de Cheeseburger dentro das classes CheeseburgerItarare e CheeseburgerIlheus, por isso vamos modificar o design das mesmas.

.
Cheeseburgers com composição
Os princípios de orientação a objetos, entre outras coisas, nos dizem para:

  • Dar prioridade à composição em à relação à herança;
  • Programar para interfaces, não para para implementações;
  • Depender de abstrações, não de classes concretas.

Na solução de cheeseburgers com herança, violamos todos esses princípios. Então vamos consertar isso.

Primeiro, vamos extrair a interface da classe Cheeseburger:

public interface ICheeseburger
{
    string Description { get; }

    int Calories();
}

.
Nossa classe Cheeseburguer não muda em nada, só que agora implementa a interface ICheeseburguer:

public class Cheeseburger : ICheeseburger
{
    public Cheeseburger()
    {
        this.Description = "Bread, Hamburger, Cheese";
    }

    public string Description { get; protected set; }

    public virtual int Calories()
    {
        return 300;
    }
}

.
Teremos então uma mudança significativa nas classes CheeseburgerItarare e CheeseburgerIlheus, que não herdarão mais de Cheeseburger e implementarão a interface ICheeseburger. Veja o diagrama de classes como fica:

Cheeseburgers com composição

Cheeseburgers com composição

.
E a nova implementação da classe CheeseburgerItarare:

public class CheeseburgerItarare : ICheeseburger
{
    private ICheeseburger cheeseburger;

    public CheeseburgerItarare(ICheeseburger cheeseburger)
    {
        this.cheeseburger = cheeseburger;
        this.Description = this.cheeseburger.Description + ", Corn";
    }

    public string Description { get; protected set; }

    public int Calories()
    {
        return this.cheeseburger.Calories() + 70;
    }
}

A classe CheeseburgerItarare agora tem um campo privado do tipo ICheeseburger (linha 3), que será atribuído valor através de um parâmetro recebido no construtor (linha 7). Dessa forma, estamos compondo a classe Cheeseburger com alguma implementação de ICheeseburger, injetando essa dependência através do seu construtor. O método Calories delega sua chamada ao método Calories do objeto que foi injetado (linha 15) e soma as 70 calorias correspondentes ao milho.

Com esse design nós podemos criar um mock de ICheeseburger, passar esse mock para o construtor de CheeseburgerItarare e testar as chamadas de método e propriedade de ICheeseburger dentro da classe CheeseburgerItarare.

Vamos ver como ficam os testes agora:

[TestClass]
public class CheeseburgerItarareTest
{
    private MockRepository mocks;
    private ICheeseburger cheeseburgerMock;
    private CheeseburgerItarare cheeseburgerItarare;

    [TestInitialize]
    public void Init()
    {
        this.mocks = new MockRepository();
        this.cheeseburgerMock = this.mocks.DynamicMock();
    }

    [TestMethod]
    public void Description_Of_Cheeseburger_Itarare()
    {
        Expect.Call(this.cheeseburgerMock.Description).Return("Cheeseburger description");

        this.mocks.ReplayAll();
        this.cheeseburgerItarare = new CheeseburgerItarare(this.cheeseburgerMock);
        this.mocks.VerifyAll();

        Assert.AreEqual("Cheeseburger description, Corn", this.cheeseburgerItarare.Description);
    }

    [TestMethod]
    public void Calories_Of_Cheeseburger_Itarare()
    {
        Expect.Call(this.cheeseburgerMock.Calories()).Return(100);

        this.mocks.ReplayAll();

        this.cheeseburgerItarare = new CheeseburgerItarare(this.cheeseburgerMock);
        Assert.AreEqual(170, this.cheeseburgerItarare.Calories());

        this.mocks.VerifyAll();
    }
}

Para o teste da quantidade de calorias, na linha 30 gravamos uma chamada esperada do método Calories do mock de ICheeseburger definindo seu valor de retorno para 100. Instanciamos CheeseburgerItarare passando o mock como parâmetro na linha 34 e quando chamamos o método Calories na linha 35, esperamos o valor de 170 calorias: 100 do mock + 70 equivalente às calorias do milho. Finalmente, na linha 37 conseguimos assegurar que o método Calories do mock da interface ICheeseburger foi chamado na implementação do método Calories da classe CheeseburgerItarare.

A implementação e os testes da classe CheeseburgerIlheus seguem o mesmo esquema da classe CheeseburguerItarare.

Com isso, atingimos nosso objetivo de testar comportamento e implementação das classes de cheeseburgers.

Mas e se nós quisermos um novo tipo de cheeseburger, por exemplo, um que venha com cebola? Ah, é fácil, basta criar uma classe nova que implementa ICheeseburger com o mesmo esquema de injeção de dependência de uma implementação de ICheeseburger. Mas e se nós também quisermos criar outras combinações de ingredientes, como por exemplo:

  • Cheeseburger com milho e molho de pimenta;
  • Cheeseburger com milho e cebola;
  • Cheeseburger com milho, cebola e molho de pimenta.

Vamos ter que criar uma classe para cada combinação de ingredientes? E se ao invés de três ingredientes nós tivermos 15 ingredientes? Hum, bastante combinações, não? E se num futuro próximo aparecer novos ingredientes? Vamos ter que criar novas classes para todas as novas combinações de ingredientes?

.
Cheeseburgers com decoradores
Ao invés de criarmos inúmeras classes para inúmeras combinações de ingredientes de cheeseburgers, podemos modificar nosso design para criarmos ingredientes independentes e “decorar” os cheeseburgers com os ingredientes que quisermos e quando quisermos.

Isso segue um outro princípio de orientação a objetos: classes devem estar abertas para extensão, mas fechadas para modificação.

Usaremos um design pattern chamado Decorator, que é definido da seguinte forma:

Atribui responsabilidades adicionais a um objeto dinamicamente. Os Decorators fornecem uma alternativa flexível a subclasses para extensão de funcionalidades.

.
E aqui temos o diagrama de classe com a estrutura do padrão:

Decorator Design Pattern

Decorator Design Pattern

Onde:

  • Component define a interface para objetos que podem ter responsabilidades acrescentadas aos mesmos dinamicante;
  • ConcreteComponent define um objeto para o qual responsabilidades adicionais podem ser atribuídas;
  • Decorator mantém uma referência para um objeto Component e define uma interface que segue a interface de Component;
  • ConcreteDecorator acrescenta responsabilidades ao componente.

Chega de teoria e vamos para a prática nos nossos cheeseburgers.

A primeira coisa a fazer é criar a classe abstrata que servirá como Component. Vamos chamá-la de Sandwich.

public abstract class Sandwich
{
    public virtual string Description { get; protected set; }

    public abstract int Calories();
}

Note que marcamos a propriedade Description como virtual para suas subclasses poderem sobrescrevê-la.

Depois criamos outra classe abstrata que servirá como Decorator e a nomeamos como SandwichDecorator. Essa classe irá herdar de Sandwich.

public abstract class SandwichDecorator : Sandwich
{
    protected Sandwich sandwich;

    public SandwichDecorator(Sandwich sandwich)
    {
        this.sandwich = sandwich;
    }

    public override string Description
    {
        get
        {
            return this.sandwich.Description;
        }
    }

    public override int Calories()
    {
        return this.sandwich.Calories();
    }
}

Da mesma forma que fizemos no exemplo de cheeseburgers com composição, a injeção de dependência é feita pelo construtor (linhas 5 a 8), atribuindo o valor do parâmetro ao campo protegido do tipo Sandwich. A propriedade Description (linhas 10 a 16) é implementada delegando seu retorno para a propriedade Description da variável sandwich, ou seja, o objeto que foi injetado. O método Calories finalmente é implementado (linhas 18 a 21) e também delega sua chamada para a variável sandwich, mas chamando o método Calories da mesma.

Agora vamos modificar a classe Cheeseburger para herdar de Sandwich. Essa é a classe que servirá como ConcreteComponent.

public class Cheeseburger : Sandwich
{
    public Cheeseburger()
    {
        this.Description = "Bread, Hamburger, Cheese";
    }

    public override int Calories()
    {
        return 300;
    }
}

.
O último passo é criar as classes que servirão como ConcreteDecorator: Corn, OnionRings e PepperSauce. Segue o código da classe Corn:

public class Corn : SandwichDecorator
{
    public Corn(Sandwich sandwich) : base(sandwich)
    {

    }

    public override string Description
    {
        get
        {
            return base.Description + ", Corn";
        }
    }

    public override int Calories()
    {
        return base.Calories() + 70;
    }
}

O construtor da classe Corn chama o construtor da classe base SandwichDecorator passando o parâmetro do tipo Sandwich (linhas 3 a 6). A propriedade Description (linhas 8 a 14) é sobrescrita para retornar a descrição da classe base juntamente com a descrição de milho. O método Calories também é sobrescrito (linhas 16 a 19) e soma o retorno do método Calories da classe base com as calorias do milho. Em suma, toda a implementação é feita chamando os membros da classe base e adicionando comportando e/ou estado relacionado ao ingrediente milho quando necessário.

O mesmo esquema para as classes OnionRings e PepperSauce:

public class OnionRings : SandwichDecorator
{
    public OnionRings(Sandwich sandwich) : base(sandwich)
    {

    }

    public override string Description
    {
        get
        {
            return base.Description + ", Onion Rings";
        }
    }

    public override int Calories()
    {
        return base.Calories() + 140;
    }
}
public class PepperSauce : SandwichDecorator
{
    public PepperSauce(Sandwich sandwich) : base(sandwich)
    {

    }

    public override string Description
    {
        get
        {
            return base.Description + ", Pepper Sauce";
        }
    }

    public override int Calories()
    {
        return base.Calories() + 20;
    }
}

.
Aqui está o diagrama de classes dos cheeseburgers com decoradores:

Cheeseburgers com decoradores

Cheeseburgers com decoradores

.
Vamos ver como isso funciona através dos testes.

[TestClass]
public class CheeseburgerVariedTest
{
    private Sandwich cheeseburger;

    [TestInitialize]
    public void Init()
    {
        this.cheeseburger = new Cheeseburger();
    }

    [TestMethod]
    public void Itarare_Cheeseburger()
    {
        this.cheeseburger = new Corn(this.cheeseburger);

        Assert.AreEqual("Bread, Hamburger, Cheese, Corn", this.cheeseburger.Description);
        Assert.AreEqual(370, this.cheeseburger.Calories());
    }

    [TestMethod]
    public void Itarare_Cheeseburger_With_Onion_Rings()
    {
        this.cheeseburger = new Corn(this.cheeseburger);
        this.cheeseburger = new OnionRings(this.cheeseburger);

        Assert.AreEqual("Bread, Hamburger, Cheese, Corn, Onion Rings", this.cheeseburger.Description);
        Assert.AreEqual(510, this.cheeseburger.Calories());
    }

    //Outros testes
}

No primeiro teste decoramos a instância da classe Cheeseburger com milho (linha 15), embrulhando-a (wrapping) com a classe Corn. Já no teste de cheeseburger com milho e cebola, a variável cheeseburger é decorada duas vezes, uma vez com a classe Corn (linha 24) e outra com a classe OnionRings (linha 25). Cada vez que um cheeseburger é decorado, o decorador adiciona seu próprio comportamento antes e/ou depois de delegar para o objeto que ele decora.

Note que, como as classes Corn e OnionRings herdam de SandwichDecorator, que por sua vez herda de Sandwich, assim como a classe Cheeseburger, no final das contas estamos sempre lidando com a classe abstrata Sandwich. Por isso, através do polimorfismo, podemos atribuir uma instância da classe Corn e/ou OnionRings à uma variável do tipo Cheeseburger.

Quando o método Calories da variável cheeseburger é chamado na linha 28, estamos chamando o método Calories do último decorador, ou seja, da classe OnionRings. Depois contamos com a delegação para adicionar as calorias dos ingredientes. Veja o acontece:

  1. OnionRings chama o método Calories da classe base SandwichDecorator;
  2. SandwichDecorator delega sua chamada para o método Calories da classe Corn;
  3. Corn chama o método Calories da classe base SandwichDecorator;
  4. SandwichDecorator delega sua chamada para o método Calories da classe Cheeseburger;
  5. Cheeseburger retorna 300 calorias;
  6. SandwichDecorator retorna o valor das calorias que retornou da classe Cheeseburger;
  7. Corn adiciona suas 70 calorias ao retorno da classe base SandwichDecorator e retorna 370 calorias;
  8. SandwichDecorator retorna o valor das calorias que retornou da classe Corn;
  9. OnionRings adiciona suas 140 calorias ao retorno da classe base SandwichDecorator e retorna 510 calorias.

Complicado para entender? Dê uma olhada novamente no diagrama de classes “Cheesebugers com decoradores” e depois volte para o código. E se você debugar os testes, poderá ver passo-a-passo como as coisas acontecem. No final do post tem o link para você baixar o código completo.

.
Uma coisa interessante é que você pode testar isoladamente as classes ConcreteDecorator (Corn, PepperSauce, OnionRings ou qualquer outro decorador que você queira acrescentar). Por exemplo:

[TestClass]
public class CornTest
{
    private MockRepository mocks;
    private Sandwich sandwichMock;
    private Corn corn;

    [TestInitialize]
    public void Init()
    {
        this.mocks = new MockRepository();
        this.sandwichMock = this.mocks.DynamicMock();
    }

    [TestMethod]
    public void Description_Of_Cheeseburger_With_Corn()
    {
        Expect.Call(this.sandwichMock.Description).Return("Sandwich description");

        this.mocks.ReplayAll();

        this.corn = new Corn(this.sandwichMock);
        Assert.AreEqual("Sandwich description, Corn", this.corn.Description);

        this.mocks.VerifyAll();
    }

    [TestMethod]
    public void Calories_Of_Cheeseburger_With_Corn()
    {
        Expect.Call(this.sandwichMock.Calories()).Return(100);

        this.mocks.ReplayAll();

        this.corn = new Corn(this.sandwichMock);
        Assert.AreEqual(170, this.corn.Calories());

        this.mocks.VerifyAll();
    }
}

Aqui usamos mocks para testar as chamadas de métodos de Sandwich, assim como fizemos nos testes das classes CheeseburgerItarare e CheeseburgerIlheus no exemplo de cheeseburgers com composição.

Com esse design nós podemos ter novos ingredientes sem termos que nos preocupar com combinações, basta criar uma classe nova do ingrediente para decorar os sanduíches. Além disso, nós podemos criar também outros tipos de sanduíche. Por exemplo, podemos criar uma classe HotDog que herda da classe Sandwich. O ponto é: qualquer classe que herde da classe Sandwich pode ser decorada com os nossos ConcreteDecorators.

Design Patterns são soluções para problemas comuns e não necessariamente soluções prontas para o seu problema. Eles lhe ajudam a encontrar a sua solução para o seu problema específico. Fique à vontade para adaptá-los às suas necessidades.

.
Dúvidas, questionamentos, discórdias, sugestões? Deixe seu comentário.

O código completo com todas as classes e seus testes está disponível aqui.

.
Referências em inglês:

Referências em português:

.
Atualização em 13/09/2009: Fiz um post com a implementação desse exemplo de Decorator Pattern em Ruby. Você pode vê-lo aqui.


.NET, Arquitetura , , , , , , , , , , , , ,

  1. 5, agosto, 2009 em 14:05 | #1

    Ótimo post, parabéns. A explicação dos padrões fica bem mais clara com exemplos práticos como esse.

  2. 10, agosto, 2009 em 19:34 | #2

    Obrigado, @Guilherme Garnier.
    A idéia de criar um exemplo assim era exatamente essa.

  1. 31, julho, 2009 em 16:17 | #1
  2. 13, setembro, 2009 em 22:41 | #2
  3. 19, setembro, 2009 em 15:15 | #3