Mittwoch, 12. Januar 2011

Code Kata

Tai Chi Sportler führen regelmäßig Übungen zur Perfektion ihrer Bewegungsabläufe  durch. Der Bewegungsablauf ist bei den Tai Chi Übungen stilisiert und fest vorgegeben. Diese Art von Übungsform wird als Kata bezeichnet. Das häufige Wiederholen der Bewegungsabläufe bei einer Kata ist unerlässlich für den persönlichen Fortschritt eines Schülers und dient neben der Entspannung zur Vorbereitung auf einen Formwettkampf. Nur die regelmäßige Übung macht den Schüler zu einem Tai Chi Meister.

Programmierer nutzen das Konzept der Kata zur Verbesserung des Programmierstils. Die Programmierung beherrscht man nur durch ständiges Training. Eine regelmäßige Code Kata ist deshalb empfehlenswert und macht als öffentliches Code Dojo sehr viel Spass. Im Rahmen einer Diskussion des Clean Code Developer Forums über stinkenden Quellcode ist spontan ein Code Dojo entstanden.

Die Aufgabenstellung des Code Dojo war eine Code Kata mit dem Ziel einen einfachen Algorithmus, der eine arabische Zahl in eine römische Zahl konvertiert, zu refaktorisieren Der Ursprungsalgorithmus in der Java Notation umfasst folgende Implementierung:
public class RomanNumerals {
    
  static int[] nums = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
  static String[] rum = { "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX",
                          "V", "IV", "I" };

   public static string toRoman(int number) {
    
      String value = "";
      for (int i = 0; i < nums.length && number != 0; i++) {    
          while (number >= nums[i]) {
              number -= nums[i];
              value += rum[i];
          }
      }
    
      return value;
   }
} 
Die ersten Refactorings haben den Algorithmus auf die Leserlichkeit hin verbessert. Kleinere Methoden wurden konform den CCD-Prinzipien geschrieben, um den Lesefluss zu erhöhen. Das Single Level of Abstraction (SLA) Prinzip wurde dabei partiell verletzt. Relativ schnell erkannte man, dass die beiden Arrays zu fehleranfällig sind. Die Array-Elemente müssen in einer definierten Ordnung vorliegen und die Arrays könnten unabhängig voneinander weitere Elemente aufnehmen.

Erweiterte Implementierungen wie Value Objects, die die konvertierte römische Zahl speichern, ein selbständiges Mapping-Objekt und andere Implementierungen sind diskutiert worden. Damit das Refactoring stattfinden konnte und damit die Pfadfinderregel überhaupt angewendet werden kann, sind zunächst eine Reihe von Unit-Tests geschrieben worden.

Die Teilnehmer der Code Kata waren nach den ersten Implementierungen noch nicht mit den Ergebnissen des Refactorings zufrieden. Der einfache Konvertierungsalgorithmus wurde mit dem Fortschritt des Refactorings durch eindeutige Bezeichner zwar lesbarer, auf der anderen Seite blähte sich der Quellcode auf. Teilweise hat sich der Quellcode sogar verfünfacht. Die Balance zwischen der Lesbarkeit und Einfachheit der Implementierung war noch nicht gegeben. Ein Teilnehmer hat die beiden Arrays durch ein Mapping-Objekt ersetzt, sodass die Zuordnung einer arabischen Ziffer zu einer römischen Ziffer gewährleistet ist. Das Mapping-Objekt wurde dabei wiederum in einem Array verwaltet. Das Array fungierte dabei als ein Dictionary. Eine naheligende Idee war deshalb basierend auf dem Dictionary Gedanken eine geradlinige Java API-Lösung zu implementieren.

API-Lösung:
import java.util.LinkedHashMap;
import java.util.Map;

public class RomanNumber {

 private static Map<String, Integer> mappings = new LinkedHashMap<String, Integer>();
 static {
            
     mappings.put("M", 1000);
     mappings.put("CM", 900);
     mappings.put("D", 500);
     mappings.put("CD", 400);
     mappings.put("C", 100);
     mappings.put("XC", 90);
     mappings.put("L", 50);
     mappings.put("XL", 40);
     mappings.put("X", 10);
     mappings.put("IX", 9);
     mappings.put("V", 5);
     mappings.put("IV", 4);
     mappings.put("I", 1);
 }
        
 public static String arabicToRoman(int arabicNumber) {
    
     int arabicNumberRemainder = arabicNumber;
     final StringBuilder romanNumber = new StringBuilder();
        
     for (Map.Entry<String, Integer> mapping : mappings.entrySet()) {
            
        while(arabicNumberRemainder >= mapping.getValue()) {

              arabicNumberRemainder -= mapping.getValue();
              romanNumber.append(mapping.getKey());
         }
     }
        
     return romanNumber.toString();
  }
}
Eine LinkedHashMap bietet sich als Dictionary für das Mapping der Ziffern an. Die LinkedHashMap hält die Einfügeordnung aufrecht und garantiert damit, dass der Konvertierungsalgorithmus funktioniert. Die API-Lösung nutzt hauptsächlich das Mapping der Ziffern und ist deshalb hinsichtlich der Erweiterbarkeit sicherer als die Array-Variante. Grundsätzlich sind Lösungen, die auf einer Datenstruktur basieren dehnfähiger als Lösungen, die ausgefeilte Algorithmen verwenden. Das ist sicherlich bei dem hier vorliegenden einfachen Softwarebaustein kein Argument, kann sich aber bei umfangreicheren Softwarebausteinen durchaus auszahlen.

Die innere Struktur der Lösung konnte mit einer Abstraktion verbessert werden. Die Abstraktion erlaubt es darüber hinaus auf Basis der Implementierung weitere Konvertierungsalgorithmen zu implementieren.

Abstraktion der API-Variante:
import java.util.Map; 

abstract class NumberConverter {
  
  abstract Map<String, Number> convertMappings();
  abstract String convert(final Map<String, Number> mappings, final Number number);
  
  String convertNumber(final Number number) {  
   
     return convert(convertMappings(), number);
  }
}
Die Abstraktion wendet das Template Method Pattern an. Die überschriebenen Methoden in einer Ableitung der Abstraktion bestimmen, welche Konvertierungsdaten und welcher Konvertierungsalgorithmus für die Konvertierung verwendet wird. Durch diese Implementierung wird innere Flexibilität erreicht, sodass in der Folge unterschiedliche Ausprägungen von Konvertierungen stattfinden können.

Implementierung der API-Variante:
import java.util.LinkedHashMap;
import java.util.Map;

public final class RomanNumber extends NumberConverter {

  public static String arabicToRoman(int number) {

     return new RomanNumber().convertNumber(number);
  }

  private RomanNumber() {/* prevents instantiation */}

  @Override
  Map<String, Number> convertMappings() {
    
     final Map<String, Number> mappings = new LinkedHashMap<String, Number>();
    
     mappings.put("M", 1000);
     mappings.put("CM", 900);
     mappings.put("D", 500);
     mappings.put("CD", 400);
     mappings.put("C", 100);
     mappings.put("XC", 90);
     mappings.put("L", 50);
     mappings.put("XL", 40);
     mappings.put("X", 10);
     mappings.put("IX", 9);
     mappings.put("V", 5);
     mappings.put("IV", 4);
     mappings.put("I", 1);
    
     return mappings;
  }
 
  @Override
  String convert(final Map<String, Number> mappings, final Number number) {

     int numberRemainder = number.intValue();
     final StringBuilder convertedNumber = new StringBuilder();

     for (Map.Entry<String, Number> mapping : mappings.entrySet()) {

         while(numberRemainder >= mapping.getValue().intValue()) {

             numberRemainder -= mapping.getValue().intValue();
             convertedNumber.append(mapping.getKey());
         }
     }

     return convertedNumber.toString();
  }
} 
Unit-Test der API-Variante:
import static org.junit.Assert.assertEquals;

import java.util.HashMap;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;

public class RomanNumberTest {

   private Map<String, Integer> mappings = new HashMap<String, Integer>();

   @Before
   public void setUp() {

      mappings.put("", 0);
      mappings.put("I", 1);
      mappings.put("II", 2);
      mappings.put("III", 3);
      mappings.put("IV", 4);
      mappings.put("V", 5);
      mappings.put("VI", 6);
      mappings.put("VIII", 8);
      mappings.put("IX", 9);
      mappings.put("X", 10);
      mappings.put("XI", 11);
      mappings.put("XL", 40);
      mappings.put("L", 50);
      mappings.put("LXXX", 80);
      mappings.put("XC", 90);
      mappings.put("C", 100);
      mappings.put("D", 500);
      mappings.put("CMXCIX", 999);
      mappings.put("M", 1000);
      mappings.put("MI", 1001);
      mappings.put("MDCCCLXXVIII", 1878);
      mappings.put("MCMLIII", 1953);
      mappings.put("MCMLXXXIV", 1984);
      mappings.put("MMM", 3000);
      mappings.put("MMMMMM", 6000);
      mappings.put("MMMMMMMMMCMXCIX", 9999);
      mappings.put("MMMMMMMMMM", 10000);
      mappings.put("MMMMMMMMMMI", 10001);
   }

   @Test
   public void testArabicToRomanNumberConvertion() {

      for(Map.Entry<String, Integer> mapping : mappings.entrySet()) {

         assertEquals(mapping.getKey(), 
                      RomanNumber.arabicToRoman(mapping.getValue()));
      }
   }
}
Die Implementierung der Klasse "RomanNumber" ist durch Vererbung nicht erweiterbar und kann aufgrund des privaten Konstruktors nicht instanziiert werden. Der einzige Zugang zu der Klasse ist die Methode "arabicToRoman". Die Verriegelung ist notwendig, damit die Klasse nicht durch Instanziierung oder Vererbung gebrochen wird.

Eine Performanzbetrachtung wurde bei der Implementierung der API-Variante nicht durchgeführt. Eine deutliche Optimierung könnte durch das einmalige Vorhalten der Konvertierungsdaten erreicht werden. Keine klar definierten, nichtfunktionalen Anforderungen ergeben unterschiedliche Lösungen. Bei der Code Kata ist der Verwender des Softwarebausteins und dessen Anforderungen nicht benannt worden, deshalb sind viele kreative und rein funktional korrekte Implementierungen entstanden.

Tipp: Bei Softwarebausteinen, die produktiv eingesetzt werden, sind der Verwender des Softwarebausteins und dessen Anforderungen die treibenden Kräfte. Ein Softwarebaustein sollte deshalb immer aus Sicht des Verwenders implementiert werden.

Die Code Kata hat verdeutlicht, dass viele Wege zum Ziel führen und je nach Implementierung die eine oder andere Lösung die Nase vorn hat. Eine Kata in einem Code Dojo ist unabhängig  von der Qualität der gefundenen Lösungen ein gutes Mittel, um sich mit anderen Entwicklern auszutauschen und dabei seinen eigenen Programmierstil zu verbessern.


Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.