J’écris en ce moment un émulateur pour Chip-8, en Ruby. Dans les outils que j’écris à coté il y a un désassembleur de code Chip-8. Dans ce désassembleur il y a un bel exemple de polymorphisme.

Un peu de contexte

La classe Opcode permet de faire la correspondance entre un opcode Chip-8 et une ligne de code assembleur. Un opcode Chip-8 est toujours représenté par un nombre hexadécimal de 4 chiffres.

Voici quelques exemples d’opcodes et leur correspondance en assembleur :

Opcode | Assembleur  | Remarque
-------|-------------|---------
2a00   | CALL a00    |
7012   | ADD V0, 12  | V0 est un registre
a22e   | LOAD I, 22e | I est un registre

On pourra remarquer (même si ça n’est pas ultra visible avec seulement trois exemples) que c’est le premier chiffre (ici 2, 7 et a) qui décide du type d’instruction.

De 0 à f, on a donc 16 types possibles, ce qui donne ce genre de code :

class Opcode

  def initialize(opcode)
    @opcode = opcode
    @assembly = compute_assembly
    ...
  end

  ...

  private

  def compute_assembly
    case @opcode[0]
    when '0' then "Return this code"
    when '1' then "Return that code"
    when '2' then # ...
    ...
    when 'd' then # ...
    when 'e' then # ...
    when 'f' then # ...
    end
  end

end

De plus, certains type d’instruction sont partagés en sous type, selon le quatrième chiffre, ou bien selon les troisième et quatrième, ça dépend. Comme toujours, on se retrouve à devoir gérer des cas particuliers, et le code ressemble rapidement à la monstruosité qui suit :

def compute_assembly
  case @opcode[0]
  when '0'
    if @opcode == '00e0'
      # do that
    elsif @opcode == '00ee'
      # do that
    else
      # do that
    end
  when '1' then # do that
  when '2' then # do that
  when '3' then # do that
  when '4' then # do that
  when '5' then # do that
  when '6' then # do that
  when '7' then # do that
  when '8'
    case @opcode[3]
    when '0' then # do that
    when '1' then # do that
    when '2' then # do that
    when '3' then # do that
    when '4' then # do that
    when '5' then # do that
    when '6' then # do that
    when '7' then # do that
    when 'e' then # do that
    else
      # do that
    end
  when '9' then ...
  when 'a' then ...
  when 'b' then ...
  when 'c' then ...
  when 'd' then ...
  when 'e'
    # Ici, encore 2 sous-groupes
  when 'f'
    # Ici, encore 10 autres sous-groupes
  end
end

C’est pas bon, hein ? Pour arranger ça, rien de tel qu’un peu de polymorphisme. La classe Opcode va donc se contenter de ceci :

class Opcode

    def initialize(opcode)
      asm = Assembly.new(opcode)
      @assembly = asm.to_s
    end

end

Vous devinez que c’est maintenant dans une nouvelle classe Assembly que sont géré les différentes instructions et sous instructions :

class Assembly

  def initialize(opcode)
    @opcode = opcode
    @assembly = build_assembly.to_s || ''
  end

  def to_s
    @assembly
  end

  private

  def build_assembly
    klass = Kernel.const_get('Asm' + @opcode[0])
    klass.new(@opcode)
  end

end

Et bien non, elles sont gérées chacune dans sa classe respective, à savoir Asm0, Asm1, Asm2, et cetera jusqu’à Asmf. Voici un exemple :

class Asm2 < AsmBase

  def to_s
    "CALL #{nnn}"
  end

end

Chacune des classes Asm0 à Asmf hérite de AsmBase qui définit le comportement commun (nnn, kk, x et y sont simplement des conventions de nommage en assembleur Chip-8) :

class AsmBase

  def initialize(opcode)
    @opcode = opcode
  end

  def nnn
    @opcode[1, 3]
  end

  def kk
    @opcode[2, 2]
  end

  def x
    @opcode[1]
  end

  def y
    @opcode[2]
  end

end

C’est un cas classique d’utilisation du polymorphisme. On troque un long switch/case (virtuellement infini) pour plusieurs petites classes simples. Le système est toujours aussi complexe dans son ensemble, mais sa maintenance est maintenant plus facile.