Freitag, 15. Oktober 2010

Information Hiding Principle by example

Einer der Grundpfeiler der objektorientierten Programmierung ist das Verbergen von Informationen. Für die Implementierung von Klassen ist die Konsequenz aus diesem Grundsatz, das Klassen und deren Mitglieder (Felder und Methoden) nur über eine schlanke Schnittstelle öffentlich zugänglich  sein sollen. Möglichst wenige Details einer Implementierung nach außen tragen und den Zugang zu der Klassenimplementierung so schwer zugänglich wie möglich zu programmieren ist eine "Best Practice". Clean Code Developer fühlen sich dieser "Best Practice" verpflichtet und wenden deshalb das Information Hiding Principle (IHP) an.

Je weniger Details der Verwender einer Klasse von außen sieht, umso geringer ist die Kopplung zwischen der Klassenimplementierung und dem Verwender der Klasse.Geringe Kopplung erhöht die Evolvierbarkeit und schafft isolierte Einheiten, die wiederum eine strenge Voraussetzung für Unit-Tests sind.

Ein Verstoß gegen die Kapselung wird oftmals durch öffentliche Felder signalisiert. Öffentliche Felder sind nicht nur aus Sicht der Evolvierbarkeit unschön, sondern bergen die Gefahr, in nebenläufigen Umgebungen zu zerbrechen. Dies führt häufig zu sporadischen Laufzeitfehlern, die nur sehr schwer zu debuggen und zu finden sind.Öffentliche Felder sind deshalb grundsätzlich zu vermeiden und ausschliesslich in geschützter Form durch Zugriffsmethoden (Getter/Setter) anzusprechen.

Vorteile des Information Hiding Principle (IHP) sind:
  1. Limitierung der Konsequenzen bei Änderungen
  2. Minimale Beeinflussung anderer Klassen bei einer Korrektur
  3. Wesentliche Erhöhung der Wiederverwendbarkeit
  4. Bessere Testbarkeit und höhere Zuverlässigkeit durch Isolation
Datenklassen sollen soweit möglich unveränderlich (immutable) entworfen werden. Dies erreicht man durch die Initialisierung der Felder einer Klasse im Konstruktor und der ausschliesslichen Definition von Getter-Methoden. Setter-Methoden, die eine Zustandsänderung herbeiführen können, sind in diesem Fall beim Entwurf zu vermeiden. Unveränderliche Klassen sind, ohne weitere Synchronisation, sicher in nebenläufigen Umgebungen anwendbar und weniger Fehleranfällig.

Kriterien für unveränderliche Klassen beinhalten:
  1. Keine Methoden, die den Status verändern (Setter)
  2. Ausschliesslich final Felder
  3. Alle Felder sind private
  4. Klasse ist final (nicht durch Vererbung erweiterbar)
Beispiel für eine einfache unveränderliche Klasse (ohne weitere Java-Verträge zu erfüllen) :

package ccd.java.idioms.immutable;

public final class ImmutableClass {

    private final String field;
   
    public ImmutableClass(String field) {
       
        this.field = field;
    }

    public String getField() {
       
        return field;
    }
}

Neben der Kapselung der Felder und Methoden einer Klasse ist es ratsam, die Sichtbarkeit auf die Klassenimplementierung einzuschränken. Grundsätzlich öffentliche Klassen zu verwenden ist fehleranfällig und kann ein Sicherheitsrisiko darstellen. Neben der Einschränkung der Klassensichtbarkeit ist es im Kontext von Sicherheitsanforderungen und der Klassenversionierung unter Umständen angebracht, Packages und JAR-Dateien zu versiegeln (sealed JARs). Versiegelte JAR-Dateien sind eine wesentliche Voraussetzung für die konsistente Versionierung von Klassen.

Eine dedizierte Sicht auf eine Klassenimplimentierung über eine schlanke Schnittstelle ist die optimale Voraussetzung für eine kompakte API. Diese Technik führt zu schnittstellengetriebenen Frameworks, bei der Factory-Methoden eine Sicht auf die öffentliche Schnittstelle einer Klassenimplementierung freigeben.

Öffentliche Schnittstelle für das Implementierungsbeispiel:

package ccd.java.idioms.hidden.implementation;

public interface PublicInterface {
  
    String implementation();
}

Implementierungsbeispiel mit zwei Ansätzen zum Verbergen einer Implementierung:

package ccd.java.idioms.hidden.implementation;

class PackagePrivateClass implements PublicInterface {

    @Override
    public String implementation() {
      
        return "hidden-implementation";
    }
}

public class PublicClass implements PublicInterface { 

    private PublicClass() {/* prevents instantiation */}
 
    public static PublicInterface newPrivateInstantiationInstance() {
     
        return new PublicClass();
    }
 
    @Override
    public String implementation() {
      
        return "hidden-implementation";
    }

    public static PublicInterface newPackagePrivateInstance() {
     
        return new PackagePrivateClass();
    }
}

Unit-Tests des Implementierungsbeispiels:

package ccd.java.idioms.hidden.implementation.test;

import static org.junit.Assert.*;
import org.junit.Test;
import ccd.java.idioms.hidden.implementation.PublicClass;
import ccd.java.idioms.hidden.implementation.PublicInterface;

public class HiddenImplementationTest {

    private static final String HIDDEN_IMPL = "hidden-implementation";

    @Test
    public void testHiddenWithPrivateInstantiation() {
     
        final PublicInterface hiddenWithPrivateInstantiation= PublicClass.newPrivateInstantiationInstance();
        assertEquals(HIDDEN_IMPL, hiddenWithPrivateInstantiation.implementation());
    }
  
    @Test
    public void testHiddenWithPackagePrivate() {
      
        final PublicInterface hiddenWithPackagePrivate= PublicClass.newPackagePrivateInstance();
        assertEquals(HIDDEN_IMPL, hiddenWithPackagePrivate.implementation());             
    }
}

Minimize the accessibility of classes, fields and methods is not only a good advice but still a best practice!


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