Object transport

Inmiddels ben ik in heel wat applicaties een vorm van object transport tegengekomen. Bepaalde objecten uit een business-logica laag of component dienen getransporteerd te worden vanaf of richting een user interface, email component of database. Wat heel vaak gedaan wordt is transport via (web)services richting een user interface waarbij de services niet de ‘echte’ business objecten vervoeren maar DTO’s (Data Transfer Objects). Aangezien logica of gedrag van deze objecten niet meegestuurd wordt over deze services zie je dat in de targetlaag vaak bepaalde logica die al bestond in de bronlaag opnieuw declareert.

Kort samengevat in een fout plaatje ziet het er als volgt uit:

Wat bovenstaand plaatje illustreert is dat bij deze redelijk recht-toe-recht-aan architectuur er 4 vertalingsmomenten zijn om je object uit een database richting een presentatie te krijgen in een user interface. Ik zou in deze post dan ook willen proberen een oplossing te schetsen waardoor er totaal geen vertalingen meer nodig zijn voor deze stappen, althans niet in (handgeschreven) code. De oplossingsrichting die ik hiervoor kies is een vorm van custom XML Serialisatie. Hiertoe zal ik eerst kort even de verschillende vormen van vertaling langslopen om hierbij wat zaken in context van XML serialisatie aan te tekenen.

Vertalingen
Objects -> XML
Nogal wiedes zou je denken, zodra ik objecten over services ga versturen is dat per definitie geserialiseerd. Volledig correct, echter out-of-the-box XML serialisatie in bijvoorbeeld .NET (in Java heb ik het niet kunnen vinden, behalve via 3rd party libraries) biedt niet de oplossing die je zoekt. Als ik een netwerk van samenhangende business objecten ga serialiseren zonder vertalingsslag richting DTO dan loop ik al vrij snel tegen vertalingsproblemen van object georienteerde structuren naar XML structuren aan: circulaire referenties bijvoorbeeld. Neem een klant met orders, waarvan het order object ook weet bij welke klant die hoort. De XML wordt dan als volgt:

<klant>
  <orders>
    <order>
      <klant>
        <orders>
            ………..

Het ligt voor de hand om dit via een ID/IDREF structuur te doen (iets dat bijvoorbeeld de serializers binnen WCF standaard ondersteunen), maar hiermee lossen we het probleem nog niet op dat we een veel te grote objectboom over de lijn gaan trekken aangezien de out-of-the-box serializers alle geassocieerde objecten meetrekken. Ik wil dus een mechanisme om objectbomen te kunnen opknippen en later on-demand alsnog op te kunnen halen (een vorm van lazy-loading dus).

XML -> Objects
De andere kant op dan. Leuk, zo’n XML bericht, maar hoe maak ik daar nou een gedragsrijk object van? Hier komt een deployment issue kijken en een stukje configuratie. De definitie van bussiness objecten moet in elke laag aanwezig zijn. Voor .NET systemen betekent dit dat een assembly met domain classes in elke laag aanwezig dient te zijn. Deze distributie is echter maar éénmalig noodzakelijk, bij deployment van het systeem (en uiteraard versie updates). Een meer extremere vorm zou mogelijk zijn door enkel een interface definitie te verspreiden en binair te gaan serialiseren, wellicht nog een keer onderwerp van discussie voor een andere post.

Objects -> Tables
Een al veel meer uitgekauwd onderwerp is mapping van objecten naar database stucturen. (N)Hibernate is hier een goed voorbeeld van, en Microsoft komt nu met het Entity Framework. Java heeft de o.a. JPA (Java Persistency Api). Ik maak hier dan ook maar niet teveel woorden aan vuil, echter ben ik wel van mening dat het in de meeste gevallen onzin is om mappings te definieren (of genereren). Persoonlijk ben ik de mening toebedeeld dat er enkel reden is tot mapping definitie als er een bestaande database is, anders kan je volstaan door je domain model direct om te zetten in een tabelstructuur (een van de vele pogingen hiertoe is te vinden op codeplex).

Voorstel tot oplossing
Ok, genoeg geklaagd, hoe lossen we zoiets nou mogelijk op? Ik geef hier een richting aan waar ik graag over wil discussieren, dus brand los in de comments op deze post als je het ergens niet mee eens bent…

We beginnen met een uitbreiding van onze business classes om uniek te kunnen identificeren. Nodig voor database opslag, maar ook voor transport zoals we later zullen zien. Ik definieer hiertoe de interface IUniqueIdentifyable die een get/set van een Guid Id vereist. Tevens wil ik dat objecten geinteresseerden kunnen notificeren van eventueel veranderde fields (ik doe zowiezo alles op fieldniveau qua data, aangezien properties business-specifieke logica kunnen bevatten en daar wil ik duidelijk onder gaan zitten). De andere kant op wil ik classes kunnen bevragen om bepaalde objecten op te zoeken (een repository achtig patroon). Om deze drie zaken te ondersteunen definieer ik de volgende drie generic static events per class:

  • ObjectChanged(T changed)
  • T ObjectRequested(Guid id);
  • List<T> ObjectsRequested(string expression, int start, int amount)

Waarom via events? Het antwoord is ontkoppeling. Als ik een class direct laat interacteren met bijvoorbeeld een database component dan creeer ik een afhankelijkheid. Door op deze manier te werken kan ik in elke laag afzonderlijk de events knopen aan subscribers die de daadwerkelijke acties uitvoeren. Je kunt je voorstellen dat een objectrequested event in de UI laag resulteert in een webservice call richting de business laag als in bovenstaand plaatje. In de businesslaag op zijn beurt wordt het event op dezelfde class doorgegeven aan een databasecomponent die het object uit de bijbehorende tabellen vist.

De keuze om deze events static te maken – dus eenmalig per class per laag te definieren – is vrij arbitrair. Je zou er ook voor kunnen kiezen losse repositories te definieren of bijvoorbeeld collecties te gebruiken die de events gooien, maar in mijn optiek is dit een vrij schone oplossing. Een punt van aandacht is echter wel de un-subscription van de static events, aangezien eventuele subscribers een referntie hebben naar een static event zullen deze wel levenslang in het geheugen blijven. Un-subscription van events zou bijvoorbeeld in een global.asax geregeld kunnen worden in een webcontext.

So far so good, maar we hebben nog wat exta eisen gesteld aan de XML serialisatie van de objecten. Om even conceptueel op een rijtje te krijgen wat we op welke manier zouden willen kunnen serialiseren beschouw ik de volgende situaties:

  • Primitive types – int, string, etc., maar ook bijvoorbeeld eigen gedefinieerde structs (value types).
  • Domain types – IUniqueIdentifyable classes
  • Collections – IUniqueIdentifyable collections

De eerste soort, primitive types, kan ik vaak wel typed serializeren. Aangezien ik hier ook value object onder schaar (niet-IUniqueIdentifyables welteverstaan) zou ik het toch handig vinden als ik deze objecten als geheel over kan sturen. Ik introduceer dus een extra optie ‘binairy’. D.w.z. serialisatie van het object door binnen de xml serialization slag bepaalde objecten als byte[] over te sturen via een binaryformatter geserialiseerd.

De tweede soort zou ik via de huidig te maken manier moeten kunnen (de-)serializeren. Een van de eisen is echter dat ik bomen wil kunnen opknippen: hiervoor introduceer ik dus een “lazy” optie. Handig dat ik nu een Id heb, aangezien ik die kan serializen en aan de andere kant de getter van het field kan aansluiten op het ObjectRequested(Id) event van de betreffende class. Dit is overigens wel iets waar ik momenteel een stukje AOP voor gebruik.

De derde soort, lijsten van IUniqueIdentifyable classes hoef ik niets voor te introduceren. Bij de lazy optie moet ik nu echter een lijst van GUID’s serializen i.p.v. een enkele.

Om mijn domainclasses nu te kunnen serializen creeer ik een custom attribute die mij helpt de metagegevens voor serialization toe te voegen.

public enum DomainSerializationOption
{ Ignore, Binary, Lazy, String, Typed }
public class DomainSerializationOptionsAttribute:Attribute
{
 
DomainSerializationOption option;
 
public DomainSerializationOptionsAttribute(DomainSerializationOption option)
  {
   
this.option = option;
  }
 
public DomainSerializationOption Option
  {
   
get { return option; }
  }
}

Dit stelt mij in staat mijn fields te annoteren met een bepaalde optie. De Ignore optie heb ik geintroduceerd voor zaken die niet interessant zijn om aan de andere kant van het kanaal beschikbaar te hebben.
Aangezien deze post nu eigenlijk al wat langer wordt dan de bedoeling was zal ik de custom (de-)serialization code niet toevoegen hier, maar het is wellicht illustratief om een geserializeerd object door te nemen:
<?xml version=”1.0″ ?>
<Employee>
 
<name serialization=”binary length=”40“>
   
<![CDATA[ AAEAAAD/////AQAAAAAAAAAGAQAAAARoZW5rCw== ]]> f(clean);  
  </name>
 
<address serialization=”typed“>empty</address>
 
<taxnumber /> 
  <function /> 
  <birthdate serialization=”typed“>2008-01-20T21:14:03.15625+01:00</birthdate> 
  <activities serialization=”lazy ids=”16d528eb-469c-4c87-aa9d-dadac047b587,0fb10f8f-e442-4d6e-b815-fbf0ced55292” /> 
</Employee>

Verschillende vormen van de serialisatie zijn hier toegepast. Door standaardisatie van de business object definitie over alle lagen heen, in combinatie met een lazy loading faciliteit in alle lagen is een ultieme vorm van distributie van het domeinmodel bewerkstelligd. Sterker nog, het maakt niet meer uit hoeveel lagen je ertussen zou willen hebben – ze zijn allen ultiem ontkoppeld.

Critici van de ‘puristische’ domain driven design school roepen echter vaak dat domain objecten nooit over een boundary mogen bestaan. Presentatielagen zullen ook altijd een andere partitie in objecten verwachten dan het business domein. Ik ben hier echter tegen – ik denk dat business domein objecten juist een goede representatie zouden moeten zijn om ook het gebruikersproces te ondersteunen. Als er echter ‘vreemde’ kunstgrepen gedaan moeten worden in de UI staat dat je ook in deze structuur natuurlijk vrij, echter het wordt dan wel beperkt tot die laag. Communicatie tussen de verschillende componenten gaat gewoon via de business object definitie.

In mijn optiek is het zelfs mogelijk om via deze structuur hybride oplossingen te maken – met lagen of componenten in zowel Dotnet als in Java. C# en Java zijn zodanig identiek dat het in mijn optiek mogelijk moet zijn om objecten geserialiseerd in .NET te de-serialiseren in Java en vice versa. Ik nodig de Javanen onder ons dan ook uit hierover mee te denken.

Mochten er nu nog lezers over zijn, graag reactie hierop! Code is overigens ook op verzoek beschikbaar.