Mittwoch, 8. September 2010

Optimize early with focus on code quality

Die Optimierung von Quellcode fördert häufig nicht die Lesbarkeit einer Software. Grundsätzlich gilt die Verständlichkeit von Quellcode über Optimierungen zu stellen. Michael Anthony Jackson hat zwei Regeln aufgestellt, über die es sich lohnt nachzudenken.

Rule 1: Don't do it.
Rule 2 (for experts only): Don't do it yet.

Den Regeln steht entgegen, dass die Antwortzeit einer Anwendung wesentlich für die Benutzerakzeptanz ist. Andererseits sind die Regeln einzuhalten, sodass sich als Schlussfolgerung folgende Regel ergibt:

Optimize early during the design and coding sessions, but still have the main focus on code quality.

Diese Regel ist weniger scharf  und ohne die Gefahr schlechte Antwortzeiten hervorzubringen. Es ist ratsam, während dem Software-Design performanzkritische Stellen herauszuarbeiten und zu behandeln. Die Anzahl der Benutzer einer Applikation, die Datenlast, Spitzenzeiten und nicht funktionale Anforderungen sind bei dieser Betrachtung heranzuziehen. In der Folge, zwei einfache Beispiele, wie Optimierungen früh bei der Software-Implementierung beachtet werden können.

In Applikationen werden oft fixe Stammdaten verarbeitet, sodass sich ein LRU-Cache (Least Recently Used Cache) zur Speicherung und schneller Abfragen eignet. Mit der Programmiersprache Java ist ein LRU-Cache zügig programmiert. Die Anzahl der Speicherplätze in dem Cache bleibt dabei konstant. Beim Speichervorgang im LRU-Cache werden die Daten, auf die am längsten nicht zugegriffen wurde, durch neue Daten verdrängt.

Schnittstelle des LRU-Cache:

package ccd.jee.cache;

public interface Cache<K,V> {

    public V store(K key, V value);
    public V read(K key);
    public V delete(K key);
    public V update(K key, V value);
    public void clear();
    public int size();
}

Ausnahme des LRU-Caches:

package ccd.jee.cache;

public class LruCacheException extends IllegalArgumentException {

    public enum EXCEPTION_REASON {
       
        ILLEGAL_KEY("Missing key for cache operation!"),
        ILLEGAL_VALUE("Missing value for cache operation!");
       
        private final String reason;

        private EXCEPTION_REASON(String reason) {
           
            this.reason = reason;
        }

        public String getReason() {
            return reason;
        }           
    }
   
    public LruCacheException(EXCEPTION_REASON reason) {
       
        super(reason.getReason());
    }
}

Implementierung des LRU-Caches:

package ccd.jee.cache;

import java.util.LinkedHashMap;
import java.util.Map;
import ccd.jee.cache.LruCacheException.EXCEPTION_REASON;

public class LruCache<K,V> implements Cache<K,V> {

    private static final boolean ACCESS_ORDER = true;
    private static final float LOAD_FACTOR = 0.75f;
  
    private final Map<K, V> cache;
  
    public LruCache(final int maxCapacity) {
      
        cache = new LinkedHashMap<K, V>(maxCapacity, LOAD_FACTOR, ACCESS_ORDER) {

            @Override
            protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
                return size() > maxCapacity;
             }
        };
    }
  
    @Override
    public V store(K key, V value) {
          
        guardClause(key, value);
      
        return cache.put(key, value);
    }

    @Override
    public V update(K key, V value) {      
      
        return store(key, value);
    }

    @Override
    public V read(K key) {
      
        guardClause(key);
      
        return cache.get(key);
    }

    @Override
    public V delete(K key) {
      
        guardClause(key);
      
        return cache.remove(key);
    }
  
    @Override
    public void clear() {
      
        cache.clear();
    }
  
    @Override
    public int size() {  
      
        return cache.size();
    }
  
    private void guardClause(K key) {
      
        if(null == key) {
          
            throw new LruCacheException(EXCEPTION_REASON.ILLEGAL_KEY);
        }
    }
  
    private void guardClause(K key, V value) {
      
        guardClause(key);

        if(null == value) {
            
            throw new LruCacheException(EXCEPTION_REASON.ILLEGAL_VALUE);
        }
    }
}

Unit-Test des LRU-Caches:

package ccd.jee.cache;

import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;

public class TestCache {
   
    private static final int CACHE_CAPACITY = 4;
    private static final String VALUE = "value-";
    private static final String KEY = "key-";   
    private static final String KEY_1 = "key-1";
    private static final String VALUE_4 = "value-4";
    private static final String KEY_4 = "key-4";
    private static final String VALUE_5 = "value-5";
    private static final String KEY_5 = "key-5";
   
    private Cache<String, String> cache;
   
    @Before
    public void setUp() {
   
        testFixture();   
    }

    private void testFixture() {
       
        cache = new LruCache<String,String>(CACHE_CAPACITY);
       
        for(int i = 1; i <= CACHE_CAPACITY + 1; i++) {
       
            cache.store(KEY + i, VALUE + i);
        }       
    }
   
    @Test
    public void testSize() {
       
        assertEquals(4, cache.size());       
    }
   
    @Test
    public void testReadLastStored() {
       
        final String entry = cache.read(KEY_5);       
        assertEquals(VALUE_5, entry);       
    }
   
    @Test
    public void testReadStored() {
       
        final String entry = cache.read(KEY_4);       
        assertEquals(VALUE_4, entry);       
    }
   
    @Test
    public void testReadOldestStored() {
       
        final String entry = cache.read(KEY_1);       
        assertEquals(null, entry);       
    }
   
    @Test
    public void testUpdate() {
       
        final String entry = cache.read(KEY_5);       
        assertEquals(VALUE_5, entry);       
    }
   
    @Test
    public void testDelete() {
       
        final String entry = cache.delete(KEY_5);       
        assertEquals(VALUE_5, entry);       
    }
   
    @Test(expected=LruCacheException.class)
    public void wrongKey() {

        cache.store(null, VALUE_5);
    }
   
    @Test(expected=LruCacheException.class)
    public void wrongValue() {

        cache.store(KEY_5, null);
    }
   
    @Test
    public void testClear() {
   
        cache.clear();
        assertEquals(0, cache.size());       
    }
}

Der LRU-Cache ist in Java SE und Java EE Anwendungen einsetzbar. Im Java EE 5 Umfeld bietet sich eine "Stateful Session Bean" als Komponente an, die die Cache-Schnittstelle öffentlich zugänglich macht. Besser ist die Java EE 6 Lösung mit einem "Singleton Bean", weil dabei nur eine einzige Instanz des Caches angesprochen wird. Der Cache ist im Applikationsserver in einer Bean Lifecycle-Methode noch vor der Bereitstellung mit den Daten aus einer Datenbank zu initialisieren, sodass nachfolgend bei der Verwendung des Caches keine Datenbankzugriffe mehr notwendig sind.

Der Einsatz eines Caches ist sicherlich früh in der Designphase einer Applikation bekannt. Die Entscheidung für einen Cache wird durch die Analyse von Spitzenzeiten und dem Mengengerüst an Daten geprägt. Wesentlich ist, dass der Cache korrekt implementiert wird. Dies ist bei einem LRU-Cache mit definierter Kapazität ein geringeres Problem als bei Caches mit variabler Kapazität, die beliebige Datenmengen aufnehmen können. Durch die schlechtere Speicherausnutzung bei Caches mit variabler Kapazität kann es wiederum zu Performanzproblemen bis hin zum "OutOfMemoryError" kommen.

Performanz in einer Applikation zu erreichen wird wesentlich durch die Wahl der richtigen Collection  geprägt. Sofern der Verwender einer Collection den Collection-Vertrag einhält, ergibt sich eine höhere Datenzugriffsgeschwindigkeit. Der Unterschied der Zugriffsgeschwindigkeit zwischen einer Liste, einem Set und einer Map ist deutlich und wird umso deutlicher bei der Verwaltung großer Datenmengen. Die Collection API zu kennen ist deshalb notwendig für die Programmierung performanter Java-Applikationen.

Im Bereich der Datentypen  ist die String Klasse unveränderlich (Immutable). Beim Zusammenbau von Strings werden jeweils neue String Objekte angelegt. Java optimiert intern das String-Handling durch einen String-Pool, in dem gleiche Strings nur einmal abgelegt werden. Dennoch ist es wichtig, die Performanzunterschiede beim Zusammenbau von Strings zu kennen.

Langsamer Zusammenbau von Strings (Quellcodeauszug):

private long concatStrings(String... strings) {

        measure.start();
        for (String str : strings) {

            str += str;
        }
        measure.end();

        return measure.getResult();
}

Schnellerer synchronisierter Zusammenbau von Strings (Quellcodeauszug):

private long concatWithStringBuffer(String... strings) {

        measure.start();
        final StringBuffer strBuffer = new StringBuffer(strings.length);
        for (String str : strings) {

            strBuffer.append(str);
        }
        measure.end();

        return measure.getResult();
}

Noch schnellerer aber nicht synchronisierter Zusammenbau von Strings (Quellcodeauszug):

private long concatWithStringBuilder(String... strings) {

        measure.start();
        final StringBuilder strBuilder = new StringBuilder(strings.length);
        for (String str : strings) {

            strBuilder.append(str);
        }
        measure.end();

        return measure.getResult();
}

Bei der folgenden Variante (Zusammenbau mit +) wird seit Java 5 der StringBuilder zur Optimierung herangezogen. Der Vorteil bei der Verwendung dieser Variante ist, dass bei zukünftigen Optimierungen, kein Änderungsaufwand im Quellcode entsteht.

Java intern optimierter Zusammenbau von Strings (wird intern mit dem StringBuilder umgesetzt):

private String concat(String s1, String s2, String s3) { 

         return s1 + s2 + s3;
}

Once again, know the API and do it right!


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