Design, Design Patterns, Lógica

Tua hierarquia não irá te salvar garoto – Strategy

Imagine que hoje seu chefe chegou inspirado e te deu a missão de desenvolver o protótipo de um jogo de esportes. Nesse jogo, existem vários jogadores de modalidades distintas. De forma que todos eles possuem capacidades físicas inerentes aos esportes que praticam. Tipo, um jogador de futebol consegue correr muito rápido e pular razoavelmente alto. Um jogador de vôlei, por outro lado, não corre tanto assim, mas pula muito alto. Você, como excelente desenvolvedor em Orientação a Objetos pensa: “Já matei! Mais um problema manjado de herança!”. Bom, não necessariamente está errado. Você provavelmente vai surgir com uma solução. Mas, é esta solução a melhor? É a mais flexível, mais fácil de testar e de melhor manutenção?

Vamos lá, a primeira coisa a se fazer se você quer criar uma hierarquia de tipos é criar uma classe ‘pai’. Para jogadores parece bem óbvio… hum… Jogador. Vamos assumir para fins didáticos que a classe Jogador tem apenas 2 métodos: correr() e pular(), ambos abstratos. Em seguida, bolamos as classes filhas, digamos: JogadorFutebol e JogadorHoquei. No caso, teríamos:

public abstract class Jogador {
     public abstract void correr();
     public abstract void pular();
}
public class JogadorFutebol extends Jogador {
   public void correr() {
       System.out.println("correr como jogador de futebol...");
   }
   public void pular() {
       System.out.println("Pular como jogador de futebol...");
   }
}

public class JogadorHoquei extends Jogador {
    public void correr() {
        System.out.println("Correr de patins...");
    }
public void pular() {
        System.out.println("Não pular...");
    }
}

Legal! Isso funciona! Então beleza, agora seu chefe chega e diz que seus clientes estão loucos ensandecidos para que o jogo dê suporte a tênis! Bom, agora você vai ter apenas que criar mais uma classe filha de Jogador, só que dessa vez o jogador corre muito e não pula (o cliente pediu isso especificamente =P). Daí, resolve criar a classe JogadorTenis, que… “peraí! Eu posso herdar o corre() do jogador de futebol e o pula do jogador de hóquei e vou economizar um monte de código!” Hehe, tá, mas existem alguns problemas aí. Muitas linguagens nem mesmo suportam herança múltipla! Outra, herança múltipla pode te trazer outros problemas seríssimos. “Ah, então eu escolho uma das duas pra herdar e o outro método eu copio. =]” Nossa! Eu não sei como eu penso nessas coisas… Mas é uma idéia terrível! Primeiro, você não deve herdar classes com o objetivo de economizar linhas de código! Isso escapa completamente o objetivo de OO. Você herda quando uma classe realmente têm um relacionamento “IS-A”. Ou seja,  quando ela for uma especialização da classe pai e esta for uma generalização das filhas. No caso, JogadorTenis não é um JogadorHoquei nem tampouco um JogadorFutebol. Esqueça economizar linhas de código.

Esse primeiro motivo deve bastar, mas eu ainda completo com o segundo. Você ainda assim estaria duplicando código de outras classes. Por exemplo, se você resolve criar uma classe JogadorTenis que ‘extends’ Jogador, e implementa o método correr() como escrito na classe JogadorFutebol e pular() como na classe JogadorHoquei você estaria duplicando código 2 vezes! E isso é ruim, muito ruim. Imagina se mais tarde é descoberto um erro no método da classe pai? “Copia e cola” seria o seu segundo nome… É… herança parece ser uma má idéia nesse caso aí. Outro problema: caso o jogador se machuque e não consiga mais, digamos, correr. Como fazer a instância de um jogador de futebol executar um correr() diferente dinamicamente? E agora? De fato, você ficaria sem saída. Você poderia fazer vários tipos de jogador de futebol representando os estados do seu JogadorFutebol, sendo que cada uma herdando a classe pai… não, essa não é uma solução elegante. A sua hierarquia iria aumentar, o que significa que a raiz do seu problema ia crescer e assim sucessivamente.  Imagina agora se surge um JogadorPoker. Mas enfim… a ideia está próxima! O que você acha de composição? Pense nos comportamentos que os jogadores podem ter relacionados com as suas ações. Por exemplo, correr como? Pular de que maneira? Correr rápido é uma forma de correr. Não correr poderia ser outra. Correr de patins seria outra… E pular? Pular alto como um jogador de vôlei seria uma forma. Pular baixo seria outra… já deu pra pegar a idéia. Então, podemos encapsular esses comportamentos visto que são eles que mudam (e não os jogadores como estávamos pensando no começo do problema!). Esses comportamentos serão uma hierarquia por si próprios. E a maldita da composição, onde entra na história? Os seus jogadores vão possuir(HAS-A) esses comportamentos. Mais explicitamente, no caso de correr:

public interface AcaoCorrer {
     public void correr();
}
public class CorrerComPatins implements AcaoCorrer {
     public void correr() {
         System.out.println("Correndo com patins...");
     }
}
public class CorrerRapido implements AcaoCorrer {
     public void correr() {
         System.out.println("Correndo rápido como um jogador de futebol...");
     }
}
public class NaoCorrer implements AcaoCorrer {   
     public void correr() {
         System.out.println("Não corre.");
     }
}

public abstract class Jogador {

     AcaoCorrer acaoCorrer;
     AcaoPular acaoPular;

     public Jogador() {
         this.acaoCorrer = new NaoCorrer();
         this.acaoPular = new NaoPular();
     }
     public void correr() {
         this.acaoCorrer.correr();
     }
     public void pular() {
         this.acaoPular.pular();
     }

     public abstract void exibir();
     public void setAcaoCorrer(AcaoCorrer acaoCorrer) {
         this.acaoCorrer = acaoCorrer;
     }
//Foi omitido, mas suponha que existe uma ampla variedade de formas de pular, sendo todas filhas de AcaoPular    
    public void setAcaoPular(AcaoPular acaoPular) {
         this.acaoPular = acaoPular;
    }
}


Funciona do mesmo jeitinho e com mais vantagens:

1 – Temos um design voltado a interfaces ao invés de classes. Favorece a flexibilidade. Quando fazemos um comportamento ser representado por uma interface, ao invés de ser implementado na classe Jogador ou em uma de suas subclasses, estamos de certa forma “liberando” esse comportamento pra fora da classe. O comportamento (ou a hierarquia de comportamentos) está fora da própria classe. Além disso, estamos nos referindo ao comportamento de forma generalista, no caso AcaoCorrer > CorrerComPatins por exemplo. E na implementação apenas mencionamos como atributo o tipo da interface AcaoCorrer e não as reais implementações.

2 – É menos complexo você compor uma classe com outra do que começar uma nova hierarquia. Você estará sujeito a menos erros bobos assim.  Ah sim! E hierarquias podem se tornar coisas do capeta depois de 3 níveis.

3 – Podemos mudar o comportamento dinamicamente ou “on the fly”. Com os métodos setter dos comportamentos, podemos mudar o tipo de comportamento que quisermos quando bem entendermos. Exemplo:


public class Jogo {

    public static void main(String[] args) {

//Um jogador de futebol corre muito e pula normalmente

        Jogador jogadorFutebol = new JogadorFutebol();

        jogadorFutebol.correr();

        jogadorFutebol.pular();

//Um jogador de hoquei corre de patins e não pula

         Jogador jogadorHoquei = new JogadorHoquei();

         jogadorHoquei.correr();

         jogadorHoquei.pular();

//Um jogador de hoquei machucado, não consegue correr nem pular

         jogadorHoquei.setAcaoCorrer(new NaoCorrer());

         jogadorHoquei.correr();

         jogadorHoquei.pular();

    }

}

Note que é possível setar os comportamentos de correr e pular porque o parâmetro esperado no método setter é uma generalização – uma interface. Qualquer classe que implemente a interface em questão (o que significa que esse comportamento saiba correr ou pular) poderá ser passada como novo comportamento do Jogador.  Caso surja um JogadorPoker, ele será composto por comportamentos já existentes, NaoCorrer e NaoPular. Logo, não haverá duplicação alguma de código.

Todos esses passos mostrados acima compõem o conhecido padrão de projeto Strategy. Esse padrão está catalogado como um dos 23 Design Patterns do “Gang of Four” ou “GoF”.  Da próxima vez que você se deparar com um problema que tenha essa natureza de encapsular vários algoritmos ou comportamentos num mesmo objeto, lembre-se do padrão Strategy, que pode “mudar as vísceras” de um objeto.  Calma! Não saia por aí dizendo que conheceu um jeito de modelar objetos que mudou a sua vida (principalmente se você descobriu isso aqui nesse blog, nesse caso pesquise mais um pouco =P ). Esta é uma solução para esse tipo de problema, NÃO PARA TODOS.  Por isso, use-a com equilíbrio.

Existem várias APIs famosas que usam Strategy, como JPA (que usa 3 algoritmos diferentes de mapear classes a tabelas do BD) e o componente JComponent (na escolha de Borders) do Swing.  Como eu falei, essa solução clássica resolve vários problemas, e se você for do tipo que se depara com muitos, Strategy vai ser algo comum na sua vida. Por isso, ao se deparar com um problema que parece ser facilmente resolvido estendendo classes e/ou criando uma nova hierarquia, pense se composição não seria uma melhor solução. Não apenas você ficará feliz com uma solução elegante e abrangente, mas os programadores que porventura pegarem esse código no futuro também ficarão. =]

Padrão

7 comentários sobre “Tua hierarquia não irá te salvar garoto – Strategy

  1. Esse é, disparado, o padrão de projetos mais útil que tem no livro do GoF. E um dos mais simples também.

    O java tem uma maneira interessante de implementar o strategy com enums. Já postei sobre isso no GUJ, e adoro usá-los para as estratégias “padrão” das minhas frameworks.
    http://www.guj.com.br/posts/list/55885.java#293436

    Sem falar que o strategy faz desaparecer aqueles ifs horríveis. Não é à toa que é uma das refatorações propostas pelo Fowler.🙂

    Ainda reforçando o que você escreveu, tem essa excelente entrevista com o Gamma (sim, o mesmo do Eclipse, JUnit e um dos membros do Gof), sobre programar para interfaces, e não para implementação:
    http://www.artima.com/lejava/articles/designprinciples.html

  2. Oi ViniGodoy! Realmente muito simples o Strategy e incrível a quantidade de programadores profissionais que não o conhecem e/ou não sabem usá-lo.

    Curioso vc falar sobre a implementação do Strategy com enums, eu tava lendo sobre isso justamente essa semana (Effective Java) e achei muito prática essa solução. Eficaz e bem simples. Se é usada um bom critério de nomes para atributos e métodos, a classe praticamente fala de tão fácil que fica de entender o código. Não falei sobre isso pq o post iria se delongar demais e tal… mas daí foi ótimo vc deixar o link acima como referência. Obrigado! =]

  3. Bruno Cartaxo disse:

    O dinamismo e a flexibilidade do relacionamento de composição é sem dúvida um dos maiores atrativos de padrões como o strategy, em detrimento do relacionamento excessivamente forte da herança.

    Olhando por outro lado, recentemente projeitei um componente que era muito baseado em composição e utilizei muitas vezes o padrão strategy. O que pude notar é que ele tem um efeito colateral meio inesperado. Percebi que criava o objeto strategy e o mesmo tinha que ser passado para um determinado objeto, tipicamente uma entidade. Até ai ok. O problema é que na maioria das vezes o objeto strategy precisava de dados que estavam na entidade. Daí acabava passando vários parâmteros para o strategy, ou pior ainda, passar uma referência para a entidade, gerando uma dependência cíclica. No fim das contas, algoritmos (um strategy) são muito emaranhados com as estruturas de dados (os atributos da entidade) que eles precisam para trabalhar.

    Foi então que pude perceber que o problema é que eu estava ferindo um dos mais básicos conceitos de orientação a objetos: Unir comportamento e estado numa única estrutura, os objetos. Por isso meu componente muito se parecia com programas em linguagens procedurais, tais como C. Tinha entidades apenas com atributos, getters e setters, e todo o comportamento em strategies. Era como se minhas classes de entidade fossem structs de C, e meus strategies fossem funções que recebem um ponteiro para essas structs… Babau OO.

    No fim das contas acho que a decisão entre composição e herança, ou o uso do padrão strategy, recai no clichê dos tradeoffs, como tudo. Costumo usar strategy quando existe uma necessidade real de comportamentos múltiplos e dinâmicos, e herança quando existe um relacionamento mais estrutural de fato.

    Concordo plenamente com o conceito de “IS-A” e “HAS-A”. O mau uso de herança normalmente é visto quando se deseja um reuso oportunista, segundo o pessoal de “software product lines”. Esse reuso oportunista é que, como você disse, acaba com a boa modelagem OO. Acredito que o principal para uma boa modelagem OO é respeitar a semântica. Se algo é parecido mas tem semêntica completamente distinta… Esqueça o reuso. Quando a modelagem for feita semanticamente, o reuso e as generalizações serão naturais.

    Muito legal o posta cara!

    Abraço

    • E ae camarada!

      Interessante sua experiência. Padrões são soluções confiáveis apenas quando nós nos comprometemos com as condições de usá-los em primeiro lugar. A saber, essas condições são o conjunto de regras básicas de OO , incluindo essa q vc citou. Ao meu ver, esse é um erro amplamente encontrado no qual os programadores disfarçam o código procedural em OO. Já fiz muito isso e até hoje me vigio muito pra fazer a coisa da forma correta. Acho q qd a gente aprende a programar num paradigma, ou passa mt tempo programando sobre um certo paradigma, nossa mente fica viciada em pensar da forma como este exige. Daí, qd há a mudança para OO, a mente demora a se adaptar. No seu caso vc percebeu bem rápido, mas imagina quanto código escrito assim tem por aí… É o conhecido modelo anêmico que continua ferindo muito código por aí.
      Concordo com vc, tudo em software recai no clichê dos tradeoffs. Devemos então procurar as responsabilidades reais dos objetos, procurando aproximar nossa modelagem o mais próxima do mundo real possível: quando houver múltiplos comportamentos/algoritmos – strategy, quando houver relacionamento “X is a Y” – herança ou composição. Preferindo sempre a segunda opção, exceto se a representação por meio de herança realmente estiver explícita e for obrigatória (caso vc projete algum módulo da sua API pra herança por exemplo). É aquela coisa, se você usar herança onde composição é apropriada, estará expondo detalhes da implementação desnecessariamente. Ah sim! Quando você falou em “reuso oportunista” (devia ter usado essa expressão no post =P) me veio a mente um erro crasso do Java nessa questão: Properties extends Hashtable. Podre. E perdura por séculos hehe.

      Valeu Bruno!

  4. Pingback: MVC não é sobre camadas! « The Good, the Bad and the Ugly in Software Development

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s