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:
- The Liskov Substitution Principle for “Duck-Typed” Languages
- SOLID Ruby, by Jim Weirich – RubyConf 2009
- SOLID Object-Oriented Design, by Sandi Metz, GoRuCo 2009