Dienstag, 7. September 2010

Liskov Substitution Principle by example

Professorin Barbara Liskov und Professorin Jeannette Wing haben das liskovsche Substitutionsprinzip formuliert. Das liskovische Subsitutionsprinzip ist ein wichtiges Kriterium der objektorientierten Programmierung.

Die wesentliche Kernaussage des Prinzips ist, dass dort wo eine Basisklasse verwendet wird, stellvertretend für die Basisklasse auch eine abgeleitete Klasse stehen darf. Das klingt selbstverständlich, ist es aber nicht. Vielmehr wird hier der Blick auf den Vertrag zwischen einer Basisklasse und seiner Ableitung gerichtet. Eine Basisklasse kann nur dann durch eine Ableitung ersetzt werden, wenn die abgeleitete Klasse den gleichen Vertrag wie die Basisklasse erfüllt. Die abgeleitete Klasse darf die Sicht auf die Basisklasse deshalb keinesfalls einschränken.

Abgeleitete Klassen dürfen Funktionalitäten ihrer Basisklasse überschreiben, einhalten oder abschwächen, aber niemals verschärfen. Eine Referenz auf eine Instanz der Basisklasse kann stets auch auf eine Instanz der abgeleiteten Klasse verweisen, wenn die Verträge beim Überschreiben von Methoden eingehalten wurden.

Typisch für die Verletzung des liskovischen Substitutionsprinzip ist der Versuch, die Sichtbarkeit einer Methode in einer abgeleiteten Klasse einzuschränken. Bei der Programmiersprache Java führt das zu einem Kompilierfehler.

Einschränken der Sichtbarkeit führt zu einem Kompilierfehler

class LspBaseBean {

    protected void lspMethod() {}
}

class LspBean extends LspBaseBean {
   
    private void lspMethod() {}
}

Kompilierfehler: Cannot reduce the visibility of the inherited method from LspBaseBean.

Eine weitere Verletzung des Prinzips ist durch die Verwendung einer Checked-Exception bei einer Methode der Basisklasse möglich, die in der überschriebenen Methode der abgeleiteten Klasse nicht deklariert worden ist. In diesem Fall ist die Basisklasse nicht mehr ohne Kompilierfehler durch die abgeleitete Klasse austauschbar.

Klassenstruktur:

class LspBaseBean {

    protected void lspMethod() throws IOException {
       
        throw new IOException();
    }
}

class LspBean extends LspBaseBean {
   
    protected void lspMethod() {}
}

Testfall:

import java.io.IOException;
import org.junit.Test;

public class LspBeanTest {
   
    @Test
    public void testLspBaseBean() {
       
        final LspBaseBean lsp = new LspBaseBean();
       
        try {
           
            lsp.lspMethod();
        }
        catch (IOException ex) {
           
            ex.printStackTrace();
        }
    }
   
    @Test
    public void testLspBean() {
       
        final LspBean lsp = new LspBean();
       
        try {
           
            lsp.lspMethod();
        }
        catch (IOException ex) {
           
            ex.printStackTrace();
        }
    }
}

Kompilierfehler: Unreachable catch block for IOException. This exception is never thrown from the try statement body.

Ein komplexeres Konstrukt entsteht, wenn man einen Typ der Basisklasse als Parameter an eine Methode übergibt und die Methode jeweils für abgeleitete Klassen erweitert werden muss. Dies äußert sich häufig durch die Verwendung von instanceof in derartigen Methoden.

Klassenstruktur:

class LspViolationBase {

    protected String baseMethod() {
      
        return "baseMethod()";
    }
}

class LspViolation extends LspViolationBase {

    protected String subMethod() {
      
        return "subMethod()";
    }
}

Testfall:

import static org.junit.Assert.*;
import org.junit.Test;

public class LspViolationTest {
  
    @Test
    public void testLspViolationBase() {
      
        assertEquals("baseMethod()", callLspMethod(new LspViolationBase()));
    }
  
    @Test
    public void testLspViolation() {
      
        assertEquals("subMethod()", callLspMethod(new LspViolation()));
    }
  
    @Test(expected=IllegalArgumentException.class)
    public void testLspGuardClause() {
      
        assertEquals("", callLspMethod(null));
    }
  
    private String callLspMethod(final LspViolationBase lspBase) {
      
        guardClause(lspBase);
      
        if(lspBase instanceof LspViolation) {
          
            return(((LspViolation)lspBase).subMethod());
        }
      
        return(lspBase.baseMethod());          
    }

    private void guardClause(final LspViolationBase lspBase) {
      
        if(null == lspBase) {
          
            throw new IllegalArgumentException();
        }
    }
}

Die Methode callLspMethod(...) müsste im vorliegenden Fall für jede weitere Ableitung der Basisklasse LspViolationBase angepasst werden, sodass in diesem Fall ebenfalls ein Verstoß gegen das liskovische Substitutionsprinzip vorliegt.


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