blog.smart-java.nl
Ordina J-Technologies – Java Blog

Archief ‘patterns’ categorie




Immutable lists

Door: Pieter van der Meer, 26 March 2010

The last couple of years there is more and more talk about concurrency. One of the main issues is the access data from the various threads that you have running.

Brian Goetz has a simple statement on how to solve this:  Use immutable objects where ever possible. Although true it is not always as simple to implement your data structures immutable. especially when you are working with Collections (List/Set/Map).

Making the collection variable immutable is easy,
(more…)




Temporale data – deel 4: bitemporale collections

Door: gnoij, 7 December 2009

Het is alweer enige tijd geleden dat ik heb geschreven over temporale patterns. In dit deel wil ik het bitemporale pattern uitbreiden met bitemporale collections. Werd met de bitemporale property nog een enkele property historisch gemaakt, in dit deel maken we een lijst historisch. Dat wil zeggen dat er meerdere elementen van een historische lijst tegelijkertijd geldig kunnen zijn.

De klasse BiTemporalObject uit het derde deel blijft hetzelfde. En naast de BiTemporalProperty voor enkelvoudige historische relaties krijgen we nu ook een klasse BiTemporalCollection voor meervoudige historische relaties.

BiTemporalCollection

public class BiTemporalCollection {
 
    protected List alleHistorischeVersies = new ArrayList();
 
    public BiTemporalCollection() {
        super();
    }
 
    public List getActueleVersies() {
        return getVersieOp(new Date(), new Date());
    }
 
    public List getVersieOp(final Date registratieTijdstip, final Date peilDatum) {
        return (List) CollectionUtils.select(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                return ((BiTemporalObject) object).isGeldigOp(registratieTijdstip, peilDatum);
            }
        });
    }
 
    public void voegActueleVersieToe(BiTemporalObject actueleVersie) {
        if (actueleVersie == null) {
            throw new IllegalArgumentException("actueleVersie is null");
        }
        this.alleHistorischeVersies.add(actueleVersie);
    }
 
    public void wijzigActueleVersie(BiTemporalObject nieuweVersie, BiTemporalObject oudeVersie) {
        if (nieuweVersie == null) {
            throw new IllegalArgumentException("nieuweVersie is null");
        }
        if (oudeVersie == null) {
            throw new IllegalArgumentException("oudeVersie is null");
        }
        beeindigVorigeVersie(nieuweVersie, oudeVersie);
        this.alleHistorischeVersies.add(nieuweVersie);
    }
 
    private void beeindigVorigeVersie(final BiTemporalObject nieuweVersie, final BiTemporalObject oudeVersie) {
 
        BiTemporalObject afTeVoerenVersie = zoekActueleVersie(oudeVersie, nieuweVersie.getIngangsdatum(), nieuweVersie.getOpvoerTijdstip()); 
 
        if (afTeVoerenVersie == null) {
            throw new IllegalStateException("Geen oude actuele versie gevonden!");
        }
 
        beeindigVersie(afTeVoerenVersie, nieuweVersie.getIngangsdatum(), nieuweVersie.getOpvoerTijdstip());
    }
 
    public void beeindig(final BiTemporalObject actueleVersie, final Date einddatum) {
        BiTemporalObject afTeVoerenVersie = zoekActueleVersie(actueleVersie, einddatum, new Date());
        if (afTeVoerenVersie == null) {
            throw new IllegalStateException("Geen oude actuele versie gevonden!");
        }
        beeindigVersie(afTeVoerenVersie, einddatum, new Date());
    }
 
    public void beeindig(final Date einddatum) {
        List afTeVoerenVersies = (List) CollectionUtils.select(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                BiTemporalObject versie = (BiTemporalObject) object;
                return versie.isGeldigOp(new Date(), einddatum);
            }
        });
 
        CollectionUtils.forAllDo(afTeVoerenVersies, new Closure() {
            public void execute(Object input) {
                beeindigVersie((BiTemporalObject) input, einddatum, new Date());
            }
        });
    }
 
    private void beeindigVersie(BiTemporalObject versie, Date einddatum, Date registratieTijdstip) {
        BiTemporalObject kopieVersie = versie.kopieer(einddatum, registratieTijdstip);
        this.alleHistorischeVersies.add(kopieVersie);
    }
 
    private BiTemporalObject zoekActueleVersie(final BiTemporalObject actueleVersie, final Date peildatum, final Date registratieTijdstip) {
        return (BiTemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                BiTemporalObject versie = (BiTemporalObject) object;
                return versie.equals(actueleVersie) && versie.isGeldigOp(registratieTijdstip, peildatum);
            }
        });
    }

De wijzigingen ten opzichte van de BiTemporalProperty uit het vorige deel zijn dat er nu niet één maar meer versies tegelijkertijd geldig kunnen zijn (getActueleVersies) en dat we versies kunnen toevoegen (voegActueleVersieToe) en kunnen wijzigen (wijzigActueleVersie). Bij deze laatste methode we de te wijzigen versie meegeven, zodat deze beëindigd kan worden en de nieuwe versie toegevoegd wordt. Ook kunnen we alle versies van de collection beëindigen (beeindig) of een enkele versie (die dan weer aan de methode meegegeven moet worden).

Een voorbeeld

In dit voorbeeld wordt het voorbeeld uit deel 3 uitgebreid met een manager (zelf ook een werknemer), die meerdere werknemers onder zich heeft.

Manager

public class Manager extends Werknemer {
 
    private static final long serialVersionUID = 1L;
 
    private BiTemporalCollection werknemers = new BiTemporalCollection();
 
    public Manager() {
        super();
    }
 
    public Manager(String naam) {
        super(naam);
    }
 
    public void voegWerknemerToe(Werknemer werknemer, Date ingangsdatum) {
        this.werknemers.voegActueleVersieToe(new WerknemerRelatie(werknemer, ingangsdatum));
    }
 
    public List getTeManagenWerknemers() {
        List relaties = getWerknemerRelaties();
        CollectionUtils.transform(relaties, new Transformer() {
            public Object transform(Object object) {
                return ((WerknemerRelatie)object).getWerknemer();
            }
        }) ;
        return relaties;
    }
 
    public List getTeManagenWerknemers(Date registratieTijdstip, Date peildatum) {
        List relaties = getWerknemerRelaties(registratieTijdstip, peildatum);
        CollectionUtils.transform(relaties, new Transformer() {
            public Object transform(Object object) {
                return ((WerknemerRelatie)object).getWerknemer();
            }
        }) ;
        return relaties;
    }
 
    public List getWerknemerRelaties() {
        return this.werknemers.getActueleVersies();
    }
 
    public List getWerknemerRelaties(Date registratieTijdstip, Date peildatum) {
        return this.werknemers.getVersieOp(registratieTijdstip, peildatum);
    }
 
    public void wijzigWerknemer(Werknemer nieuweWerknemer, Werknemer oudeWerknemer, Date wijzigingsdatum) {
        WerknemerRelatie nieuweRelatie = new WerknemerRelatie(nieuweWerknemer, wijzigingsdatum);
        WerknemerRelatie oudeRelatie = bepaalWerknemerRelatie(oudeWerknemer);
        this.werknemers.wijzigActueleVersie(nieuweRelatie, oudeRelatie);
    }
 
    private WerknemerRelatie bepaalWerknemerRelatie(final Werknemer werknemer) {
        return (WerknemerRelatie)CollectionUtils.find(this.werknemers.getActueleVersies(), new Predicate() {
            public boolean evaluate(Object obj) {
                return ((WerknemerRelatie)obj).getWerknemer().equals(werknemer);
            }
        });
    }
 
    public void stopManagenVanWerknemer(Werknemer werknemer, Date einddatum) {
        WerknemerRelatie relatie = bepaalWerknemerRelatie(werknemer);
        this.werknemers.beeindig(relatie, einddatum);
    }
 
    public void stopManagen(Date einddatum) {
        this.werknemers.beeindig(einddatum);
    }
}

Deze klasse representeert de manager. Als een manager een werknemer gaat managen, wordt de werknemer toegevoegd aan de lijst van te managen werknemers (voegWerknemerToe). Als een werknemer wordt vervangen door een andere werknemer wordt de methode wijzigWerknemer aangeroepen, waardoor de relatie met de oude werknemer wordt beëindigd en de nieuwe werknemer wordt begonnen. Als een werknemer uit dienst gaat of naar een andere afdeling, stopt de manager met het managen van deze werknemer (stopManagenVanWerknemer), waardoor de historische relatie van de manager met de werknemer wordt beëindigd. Als de manager stopt met managen (stopManagen), wordt de relatie met alle gemanagede werknemers beëindigd.

De historische relatie van de manager met de werknemer wordt gerepresenteerd door de klasse WerknemerRelatie.

WerknemerRelatie

public class WerknemerRelatie extends BiTemporalObject {
 
    private static final long serialVersionUID = 1L;
 
    private Werknemer werknemer;
 
    protected WerknemerRelatie() {
        super();
    }
 
    protected WerknemerRelatie(Werknemer werknemer, Date ingangsdatum) {
        super(ingangsdatum);
 
        this.werknemer= werknemer;
    }
 
    public Werknemer getWerknemer() {
        return this.werknemer ;
    }
}

De test ziet er als volgt uit:

public void testManager() {
 
	Manager jan = new Manager("Jan");
 
	// simuleer de opvoer tijd 1-1-2003
	Date aanvangsdatum = DateUtils.maakDate(2003, 1, 1);
	BiTemporalObject.TEST_OPVOER_TIJDSTIP = aanvangsdatum; 
 
	Afdeling inkoop = new Afdeling("Inkoop");
	jan.setAfdeling(inkoop, aanvangsdatum);
 
	Werknemer kees = new Werknemer("Kees");
	kees.setAfdeling(inkoop, aanvangsdatum);
	jan.voegWerknemerToe(kees, aanvangsdatum);
 
	Werknemer piet = new Werknemer("Piet");
	piet.setAfdeling(inkoop, aanvangsdatum);
	jan.voegWerknemerToe(piet, aanvangsdatum);
 
	assertEquals(2, jan.getTeManagenWerknemers().size());
	assertTrue(jan.getTeManagenWerknemers().contains(kees));
	assertTrue(jan.getTeManagenWerknemers().contains(piet));
 
	Date wijzigingsdatum = DateUtils.maakDate(2006, 1, 1);
	BiTemporalObject.TEST_OPVOER_TIJDSTIP = wijzigingsdatum; 
 
	Werknemer johan = new Werknemer("Johan");
	johan.setAfdeling(inkoop, wijzigingsdatum);
 
	// Johan vervangt Piet, die uit dienst gaat
	jan.wijzigWerknemer(johan, piet, wijzigingsdatum);
	piet.uitDienst(wijzigingsdatum);
 
	assertEquals(2, jan.getTeManagenWerknemers().size());
	assertTrue(jan.getTeManagenWerknemers().contains(johan));
	assertFalse(jan.getTeManagenWerknemers().contains(piet));
 
	// jan managete gisteren piet nog wel
	Date gisteren = DateUtils.maakDate(2005, 12, 31);
	assertTrue(jan.getTeManagenWerknemers(new Date(), gisteren).contains(piet));
 
	Date einddatum = DateUtils.maakDate(2009, 1, 1);
	BiTemporalObject.TEST_OPVOER_TIJDSTIP = wijzigingsdatum; 
 
	jan.stopManagenVanWerknemer(kees, einddatum);
	kees.uitDienst(einddatum);
 
	assertEquals(1, jan.getTeManagenWerknemers().size());
	assertFalse(jan.getTeManagenWerknemers().contains(kees));
 
	jan.stopManagen(einddatum);
	assertTrue(jan.getTeManagenWerknemers().isEmpty());
}

In deze test maken we een manager Jan aan, die werkt op de afdeling Inkoop op 1 januari 2003. Vanaf deze dag begint hij ook met het managen van twee werknemers Kees en Piet. Vanaf 1 januari 2006 vervangt een nieuwe werknemer Johan Piet, die uit dienst gaat. Op 1 januari 2009 stopt Jan met het managen van al zijn werknemers.

Hibernate mappings

De hibernate mapping van de klasse WerknemerRelatie is analoog aan de mapping van de AfdelingRelatie uit deel 3.

De mapping van de werknemer is uitgebreid met de subclass Manager, die de werknemers als BiTemporalCollection property mapt.

Werknemer.hbm.xml

<hibernate-mapping package="nl.ordina.bitemporal.example" default-access="field">
	<class name="Werknemer" table="Werknemer" discriminator-value="Werknemer">
		<id name="id" column="id">
			<generator class="identity"></generator>
		</id>
		<discriminator column="type" />
		<version name="versie" />
 
		<!-- properties -->
		<property name="naam" column="naam" />
 
		<!-- de verwijzing naar de afdelingrelatie -->
		<component name="afdelingRelatie" 
			class="nl.ordina.temporal.bitemporal.BiTemporalProperty"
			lazy="false">
			<bag name="alleHistorischeVersies"
				cascade="all, delete-orphan" >
				<key column="werknemerId" />
				<one-to-many
					class="AfdelingRelatie" />
			</bag>
		</component>
 
		<subclass name="Manager" discriminator-value="Manager">
		<!-- de verwijzing naar de werknemerrelatie -->
			<component name="werknemers" 
				class="nl.ordina.temporal.bitemporal.BiTemporalCollection"
				lazy="false">
				<bag name="alleHistorischeVersies"
					cascade="all, delete-orphan" >
					<key column="managerId" />
					<one-to-many
						class="WerknemerRelatie" />
				</bag>
			</component>
		</subclass>
	</class>
</hibernate-mapping>

LET OP: In bovenstaand artikel wordt gesproken over BiTemporalCollection. Dit is niet dezelfde als de BiTemporalCollection van Fowler , wat eigenlijk de BiTemporalProperty uit deel 3 is.




Singleton Smells

Door: Jan-Kees van Andel, 19 April 2009

I really hate the Singleton pattern. It makes code hard to test, it has nothing to do with general OO principles and the Singletons I see daily (written by others of course :) ) usually look very procedural.

People who use Singletons in Java all the time have more fundamental problems (I’m not talking about singleton scope in Spring btw), so I’m not gonna rant about that problem here, but I do like to show you an alternative which makes it easier to unit test Singletons.

Neal Ford published a talk on InfoQ called: “10 Ways to Improve Your Code”. Item 4: “Good citizenship” is about static methods and especially the Singleton design pattern. Static methods have their usage, for example simple stateless utilities like: int Math.sqrt(double) or Apache Commons. But when you’re going to add state into the mix, things get messy.

Singleton usage

Let’s step back. Why are we using Singletons in the first place?

  1. First, as the name says, because you want to guarantee that there is only one instance. Typical examples in the Java libraries are java.awt.Toolkit and java.io.Console.
    For those of you interested, Console doesn’t have a static getConsole method, but can be accessed through System.console() and – in the Sun implementation – SharedSecrets.getJavaIOAccess().console(). In these cases, a Singleton is needed because there may NEVER be two instances of these classes. Otherwise strange things will happen, because you never know which instance contains the state you need.
  2. Second, and depending on the circumstances this is a good or a bad thing, Singletons are the Object Oriented equivalent of globals. Almost everybody agrees that (non-immutable) globals are very bad, but for some reason, many developers still use Singletons. But, for certain objects, it’s useful to be able to access them from everywhere, without needing parameters all the time.

And of course, there are plenty of myths regarding Singletons, for example because singletons can be lazily instantiated on first use. Well, guess what? Classes are lazily initialized by the ClassLoader, so unless you’re messing with cyclic initialization dependencies you don’t need any custom lazy loading mechanism.
By the way, lazy initialization is a bad thing that should be avoided if possible. See Joshua Bloch’s Effective Java for more details.

An example Singleton

Below is a snippet with an example Singleton. You probably see Singletons like this all day.

public final class BadSingleton {
    private final String prop1;
    private final String prop2;
 
    private BadSingleton() {
        this.prop1 = System.getProperty("prop1");
        this.prop2 = System.getProperty("prop2");
    }
 
    private static final BadSingleton INSTANCE = new BadSingleton();
 
    public static BadSingleton getInstance() {
        return INSTANCE;
    }
 
    // Other methods
 
}

It’s just a simple Singleton. It uses field initialization for thread safety and simplicity, but is still lazily initialized (by the ClassLoader). Further, initialization is done in the constructor where the environment is queried for some settings.

As you might know, this code is not testable, not even with reflection/setAccessible(true) hacking, since the field is declared static + final. See the following snippet for a misguided attempt to create a unit test:

public class BadSingletonTest {
    @Test
    public void testGetInstance_Fail() throws Exception {
        Constructor<BadSingleton> constructor = BadSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        BadSingleton obj1 = constructor.newInstance();
        Field field = BadSingleton.class.getDeclaredField("INSTANCE");
        field.setAccessible(true);
        field.set(null, obj1); // Exception because INSTANCE is static + final
        BadSingleton obj1 = BadSingleton.getInstance();
        BadSingleton obj2 = BadSingleton.getInstance();
        Assert.assertSame(obj1, obj2);
    }
}

An alternative for this issue would be lazy initialization, but this has the disadvantages of not being thread safe when not synchronized correctly. Java Concurrency in Practice by Brian Goetz is a must-read when you are interested in thread safety issues.

Alternative using a factory

This is the alternative Singleton class. It’s a modified version of Neal’s example.

public final class GoodSingleton {
    private final String prop1;
    private final String prop2;
 
    private GoodSingleton(String prop1, String prop2) {
        this.prop1 = prop1;
        this.prop2 = prop2;
    }
 
    // Other methods
}

The responsibility of instantiating the Singleton is programmed into the Factory, which is shown below:

public class GoodSingletonFactory {
    private static final GoodSingleton INSTANCE;
    static {
        try {
            String prop1 = System.getProperty("prop1");
            String prop2 = System.getProperty("prop2");
 
            Constructor<GoodSingleton> constructor = GoodSingleton.class.getDeclaredConstructor(String.class, String.class);
            constructor.setAccessible(true);
            INSTANCE = constructor.newInstance(prop1, prop2);
        } catch (Exception e) {
            throw new RuntimeException("error initializing " + GoodSingletonFactory.class.getName(), e);
        }
    }
 
    public static final GoodSingleton getInstance() {
        return INSTANCE;
    }
}

As shown in the snippet, the factory uses reflection to access the private constructor. Note that this trick only works if there is no SecurityManager or if the active SecurityManager approves this action.

It’s also a little bit less “Singleton” since it is possible to have more than one instance. The same reflection trick we used, now works a bit against us. If this is not an issue for you, it doesn’t matter. After all, developers who want to abuse your API always succeed if they really want to.

But we do get the advantage of better testability, as shown in the following two snippets:

public class GoodSingletonTest {
    @Test
    public void testConstructor_Ok() throws Exception {
        Constructor<GoodSingleton> constructor = GoodSingleton.class.getDeclaredConstructor(String.class, String.class);
        constructor.setAccessible(true);
        GoodSingleton obj1 = constructor.newInstance("1", "2");
        // Assert stuff
    }
}
public class GoodSingletonFactoryTest {
    @Test
    public void testGetInstance_Only_one_instance() throws Exception {
        System.setProperty("prop1", "test value");
        System.setProperty("prop2", "test value");
        GoodSingleton instance1 = GoodSingletonFactory.getInstance();
        Assert.assertNotNull(instance1);
        GoodSingleton instance2 = GoodSingletonFactory.getInstance();
        Assert.assertNotNull(instance2);
        Assert.assertSame(instance1, instance2);
    }
}

As you can see, testing now becomes trivial.

Notes

Choosing the factory approach has some effects that may be important to you. Here’s a list of consequences and random thoughts.

  • When using the reflection approach, it is possible for developers to mess up the Singleton behavior using reflection. When they do this, you may end up with multiple instances, which may be undesirable. When using a Singleton and the instance field is declared as static + final, this is not possible. The importance of this point differs per situation.
  • You can choose to make the constructor package private (default) accessible. This way, you don’t need reflection to access it from the same package. If you put the Factory and the unit tests in the same package, this will make the code much more readable, especially for less experienced developers. It’s also less error prone, because the compiler isn’t capable of doing thorough checks on reflective code. On the other hand, if a developer put’s a custom class in the same package, it’s possible to invoke the constructor and thus create multiple instances.
  • There is a myth that reflection is slow. It’s true that reflection is not as fast as JIT optimized (plain Java) bytecode. But since about 2000, there have been massive improvements in reflective invocation performance. And of course, if you look at the snippets, reflection is only used the first time.
  • Using a static initializer block in the Factory and an immutable (all fields final, no lazy initialization) Singleton class makes the entire structure less sensitive to Concurrency hazards. Again, see Brian Goetz’ book for details.

If you ask me, the advantages outweigh the disadvantages by miles, especially when you consider that when developers misbehave (like in the first two bullets) in such a way that they break the Singleton principle, they should (and on my projects will) be ass whooped. :)




Cyclische dependencies @ runtime

Door: Frank Verbruggen, 16 April 2009

In een gelaagd project zijn er meestal aparte lagen gedefinieerd voor services, domein logica en data access. Stel nu dat er een noodzaak is om vanuit een lager gelegen laag gebruik te maken van functionaliteit die thuis hoort in een hoger gelegen laag. Bijvoorbeeld omdat er in de domein objecten een noodzaak is om gebruik te maken van een service. Hoe zorg je er dan voor dat je nette object georienteerde code houdt, zonder dat je compile time dependencies krijgt tussen je projecten / lagen? Het hier gepresenteerde design pattern is een elegante object georienteerde oplossing voor het bovenstaande probleem.

Gegeven een DomeinObject als object uit de lagere laag, en een ServiceImplementation als object uit de hogere laag. De serviceImplementation heeft een een interface methode ’someMethod()’ uit de ServiceInterface die benodigd is om de methode ‘businessMethod()’ uit het DomeinObject te implementeren (zie figuur 1, originele klasse diagram).

N.B. Het design pattern is uitgewerkt met domein objecten en service implementaties, maar dat is slechts een invulling.

—————————————————————-

—————————————————————-
Figuur 1, originele klasse diagram
—————————————————————-

Los dit als volgt op. Trek de ServiceInterface uit de hogere laag, en zet deze in de lagere laag. Maak een RequiredInterfaceFactory die een instantie van de ServiceInterface kan bevatten. Initialiseer bij het opstarten van je applicatie vanuit de bovenste laag de RequiredInterfaceFactory met de ServiceImplementation (dit kan bijvoorbeeld goed met de Spring configuratie uit Listing 1, Spring configuratie). En implementeer de businessMethod door de aanroep naar de ServiceImplementation als volgt te abstraheren:

RequiredInterfaceFactory.getInstance().getServiceInterface().someMethod();

Het klasse diagram ziet er dan uit zoals weergegeven in Figuur 2, klasse diagram.

—————————————————————-

 

—————————————————————-
Listing 1, Spring configuratie
—————————————————————-

—————————————————————-

—————————————————————-
Figuur 2, klasse diagram
—————————————————————-

Voordelen en nadelen

Het gebruik van dit design pattern biedt de volgende voordelen:

  • Compile time dependencies zijn niet nodig
  • At runtime kunnen interface methoden van bovenliggende klassen toch gebruikt worden
  • Ieder object kan gebruik maken van de interface, ongeacht in welk package het zit, dus het ophalen van de interface kan in de code altijd op dezelfde manier gebeuren

Het gebruik van dit design pattern biedt de volgende nadelen:

  • Je moet de RequiredInterfaceFactory injecteren met de ServiceImplementation voordat de rest van de applicatie aangesproken wordt



Boekbespreking: Code Complete, 2nd Edition

Door: Hedzer Westra, 21 January 2009

 

Auteur: Steve McConnell, publicatiejaar: 2004, pagina’s: 906, ISBN-13: 978-0735619678, website: http://www.cc2e.com/, Amazon rating: 5 sterren.

 

Code Complete was me jaren geleden aangeraden als hét boek (‘de bijbel’) om te leren nette code te produceren. Enige tijd geleden (!) is de tweede editie uitgekomen, wat me een goed moment leek om deze eens te bestuderen. Het viel me gelijk op dat deze dikke pil van Microsoft Press afkomstig is; auteur Steve McConnell komt uit de (Microsoft) wereld van C-programmeurs, maar beheerst vele andere talen.

 

Het boek geeft de lezer diverse praktische, aan de praktijk gestaafde, gedetailleerde tips en voorbeelden om op een juiste manier programmacode te maken. De meeste voorbeelden gaan uit van C++, Visual Basic, Java en C# (in die volgorde). Daar zit ’m wat mij betreft ook meteen een manco: ikzelf ben niet (meer) geïnteresseerd in allerlei tips om onder anderen netjes met pointers en globale variabelen om te gaan. Dat heeft Java netjes voor me afgeschermd… Wel krijg je een interessant kijkje in de keuken van C/C++, en wat extra respect voor programmeurs daarin, die zelfdiscipline nodig hebben, waar wij de taal Java hebben om dezelfde mate van noodzakelijke restrictie te krijgen.

 

De structuur, lay-out en leesbaarheid van het boek is zeer goed: er is op een prettig leesbare manier geschreven, hier en daar humoristisch en nergens flauw. Een uitgebreide inhoudsopgave, lijst van tabellen & figuren, referenties, leeslijst van tijdschriften en boeken, en index ontbreken niet. Elk hoofdstuk begint met een duidelijke inleiding van de inhoud, en eindigt met een checklist, samenvatting van de ‘key points’ en vele referenties naar extra leesvoer. Dat laatste is erg opvallend: Steve heeft ontzettend veel materiaal gebruikt bij het samenstellen van dit boek. In het voorwoord claimt hij dan ook dat zijn boek een samenvatting is van heel veel (zo niet alle) succesvolle ontwikkelingen en ontdekkingen op het gebied van softwareconstructie. Overal in het boek staan in de kantlijn referenties naar de ‘cc2e’ website met meer informatie, betekenisvolle quotes van bekende IT-goeroes en kruisverwijzingen. Naast verwijzingen naar onderzoeksresultaten van academici, veelal met statistische gegevens, put de auteur ook uit zijn eigen rijke ervaringen bij o.a. Microsoft, Boeing en NASA. Veel stukken code worden begeleid door een “coding horror” icoontje, om aan te geven hoe verschrikkelijk het desbetreffende antivoorbeeld is.

 

Het boek omvat 6 delen, onderverdeeld in 35 hoofdstukken, elk met hun eigen onderwerp. Teveel om allemaal te noemen, maar enkele sappige details & citaten wil ik je niet onthouden:

  • Software Construction is the only activity that is guaranteed to be done: dit is een stevig argument voor Agile werken – waarin documentatie een ondergeschikte rol krijgt toebedeeld ten gunste van productie van programmacode.
  • Program into, not in, a language: hiermee wil Steve je aanzetten om de mogelijkheden van je taal wijselijk te gebruiken, en niet blind alle mogelijkheden – al dan niet op een verkeerde wijze – toe te passen.
  • De sectie over complexiteit en het beperkte menselijke begripsvermogen is erg vermakelijk.
  • “Don’t optimise your code, unless a profiler or production problem forces you to.”
  • Als alternatief voor nette UML ontwerpen: neem foto’s van tekeningen op whiteboard, of bewaar flip-overs.
  • Itereer je design.
  • Gebruik tijdens ontwikkeling asserts of excepties die je programma hard doen crashen, maar zorg dat in de productieversie dezelfde fout netjes (liefst zonder interactie van, en overlast voor, de gebruiker) wordt afgehandeld. Dit wordt ook wel offensive vs. defensive programming genoemd.
  • Schrijf alles altijd uit in pseudo-code. Persoonlijk doe ik dit zeer zelden – en ik heb uit dit boek ook geen extra stimulans daarvoor gehaald, onder anderen door het detailniveau dat wordt aangeraden. 

Veel van de onderwerpen die beschreven worden – requirements, design, optimalisatie, naming convention, (unit) testing, Design Patterns, encapsulatie, factorisatie, assert vs. excepties vs. unit tests, continuous integration, comments vs. refactoring – waren eerlijk gezegd een samenvatting van wat ik al wist. Nuttig om hier en daar iets nieuws te horen, of om even opgefrist te worden, maar echt iets fundamenteels geleerd: nee.

 

Wat pijnlijk duidelijk wordt, is dat het boek geschreven is vlak voordat Java5 uitkwam – hier en daar wordt Java verweten enkele mogelijkheden te ontberen, terwijl we die al jaren tot onze beschikking hebben.

 

Concluderend: het boek maakt wat mij betreft zijn belofte slechts gedeeltelijk waar. Voor C++/C#/VB programmeurs is het vast en zeker erg bruikbaar, maar voor Java is het net iets te oud, en ligt er te weinig focus op problemen in Java. Als je junior of medior bent, of voor een breed scala aan onderwerpen in software constructie met één boek klaar wilt zijn, is het een goed boek, maar helaas geen uitstekend boek…

 

Kleurindicatie: Amber (2 op schaal van 3)

 

Hierbij nodig ik al mijn collega’s uit om ook boekbesprekingen te publiceren op de J-Tech blog, en tevens informatie te plaatsen op de Wiki (https://wiki.ordina.nl/confluence/display/GENBIEB/Alle+Boeken), zodat iedereen een goede keus kan maken uit de vele boeken waarmee je je vaardigheden en kennis kunt uitbreiden. Ikzelf hoop binnenkort te komen met een bespreking van “Programming in Scala,” het eerste boek over deze nieuwe en spannende programmeertaal.

Hedzer Westra




Temporale data – deel 3: het bitemporale pattern

Door: gnoij, 22 December 2008

In dit derde deel van de serie over temporale data breidt ik het temporale pattern uit het tweede deel uit met de registratietijd voor opvoer en afvoer. Hierdoor ontstaat het bitemporale pattern.

BiTemporalObject

public abstract class BiTemporalObject implements Cloneable {
 
    private Date ingangsdatum;
 
    private Date einddatum;
 
    private Date opvoerTijdstip;
 
    private Date afvoerTijdstip;
 
    protected BiTemporalObject(Date ingangsdatum) {
        super();
 
        if (ingangsdatum == null) {
            throw new IllegalArgumentException("Ingangsdatum is null");
        }
 
        this.ingangsdatum = ingangsdatum;
 
        this.opvoerTijdstip = new Date();
    }
 
    public boolean isGeldigOp(Date registratieTijdstip, Date peilDatum) {
        return isGeregistreerdOp(registratieTijdstip) && isGeldigOp(peilDatum);
    }
 
    private boolean isGeregistreerdOp(Date registratieTijdstip) {
        return !isAfgevoerd(registratieTijdstip)
                && (registratieTijdstip.after(this.opvoerTijdstip) || registratieTijdstip.equals(this.opvoerTijdstip));
    }
 
    private boolean isAfgevoerd(Date registratieTijdstip) {
        return this.afvoerTijdstip != null && (registratieTijdstip.after(this.afvoerTijdstip) || registratieTijdstip.equals(this.afvoerTijdstip));
    }
 
    private boolean isGeldigOp(Date peilDatum) {
        boolean geldig = peilDatum.after(this.ingangsdatum) || peilDatum.equals(this.ingangsdatum);
        if (this.einddatum != null) {
            geldig = geldig && peilDatum.before(this.einddatum);
        }
        return geldig;
    }
 
    protected BiTemporalObject kopieer(Date wijzigingsdatum, Date registratieTijdstip) {
        // maak een kopie met gevulde einddatum
        BiTemporalObject versie = (BiTemporalObject) super.kopieer();
        versie.opvoerTijdstip = registratieTijdstip;
        versie.afvoerTijdstip = null;
        versie.einddatum = wijzigingsdatum;
 
        // voer de oude versie af
        this.afvoerTijdstip = registratieTijdstip;
 
        return versie;
    }
 
    public Date getIngangsdatum() {
        return this.ingangsdatum;
    }
 
    public Date getEinddatum() {
        return this.einddatum;
    }
 
    public Date getOpvoerTijdstip() {
        return this.opvoerTijdstip;
    }
 
    public Date getAfvoerTijdstip() {
        return this.afvoerTijdstip;
    }

In deze klasse vallen (ten opzichte van TemporalObject) een aantal dingen op. Allereerst dat deze klasse clonable is, verder dat een object pas geldig is als de registratietijd binnen de opvoer en afvoer registratie is en dat er een kopieer methode is, die een kopie (clone) van het object maakt met de huidige opvoertijd en einddatum. Het oorspronkelijke object wordt dan afgevoerd door het vullen van de afvoer registratietijd. Bij creatie van een BiTemporalObject wordt automatisch het opvoertijdstip gevuld met de systeemdatum.

BiTemporalProperty

public class BiTemporalProperty {
 
    protected List alleHistorischeVersies = new ArrayList();
 
    public BiTemporalProperty() {
        super();
    }
 
    public BiTemporalObject getActueleVersie() {
        return getVersieOp(new Date(), new Date());
    }
 
    public BiTemporalObject getVersieOp(final Date registratieTijdstip, final Date peilDatum) {
        return (BiTemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                return ((BiTemporalObject) object).isGeldigOp(registratieTijdstip, peilDatum);
            }
        });
    }
 
    public void setActueleVersie(BiTemporalObject actueleVersie) {
        if (actueleVersie == null) {
            throw new IllegalArgumentException("actueleVersie is null");
        }
 
        beeindigVorigeVersie(actueleVersie);
        this.alleHistorischeVersies.add(actueleVersie);
    }
 
    private void beeindigVorigeVersie(final BiTemporalObject actueleVersie) {
        BiTemporalObject afTeVoerenVersie = (BiTemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                BiTemporalObject versie = (BiTemporalObject) object;
                return versie.isGeldigOp(actueleVersie.getOpvoerTijdstip(), actueleVersie.getIngangsdatum());
            }
        });
 
        if (afTeVoerenVersie != null) {
            BiTemporalObject kopieVersie = afTeVoerenVersie.kopieer(actueleVersie.getIngangsdatum(), actueleVersie.getOpvoerTijdstip());
            this.alleHistorischeVersies.add(kopieVersie);
        }
    }
 
    public void beeindig(Date einddatum) {
        BiTemporalObject teBeeindigenVersie = getActueleVersie();
        if (teBeeindigenVersie != null) {
            BiTemporalObject kopieVersie = teBeeindigenVersie.kopieer(einddatum, new Date());
            this.alleHistorischeVersies.add(kopieVersie);
        }
    }
}

Het verschil van de BiTemporalProperty met de TemporalProperty is dat er nu twee tijdstippen worden meegegeven om de juiste versie te bepalen. Naast de peildatum is dat ook het registratietijdstip. Als er een actuele versie wordt toegevoegd of beëindigd, wordt de op dat moment geldende versie gekopieerd en afgevoerd. De gekopieerde versie wordt beëindigd. Hierdoor zal er altijd bij beëindiging of wijziging een extra record ontstaan. Dit wordt verklaard in de concepten voor bitemporale data in deel 1.

Een voorbeeld

We gebruiken hier hetzelfde voorbeeld als uit deel 2.

Werknemer

public class Werknemer {
 
    private BiTemporalProperty afdelingRelatie = new BiTemporalProperty();
 
    private String naam;
 
    public Werknemer(String naam) {
        super();
 
        this.naam = naam;
    }
 
    public String getNaam() {
        return this.naam;
    }
 
    public void setAfdeling(Afdeling afdeling, Date ingangsdatum) {
        this.afdelingRelatie.setActueleVersie(new AfdelingRelatie(afdeling, ingangsdatum));
    }
 
    public Afdeling getAfdeling() {
        AfdelingRelatie relatie = getAfdelingRelatie();
        if (relatie != null) {
            return relatie.getAfdeling();
        }
        return null;
    }
 
    public Afdeling getAfdeling(Date registratieTijdstip, Date peildatum) {
        AfdelingRelatie relatie = (AfdelingRelatie) this.afdelingRelatie.getVersieOp(registratieTijdstip, peildatum);
        if (relatie != null) {
            return relatie.getAfdeling();
        }
        return null;
    }
 
    public AfdelingRelatie getAfdelingRelatie() {
        return (AfdelingRelatie) this.afdelingRelatie.getActueleVersie();
    }
 
    public AfdelingRelatie getAfdelingRelatie(Date registratieTijdstip, Date peildatum) {
        return (AfdelingRelatie) this.afdelingRelatie.getVersieOp(registratieTijdstip, peildatum);
    }
 
    public void uitDienst(Date datumUitdienst) {
        this.afdelingRelatie.beeindig(datumUitdienst);
    }
}

Ten opzichte van de klasse Werknemer uit deel 2 hebben de methoden getAfdeling en getAfdelingRelatie een extra parameter registratieTijdstip.

AfdelingRelatie

public class AfdelingRelatie extends BiTemporalObject {
 
    private Afdeling afdeling;
 
    protected AfdelingRelatie(Afdeling afdeling, Date ingangsdatum) {
        super(ingangsdatum);
 
        this.afdeling = afdeling;
    }
 
    public Afdeling getAfdeling() {
        return this.afdeling;
    }
}

Deze klasse AfdelingRelatie is afgeleid van de klasse BiTemporalObject. Verder is deze analoog aan de klasse uit deel 2.

De klasse Afdeling is analoog aan de klasse Afdeling uit deel 2.

De test ziet er als volgt uit:

    public void testWerknemer() {
        Werknemer werknemer = new Werknemer("Kees");
 
        // simuleer de opvoer tijd 1-1-2003
        BiTemporalObject.TEST_OPVOER_TIJDSTIP = DateUtils.maakDate(2003, 1, 1);
 
        Afdeling inkoop = new Afdeling("Inkoop");
        werknemer.setAfdeling(inkoop, DateUtils.maakDate(2003, 1, 1));
 
        // simuleer de opvoer tijd 1-2-2006
        BiTemporalObject.TEST_OPVOER_TIJDSTIP = DateUtils.maakDate(2006, 2, 1);
 
        Afdeling verkoop = new Afdeling("Verkoop");
        werknemer.setAfdeling(verkoop, DateUtils.maakDate(2006, 1, 1));
 
        assertEquals("Kees", werknemer.getNaam());
        assertEquals("Verkoop", werknemer.getAfdeling().getNaam());
        assertEquals("Verkoop", werknemer.getAfdeling(DateUtils.vandaag(), DateUtils.maakDate(2006, 1, 15)).getNaam());
        assertEquals("Inkoop", werknemer.getAfdeling(DateUtils.maakDate(2006, 1, 15), DateUtils.maakDate(2006, 1, 15)).getNaam());
 
        // simuleer de opvoer tijd 1-2-2008
        BiTemporalObject.TEST_OPVOER_TIJDSTIP = DateUtils.maakDate(2008, 2, 1);
 
        werknemer.uitDienst(DateUtils.maakDate(2008, 1, 1));
        assertNull(werknemer.getAfdeling());
    }

In deze test maken we een werknemer Kees aan, die begint te werken op de afdeling Inkoop op 1 januari 2003. Dit wordt op dezelfde datum geregistreerd. Vanaf 1 januari 2006 werkt Kees op de afdeling Verkoop, wat pas op 1 februari wordt geregistreerd en vanaf 1 januari 2008 is Kees uit dienst. Dit wordt een maand later pas geregistreerd.

Omdat de registratietijd automatisch wordt bepaald heb ik (uitsluitend voor testdoeleinden) de klasse BiTemporalObject uitgebreid met een public static member TEST_OPVOER_TIJDSTIP, om de registratietijd uit het voorbeeld te kunnen simuleren. Hierdoor verandert de constructor van BiTemporalObject in

    protected BiTemporalObject(Date ingangsdatum) {
        super();
 
        if (ingangsdatum == null) {
            throw new IllegalArgumentException("Ingangsdatum is null");
        }
 
        this.ingangsdatum = ingangsdatum;
 
        // voor testdoeleinden
        if (TEST_OPVOER_TIJDSTIP == null) {
            this.opvoerTijdstip = new Date();
        } else {
            this.opvoerTijdstip = TEST_OPVOER_TIJDSTIP;
        }
    }

Hibernate mappings

De hibernate mapping van de klasse Werknemer en Afdeling zijn analoog aan die van de klasse Werknemer uit deel 2.
De mapping van de klasse AfdelingRelatie is uitgebreid met de opvoer- en afvoer registratietijd.

AfdelingRelatie

<hibernate-mapping package="nl.ordina.bitemporal.example"
	default-access="field">
	<class name="AfdelingRelatie" table="AfdelingRelatie">
		<id name="id" column="id">
			<generator class="identity"></generator>
		</id>
		<version name="versie" />
 
		<!-- temporale informatie -->
		<property name="ingangsdatum" type="date" />
		<property name="einddatum" type="date" />
		<property name="opvoerTijdstip" type="date" />
		<property name="afvoerTijdstip" type="date" />
 
		<!-- de verwijzing naar de afdeling -->
		<many-to-one name="afdeling" lazy="false" class="Afdeling"
			column="afdelingId" cascade="none" />
	</class>
</hibernate-mapping>

Het volgende deel zal het bitemporale pattern uitbreiden met historische collections.




Temporale data – deel 2: het temporale pattern

Door: gnoij, 7 December 2008

Zoals al in het eerste deel te zien was is temporale data op te delen in temporale (alleen geldigheid) of bitemporale data (geldigheid met registratietijd). In dit tweede deel beschrijf ik het pattern voor temporale data.

De kern van dit pattern zijn de Java klassen TemporalObject en TemporalProperty. TemporalObject bevat de logica voor de geldigheid en TemporalProperty bevat de code om een de juiste versie van een property te kunnen bepalen.

TemporalObject

public abstract class TemporalObject {
 
    private Date ingangsdatum;
 
    private Date einddatum;
 
    protected TemporalObject(Date ingangsdatum) {
        super();
 
        if (ingangsdatum == null) {
            throw new IllegalArgumentException("Ingangsdatum is verplicht");
        }
 
        this.ingangsdatum = ingangsdatum;
    }
 
    public boolean isGeldigOp(Date peildatum) {
        boolean geldig = peildatum.after(this.ingangsdatum) || peildatum.equals(this.ingangsdatum);
        if (this.einddatum != null) {
            geldig = geldig && peildatum.before(this.einddatum);
        }
        return geldig;
    }
 
    public Date getIngangsdatum() {
        return this.ingangsdatum;
    }
 
    public Date getEinddatum() {
        return this.einddatum;
    }
 
    public void beeindig(Date einddatum) {
        this.einddatum = einddatum;
    }
}

Deze klasse is de superklasse van een klasse met geldigheid. Deze klasse bevat de ingangsdatum en einddatum van een versie. Deze klasse bevat alleen een constructor met ingangsdatum, omdat deze verplicht is (anders kunnen we geen historie bijhouden). Verder kent deze klasse de methode om de te vragen of een object geldig is op een bepaalde datum en een methode om een instantie te kunnen beëindigen. Deze klasse is gebaseerd op het Effectivity Pattern van Martin Fowler.

TemporalProperty

public class TemporalProperty {
 
    protected List alleHistorischeVersies = new ArrayList();
 
    public TemporalProperty() {
        super();
    }
 
    public TemporalObject getActueleVersie() {
        return getVersieOp(new Date());
    }
 
    public TemporalObject getVersieOp(final Date peildatum) {
        return (TemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                return ((TemporalObject) object).isGeldigOp(peildatum);
            }
        });
    }
 
    public void setActueleVersie(TemporalObject actueleVersie) {
        if (actueleVersie == null) {
            throw new IllegalArgumentException("actueleVersie is null");
        }
 
        beeindigVorigeVersie(actueleVersie);
        this.alleHistorischeVersies.add(actueleVersie);
    }
 
    private void beeindigVorigeVersie(final TemporalObject actueleVersie) {
        TemporalObject teBeeindigenVersie = (TemporalObject) CollectionUtils.find(this.alleHistorischeVersies, new Predicate() {
            public boolean evaluate(Object object) {
                TemporalObject versie = (TemporalObject) object;
                return versie.isGeldigOp(actueleVersie.getIngangsdatum());
            }
        });
 
        if (teBeeindigenVersie != null) {
            teBeeindigenVersie.beeindig(actueleVersie.getIngangsdatum());
        }
    }
 
    public void beeindig(Date einddatum) {
        TemporalObject teBeeindigenVersie = getActueleVersie();
        if (teBeeindigenVersie != null) {
            teBeeindigenVersie.beeindig(einddatum);
        }
    }
}

Deze klasse wordt gebruikt om alle historische versies van een property bij te houden. Deze klasse kent methoden om de actuele versie op te vragen, om de versie die op een bepaalde datum geldig is op te vragen en om een nieuwe actuele versie toe te voegen of te beëindigen. Als er een nieuwe actuele versie wordt toegevoegd, wordt de op dat moment geldende versie beëindigd. Deze klasse is gebaseerd op de TemporalProperty Pattern van Martin Fowler.

Let op: Fowler noemt deze klasse TemporalCollection. Deze naam gebruik ik voor de lijst properties met historie, die ik in deel 4 zal behandelen.

Voorbeeld

Om de werking van bovenstaande klassen uit te leggen gebruiken we het voorbeeld uit deel 1.

Kees werkte vanaf 1-1-2003 op de afdeling Inkoop. Vanaf 1-1-2006 werkt hij op de afdeling Verkoop. Deze overgang werd op 1-2-2006 geregistreerd in het systeem.

We hebben hier te maken met de klassen Werknemer en Afdeling. Omdat de afdeling bij meerdere werknemers voor kan komen gebruiken we een AfdelingRelatie klasse om de geldigheid van een afdeling bij een werknemer te registreren.

Het klasse model ziet er als volgt uit:

Werknemer

public class Werknemer {
 
    private TemporalProperty afdelingRelatie = new TemporalProperty();
 
    private String naam;
 
    public Werknemer(String naam) {
        super();
 
        this.naam = naam;
    }
 
    public String getNaam() {
        return this.naam;
    }
 
    public void setAfdeling(Afdeling afdeling, Date ingangsdatum) {
        this.afdelingRelatie.setActueleVersie(new AfdelingRelatie(afdeling, ingangsdatum));
    }
 
    public Afdeling getAfdeling() {
        AfdelingRelatie relatie = getAfdelingRelatie();
        if (relatie != null) {
            return relatie.getAfdeling();
        }
        return null;
    }
 
    public Afdeling getAfdeling(Date peildatum) {
        AfdelingRelatie relatie = (AfdelingRelatie) this.afdelingRelatie.getVersieOp(peildatum);
        if (relatie != null) {
            return relatie.getAfdeling();
        }
        return null;
    }
 
    public AfdelingRelatie getAfdelingRelatie() {
        return (AfdelingRelatie) this.afdelingRelatie.getActueleVersie();
    }
 
    public AfdelingRelatie getAfdelingRelatie(Date peildatum) {
        return (AfdelingRelatie) this.afdelingRelatie.getVersieOp(peildatum);
    }
 
    public void uitDienst(Date datumUitdienst) {
        this.afdelingRelatie.beeindig(datumUitdienst);
    }
}

De klasse Werknemer bevat een temporal property afdelingRelatie die alle historische versies van de afdelingen van de werknemer bevat. Verder bevat deze klasse de public methoden om de huidige afdeling op te vragen, de afdeling waar de werknemer werkte op een bepaalde datum op te vragen en om een nieuwe afdeling te koppelen aan de werknemer. Het opvragen en koppelen van de afdeling verloopt via de relatieklasse AfdelingRelatie, die de historie van een enkele versie bijhoudt. Als een werknemer uit dienst gaat, wordt de actuele versie van de relatie beëindigd.

AfdelingRelatie

public class AfdelingRelatie extends TemporalObject {
 
    private Afdeling afdeling;
 
    protected AfdelingRelatie(Afdeling afdeling, Date ingangsdatum) {
        super(ingangsdatum);
 
        this.afdeling = afdeling;
    }
 
    public Afdeling getAfdeling() {
        return this.afdeling;
    }

De klasse AfdelingRelatie is afgeleid van de klasse TemporalObject omdat deze klasse de geldigheid bevat van de relatie tussen de werknemer en de afdeling. Omdat een temporal object immutable is, kan er alleen maar een nieuwe versie via de constructor worden aangemaakt met een ingangsdatum die de datum voorstelt waarop de werknemer op de nieuwe afdeling komt te werken.

Afdeling

public class Afdeling {
 
    private String naam;
 
    public Afdeling(String naam) {
        super();
        this.naam = naam;
    }
 
    public String getNaam() {
        return this.naam;
    }
}

De klasse Afdeling is een eenvoudige klasse die voor ons voorbeeld alleen de naam van de afdeling bevat.

Het gebruik van de klassen en methoden volgt uit de volgende unit test.

public void testWerknemer() {
		Werknemer werknemer = new Werknemer("Kees");
 
		Afdeling afdeling = new Afdeling("Inkoop");
		werknemer.setAfdeling(afdeling, DateUtils.maakDate(2003, 1, 1));
 
		afdeling = new Afdeling("Verkoop");
		werknemer.setAfdeling(afdeling, DateUtils.maakDate(2006, 1, 1));
 
		assertEquals("Kees", werknemer.getNaam());
		assertEquals("Verkoop", werknemer.getAfdeling().getNaam());
		assertEquals("Verkoop", werknemer.getAfdeling(DateUtils.maakDate(2006, 1, 15)).getNaam());
        	assertEquals("Inkoop", werknemer.getAfdeling(DateUtils.maakDate(2004, 1, 1)).getNaam());
 
		werknemer.uitDienst(DateUtils.maakDate(2008,1,1));
        	assertNull(werknemer.getAfdeling());
 
}

In deze test maken we een werknemer Kees aan, die begint te werken op de afdeling Inkoop op 1 januari 2003. Vanaf 1 januari 2006 werkt Kees op de afdeling Verkoop en vanaf 1 januari 2008 is Kees uit dienst.

Hibernate mappings

Tenslotte wil ik nog de Hibernate mappings voor de klassen Werknemer en AfdelingRelatie tonen om te laten zien hoe dit in zijn werk gaat.

Werknemer.hbm.xml

<hibernate-mapping package="nl.ordina.temporal.example" default-access="field">
	<class name="Werknemer" table="Werknemer">
 
		... 
		<property name="naam" column="naam" />
 
		<!-- de verwijzing naar de afdelingrelatie -->
		<component name="afdelingRelatie" 
			class="nl.ordina.temporal.singletemporal.TemporalProperty"
			lazy="false">
			<bag name="alleHistorischeVersies"
				cascade="all, delete-orphan" lazy="false">
				<key column="werknemerId" />
				<one-to-many class="AfdelingRelatie" />
			</bag>
		</component>
 
	</class>
</hibernate-mapping>

De temporal property afdelingRelatie wordt als een component opgenomen in de mapping van Werknemer. Hierbinnen wordt de lijst alleHistorischeRelaties gemapt als een bag.

AfdelingRelatie.hbm.xml

<hibernate-mapping package="nl.ordina.temporal.example" 	default-access="field">
	<class name="AfdelingRelatie" table="AfdelingRelatie">
 		...
 
		<!-- temporale informatie -->
		<property name="ingangsdatum" type="date" />
		<property name="einddatum" type="date" />
 
		<!-- de verwijzing naar de afdeling -->
		<many-to-one name="afdeling" lazy="false" class="Afdeling" column="afdelingId" cascade="none" />
	</class>
</hibernate-mapping>

De mapping van de AfdelingRelatie bevat de properties ingangsdatum en einddatum en een many-to-one relatie met de klasse Afdeling, die hier verder niet getoond wordt.

In het volgende deel zal het bitemporale pattern getoond worden. Hierbij zal het voorbeeld worden uitgebreid met registratiegegevens.




Temporale data – deel 1: de concepten

Door: gnoij, 19 November 2008

In een serie van 4 artikelen wil ik een framework voor gebruik van temporale data presenteren. In dit eerste deel wordt uitgelegd wat temporale data is en wat de consequenties zijn. In de volgende delen wordt het framework in een aantal stappen gepresenteerd.

Een korte introductie in temporale data

Temporale data is data waarbij tijdsaspecten een rol spelen.

Ter illustratie even een voorbeeld van een simpel HRM systeem:

Kees werkte vanaf 1-1-2003 op de afdeling Inkoop. Vanaf 1-1-2006 werkt hij op de afdeling Verkoop. Deze overgang werd op 1-2-2006 geregistreerd in het systeem.

Schematisch ziet dat er als volgt uit:

HRM voorbeeld

Uit bovenstaand voorbeeld blijkt dat de tijdsaspecten die een rol kunnen spelen zijn: een ingangsdatum, een einddatum en een registratiedatum. Welke tijdaspecten voor de te ontwikkelen applicatie een rol spelen hangt af van de vragen die de applicatie moet kunnen beantwoorden.

De mogelijke vragen met betrekking tot de tijdsaspecten in bovenstaand voorbeeld zijn:

  1. Waar werkt Kees nu?
  2. Sinds wanneer werkt Kees op de afdeling Verkoop?
  3. Gedurende welke periode werkte Kees op de afdeling Inkoop?
  4. Op welke afdeling werkte Kees op 15-1-2006 als we keken in het systeem op 15-1-2006?

Om elke vraag te kunnen beantwoorden heb je andere tijdsaspecten nodig.

Vraag 1 kun je beantwoorden zonder extra tijdsgegevens op te slaan.Vraag 2 kun je beantwoorden door een ingangsdatum te gebruiken.Voor vraag 3 heb je een ingangsdatum en een einddatum nodig en voor vraag 4 heb je naast de ingangs- en einddatum ook registratiedata nodig.

Om de vragen 2, 3 en 4 ook voor het verleden te kunnen beantwoorden moet er ook historie van de data worden bijgehouden. Dat betekent dat elke wijziging leidt tot een nieuw record in de database.

De historievormen met geldigheid (een ingangsdatum en een einddatum) noemen we temporale data en met geldigheid en registratiedata bitemporale data.

temporale data

Bij temporale data hebben we te maken met de tijdsaspecten ingangsdatum en einddatum. Willen we ook historie gaan bijhouden, moeten de wijzigingen op een temporal object leiden tot een nieuw record in de database. Dat betekent dat een object in principe immutable is. Wijzigingen op een object leiden altijd tot een nieuw object.

In principe onderscheiden we drie situaties: inschrijving, wijziging en beëindiging van de data.

Bij inschrijving wordt er een record aan een tabel toegevoegd in de database toegevoegd. Voor ons voorbeeld ziet dat er als volgt uit:

temporal insert

De FK kolom wijst naar de tabel Werknemer met Primary Key 1.

Voor een wijziging wordt er een nieuw record in de tabel geschreven, en wordt het vorige record afgesloten met een einddatum.

Tenslotte voor een beëindiging wordt het actuele record afgesloten met een einddatum. Als in het voorbeeld Kees uit dienst gaat op 1-1-2008 krijgen we de volgende records te zien.

We kunnen nu de vragen 1 tot en met 3 beantwoorden, maar niet vraag 4.

Antwoorden:

  1. Nergens, want Kees is per 1-1-2008 uit dienst.
  2. Sinds 1-1-2006 werkte Kees op de afdeling Verkoop
  3. Tussen 1-1-2003 en 1-1-2006 werkte Kees op de afdeling Inkoop
  4. Op 15-1-2006 wisten wij dat Kees op de afdeling Verkoop werkte. (Terwijl het systeem dit pas op 1-2-2006 heeft geregistreerd, dus is het antwoord fout. Dit moet Inkoop zijn)

bitemporale data

Zoals uit bovenstaande records blijkt kunnen we niet achterhalen wanneer iets is geregistreerd. Of wanneer de organisatie wist wanneer een wijziging is doorgevoerd. In sommige (vooral juridische) situaties is dit wel nodig. Hiervoor moeten we de geldigheid uitbreiden met registratiedata voor opvoer en afvoer. Dit noemen we bitemporale data

We kunnen dezelfde drie situaties als bij temporale data onderscheiden: inschrijving, wijziging en beëindiging.

Bij inschrijving wordt er een record aan de database toegevoegd. Dit is te zien in onderstaande tabel.

Hierbij zijn de kolommen Opvoer en Afvoer toegevoegd aan de tabel.

Bij wijziging wordt het eerste record afgesloten, wordt er een kopie gemaakt met einddatum en wordt de wijziging toegevoegd.

Per wijziging wordt dus (naast de wijziging) een extra record in de database toegevoegd met de oorspronkelijke informatie plus een afvoerdatum.

Bij beëindiging wordt een kopie gemaakt van het actuele record, de actuele versie krijgt een afvoerdatum en de kopie krijgt een einddatum.

Ook voor beëindiging wordt een extra record toegevoegd met de oorspronkelijke informatie plus een afvoerdatum.

Met het gebruik van zowel geldigheid als registratiedata is ook vraag 4 juiste te beantwoorden. Door te zoeken naar het record dat opgevoerd is voor 15-1-2006 en niet voor die datum is afgevoerd zien we dat het juiste antwoord Inkoop is.

Mogelijke oplossingen

Mogelijke oplossingsrichtingen voor de bouw van applicaties met (bi)temporale data zijn:

Het framework dat ik in de volgende artikelen wil presenteren is gebaseerd op de maatwerkoplossing, waarbij gebruik wordt gemaakt van de Temporal patterns van Fowler.

Dit framework bestaat uit patterns voor temporale data en bitemporale data. In het volgende artikel zal ik patterns voor temporale data presenteren. Het derde deel zal patterns voor bitemporale data behandelen en in het (voorlopig) laatste deel zullen we deze patterns uitbreiden voor collections.