Fork me on GitHub

Keep Learning Conhecimento nunca é o bastante

Postado em
18 May 2011 @ 19:38

Tag(s)
Desenvolvimento, Ruby, Test-Driven Development

SOLID Ruby: Liskov Substitution Principle e Interface Segregation Principle

Para fechar os cinco princípios do SOLID, vamos falar sobre os dois princípios restantes: Liskov Substitution Principle (LSP) e Interface Segregation Principle (ISP).

Como já foi falado anteriormente, esses princípios foram formulados com linguagens estáticas em mente e, por essa razão, precisam ser “adaptados” para que sejam aplicados em linguagens dinâmicas. Note que, em suas formas originais, esses princípios em geral recorrem a técnicas como herança para contornar as “amarras” do sistema estático de tipos.

No caso do LSP temos mais um exemplo disso. Em sua forma original ele é definido da seguinte forma:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.

O que foi “traduzido” para o OOP da seguinte forma:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

Em Ruby isso não é muito problemático pois, como sabemos, não estamos presos à tipos. O importante, como já vimos com o OCP, é manter a mesma interface, de forma que se precisarmos modificar alguma entidade, as demais entidades que dela dependem não precisem ser modificadas.

Além da interface, também precisamos prestar atenção ao comportamento. Modificações no comportamento podem fazer com que clientes da entidade sofram consequências inesperadas.

Já o ISP é definido da seguinte forma:

Clients should not be forced to depend upon interfaces that they do not use.

Isso quer dizer que um cliente (entidade que depende de alguma outra) não deve depender de interfaces não utilizadas por ele pois, em caso de modificação nessa interface, mesmo que o cliente em questão não a utilize, também terá que ser modificado.

Em linguagens estáticas há uma série de técnicas e artifícios para atingir isso. Em Ruby, o Duck Typing nos entrega esse princípio “de graça”, já que cada entidade depende apenas da interface que utiliza, independente do restante (e de tipos). Apesar disso, devemos sempre buscar entidades com interfaces coesas e bem delimitadas, evitando que sejam muito extensas e genéricas (o SRP se aplica aqui também). Aplicar o design pattern Adapter é uma boa forma de respeitar isso.

Bem, é fácil perceber que os últimos três princípios discutidos (OCP, LSP e ISP) lidam diretamente com formas de garantir interfaces estáveis para atingirmos o nosso objetivo de evitar que mudanças no código gerem um “efeito dominó” e façam com que tenhamos que alterar várias partes do software.

Mas, já que não temos estruturas como Interfaces e classes abstratas para garantir que estamos respeitando a interface definida, como garantir a aplicação desses princípios? Bom, há algumas formas para fazer isso. Você pode “emular” uma interface da seguinte forma:

class MyInterface
  def method_1
    raise "abstract method called"
  end
 
  def method_2
    raise "abstract method called"
  end
 
  # and so on...
end
 
class MyClass < MyInterface
  def method_1
    # do something
  end
 
  def method_2
    # do something
  end
end

Obs: também pode ser feito através de módulos e mixins ao invés de herança.

Isso funciona, mas não é muito o estilo Ruby, certo? A maneira que prefiro fazer é através de specs. Sempre que vou criar um wrapper/adapter ou qualquer estrutura que precise de uma interface estável mesmo quando o código encapsulado for modificado, crio um grupo de “specs de interface” e uso ela para toda estrutura que precise implementar a mesma.

Por exemplo, poderíamos escrever um cliente para o Twitter e escrever nosso código de modo que a gem que consome a API do serviço possa ser trocada sem maiores consequências:

 
# app/adapters/twitter_gem_adapter.rb
class TwitterGemAdapter
  def self.timeline(page=1, per_page=5, since_id = nil)
    options = {:page => page, :count => per_page}
    options[:since_id] = since_id if since_id
 
    Twitter.home_timeline(options)
  end
 
  def self.update(text)
    Twitter.update(text)
  end
 
  def self.reply(in_reply_to_status_id, text)
    Twitter.update(text, :in_reply_to_status_id => in_reply_to_status_id)
  end
 
  def self.retweet(tweet_id)
    Twitter.retweet(tweet_id)
  end
 
  def self.favorite(tweet_id)
    Twitter.favorite_create(tweet_id)
  end
end
 
# spec/support/shared_examples/twitter_adapter_examples.rb
module TwitterAdapterExamples
  shared_examples_for "any adapter for a twitter api gem" do
    it { should respond_to(:timeline)}
    it { should respond_to(:update)}
    it { should respond_to(:reply)}
    it { should respond_to(:retweet)}
    it { should respond_to(:favorite)}
  end
end
 
# spec/adapters/twitter_gem_adapter_spec.rb
describe TwitterGemAdapter do
  context "API contract" do
    subject { TwitterGemAdapter }
 
    it_behaves_like "any adapter for a twitter api gem"
  end
 
  # other specs ...
end

Aqui utilizamos a feature de “exemplos compartilhados” do RSpec, mas isso pode ser feito facilmente em outros frameworks. O importante é que, através dessas specs, garantimos que qualquer adapter implemente a mesma interface. Nesse caso cuidamos apenas disso, mas qualquer comportamento comum também pode ser especificado da mesma forma.

Material recomendado:


1 Comentário

[…] http://blog.lucashungaro.com/2011/05/18/solid-ruby-liskov-substitution-principle-e-interface-segrega… *Lucas Húngaro é desenvolvedor da equipe de Ruby da Gonow. […]


Deixe um comentário