Before/After Patterns

Några pattern som är användbara vid trådprogrammering.

Litteratur

Avsnitt 1.4 i Lea: Concurrent Programming in Java.

Vad är ett pattern?

Ett pattern (det skulle väl heta mönster på svenska men jag har aldrig sett det översatt) är helt enkelt en generell lösning som kan tillämpas på problem av en viss typ. Här handlar det om design patterns, dvs förslag på programvarudesign. Ett design pattern är vanligtvis en liten objektstruktur, dvs några interface och klasser som relaterar till varandra på något visst sätt. En bok med patterns kan ses som en kokbok där varje pattern är ett recept på en listig programvarudesign och en specifikation av tillfällen när den är bra att använda.

Vad är ett Before/After pattern?

Before/After patterns är en typ av patterns som är användbara bland annat när invariants ska kontrolleras. Det villkor som en invariant beskriver kontrolleras innan en metod utförs och efter att den har utförts. Om en metod method() ska utföras skriver vi koden på följande sätt:

before();
try { method(); }
finally { after(); }

På det sättet kommer kontrollerna i before() alltid att utföras innan själva metoden och kontrollerna i after() att utföras efter, även om method() kastar ett undantag. I fallet med en invariant är det samma kontroll som kommer att utföras i before() och after() och de kan därför slås ihop till en metod.

Som exempel används ett program som beskriver en vattentank. Vi har en invariant som säger att vattenmängden i tanken måste vara mellan noll och tankens volym. Först definierar vi ett undantag som kan kastas om villkoret inte uppfylls:
class AssertionError extends java.lang.Error {
    public AssertionError() { super(); }
    public AssertionError(String message) { super(message); }
}

Sedan definierar vi ett interface som beskriver tanken:
interface Tank {
    float getCapacity();
    float getVolume();
    void  transferWater(float amount);
}

Låt oss nu titta på några patterns som beskriver listiga sätt att använda dessa.

Adapters

Antag att vi har en klass TankImpl som implementerar interfacet Tank men inte utför kontrollen av vattenvolymen. Om vi nu ska lägga till kontrollen kan vi, istället för att skriva om TankImpl, använda en adapter, låt oss kalla den AdaptedTankImpl. Adaptern implementerar även den interfacet Tank, dessutom har den en referens till ett objekt av klassen TankImpl dit den vidarebefodrar anropet av transferWater(). AdaptedTankImpl lägger till kontrollen av vattenvolymen, men anropen av transferWater() sker precis som tidigare!

Här kommer ett klassdiagram:

Och ett kodexempel:
class AdaptedTankImpl implements Tank {
    protected final Tank delegate;

    public AdaptedTankImpl(Tank t) { delegate = t; }

    public float getCapacity() { return delegate.getCapacity(); }

    public float getVolume() { return delegate.getVolume(); }

    protected void checkVolumeInvariant() throws AssertionError {
        /* Replaces berfore() and after(). */
        float v = getVolume();
        float c = getCapacity();
        if ( !(v >= 0.0 && v <= c) )
            throw new AssertionError();
    }

    public synchronized void transferWater(float amount) {

        checkVolumeInvariant();  // before-check

        try {
            delegate.transferWater(amount);
        }

        // The exceptions will be re-thrown, but the finally clause will
        // be executed first.
        catch (OverflowException ex)  { throw ex; }
        catch (UnderflowException ex) { throw ex; }

        finally {
            checkVolumeInvariant(); // after-check
        }
    }

}

Metoderna before() och after() är sammanslagna till metoden checkVolumeInvariant(). Poängen med att TankImpl och AdaptedTankImpl implementerar samma interface är att vi överallt kan byta ut TankImpl mot AdaptedTankImpl utan att behöva ändra någonting i koden. Rader av typen Tank t = new TankImpl() ändras bara till Tank t = new AdaptedTankImpl( new TankImpl() ).

Förutom att vi nu har lärt oss ett användbart pattern har vi sett ett exempel på något ohyggligt viktigt: Skillnaden mellan definition (interface) och implementation (klass). Tack vare interfacet Tank blev det väldigt lätt att byta TankImpl mot AdaptedTankImpl. Använd interface!!

Subclassing

Mycket snarlikt exemplet ovan, men istället för att AdaptedTankImpldelegerar till en instans av TankImpl låter vi den ärva av TankImpl. Klassdiagrammet blir då som följer:
Koden blir identisk med den ovanstående bortsett från att anropet av transferWater() i try-blocket byts från delegate.transferWater(amount); till super.transferWater(amount);

Dessa två pattern (adapters och subclassing) illustrerar skillnaden mellan två grundstenar inom objektorienterad programmering: delegering och arv.