Design pattern Adapter
🎯 Objectif : éviter de faire une classe "Boite à outil", cad qui fait plus que ce qu'elle doit, et qui intègre du code d'adaption et donc du savoir spécifique, on utilise le Design pattern Adapter pour limiter l'adaptation des inputs dans la classe elle même
Anti-pattern
class Hammer
def swing!
puts "swing"
end
end
class Screwdriver
def drive!
puts "drive"
end
end
class ToolSequence
def initialize(tools: [])
@sequence = tools
end
def execute!
@sequence.map {|tool| Object.const_get("#{tool.to_s.capitalize}")::new }.each do |tool|
if tool.class == Screwdriver then
tool.drive!
elsif tool.class == Hammer then
tool.swing!
end
end
end
end
sequence = ToolSequence::new tools: [:hammer, :screwdriver, :hammer]
sequence.execute!
👆 Constat : La ToolSequence
doit "savoir" comment utiliser chaque outil
Pattern pur
On va utiliser une Classe Adapter
pour chaque outil :
class Hammer
def swing!
puts "swing"
end
end
class Screwdriver
def drive!
puts "drive"
end
end
class HammerAdapter
def initialize(hammer)
@hammer = hammer
end
def use!
@hammer.swing!
end
end
class ScrewdriverAdapter
def initialize(screwdriver)
@screwdriver = screwdriver
end
def use!
@screwdriver.drive!
end
end
class ToolSequence
def initialize(tools: [])
@sequence = tools
end
def execute!
@sequence.map {|tool|
adapter = Object.const_get("#{tool.to_s.capitalize}Adapter")
tool = Object.const_get("#{tool.to_s.capitalize}")::new
adapter::new(tool)
}.each do |adapter|
adapter.use!
end
end
end
sequence = ToolSequence::new tools: [:hammer, :screwdriver, :hammer]
sequence.execute!
👆 Constat : ici on utilise un ObjetAdapter
pour que LaToolsequence
n'est pas de savoir spécifique lié à un objet
=> Si on vient à ajouter un outil on ne remodifiera plus la classe ToolSequence
, on ajoutera son outil et l’adapter qui va avec
Pattern Ruby-way
Si on considèrent le cas ou l'outil n'implémente qu'une méthode d'action , la cas suivant serait bien plus Ruby-way
On utilise ici une adaptation par Mixin
ℹ️ Remarque : Cet exemple pourrait marcher avec une classe avec plus de méthodes, si on suffixe la méthode d'action par !, alors on ne cherche plus la#first
mais la#select {|item| item[-1] == '!'}.first
ℹ️ Remarque 2 : self.class.instance_methods(false)
renvoie la list des méthodes d'instance spécifiques à la classe (le false)
class Hammer
def swing!
puts "swing"
end
end
class Screwdriver
def drive!
puts "drive"
end
end
module ToolAdapter
def use!
self.send self.class.instance_methods(false).first
end
end
class ToolSequence
def initialize(tools: [])
@sequence = tools
end
def execute!
@sequence.each do |tool|
tool = Object.const_get("#{tool.to_s.capitalize}")::new
tool.extend ToolAdapter
tool.use!
end
end
end
sequence = ToolSequence::new tools: [:hammer, :screwdriver, :hammer]
sequence.execute!
Là où l'adaptation pure est utile
un exemple concret
(pas l'encrypter en lui-même qui est "ugly")
class Encrypter
def initialize(key)()
@key = key
end
def encrypt(reader,writer)
key_index = 0
while not reader.eof?
clear_char = reader.getc
encrypted_char = clear_char ^ @key[key_index]
writer.putc(encrypted_char)
key_index = (key_index + 1) % @key.size
end
end
end
reader = File.open('../Cache/message.txt')
writer = File.open('../Cache/message.enc','w')
encrypter = Encrypter.new('my secret key')
encrypter.encrypt(reader,writer)
class StringIOAdapter
def initialize(string)
@string = string
@position= 0
end
def getc
if @position >= @string.length then
raise EOFError
end
ch = @string[@position]
@position += 1
return ch
end
def eof?
return @position >= @string.length
end
end
encrypter = Encrypter.new('XYZZY')
reader = StringIOAdapter.new('a secret story')
writer = File.open('../Cache/message.enc','w')
encrypter.encrypt(reader,writer)