DCI arkitekturen


torsdag 18. mars 2010 OO Patterns Ruby

Eller: Hvordan jeg fikk sansen for multippel arv.

På QCon London fikk jeg ved en tilfeldighet med meg en halv forelesning med Jim O. Coplien, som snakket om noe han kalte The DCI Architecture. DCI står for Data-Context-Interaction, og er ifølge Coplien en bedre måte å designe systemer på enn dagens "normale" bruk av objektorientering.

"Object-oriented programming was supposed to unify the perspectives of the programmer and the end user in computer code: a boon both to usability and program comprehension. While objects capture structure well, they fail to capture system action. DCI is a vision to capture the end user cognitive model of roles and interactions between them."

Fakta:
Opphavsmannen til DCI er professor ved Univeritetet i Oslo Trygve Reenskaug. Dette er samme nordmann som formulerte model-view-controller mønsteret i 1979.

For å få en komplett forståelse av hva denne visjonen er for noe bør du klikke på referansene i slutten av blogposten. Det du får her er en liten demo av min forståelse av DCI, gjennom at jeg ved hjelp av Ruby implementerer overføring av penger mellom to kontoer. Valget av programmeringsspråk er ikke tilfeldig – for å få til DCI er jeg avhengig av støtte for multippel arv, noe Ruby har i form av mix-ins. Og for at det skal bli virkelig elegant må jeg kunne påføre arven dynamisk i runtime.

La oss anta at vi allerede har en Account-klasse i domenet vårt som representerer en bankkonto. Den er veldig enkel (eller blodfattig som mange vil si), og har bare to read-only properties: navn og balanse. I tillegg har den en en liten metode for å representere kontoen som en streng (to_s tilvarer ToString() i C#).

account.rb:
1 class Account
2   attr_reader :name, :balance
3   def initialize name, balance
4     @name, @balance = name, balance
5   end
6   def to_s
7     "#{@name}\t$#{@balance}"
8   end
9 end

Når vi nå skal implementere overføring av penger mellom kontoer rører vi ikke den eksisterende Account-klassen. Vi vil i stedet implementere denne nye adferden i helt egne entiteter. Det å overføre penger er i teorien et abstrakt begrep, noe som kan foregå mellom andre ting enn bare Accounts. Vi vil derfor snakke om roller i stedet – overføring av penger har to roller: sender og mottager. Først implementerer jeg senderen, som jeg kaller for MoneySource:

money_tranfer.rb (del 1):
1 require 'transaction/simple'
2
3 module MoneySource
4   class InsufficientFundsError < StandardError; end
5   attr_writer :recipient
6
7   def transfer amount
8     validate_transfer_of amount
9     Transaction::Simple.start(self) do |trans|
10       begin       
11         remove amount
12         @recipient.receive amount
13       rescue
14         log "Aborting transfer of $#{amount} from #{name}"
15         trans.abort_transaction
16       end
17     end
18   end
19   def validate_transfer_of amount
20     raise InsufficientFundsError if @balance < amount
21   end
22   def remove amount
23     @balance -= amount
24     log "Removing $#{amount} from #{@name}"
25   end
26 end

MoneySource har en metode kalt transfer som tar et beløp som eneste parameter. Først valideres det at det er tilstrekkelig med penger for å foreta overføringen. Deretter startes det en transaksjon ("transaction-simple" en et lite Ruby-biblotek som gir grei, in-memory transaksjonsstøtte) hvor MoneySource først fjerner det gitte beløpet fra sin egen balanse, for deretter å sende det samme beløpet til en mottager. Transaksjonen avbrytes om noe av en eller annen grunn skulle gå galt.

Merk at MoneySource ikke vet noe om Account, og Account vet ikke noe om MoneySource. Antagelsen MoneySource gjør er at den har variablene @name og @balance.., det er ikke tilfeldig at Account har det samme.

MoneySource har også en property for å sette en mottager (@recipient), og antar at denne har en receive-metode. Det er nå på tide å implementere den andre rollen, nemlig MoneyDestination:

money_tranfer.rb (del 2):
28 module MoneyDestination
29   def receive amount
30     @balance += amount
31     log "Adding $#{amount} to #{@name}"
32   end
33 end

I design time følger vi Single Responsibility Principle – hver modul/klasse har ett klart definert ansvar (om du lurte så er Accounts ansvar er å ha en balanse) – og vi unngår som nevnt tett kobling mellom entitetene. DCI er også en løsning som følger Open-Closed Principle, og er i høy grad en smidig arkitektur. Vi forsøker å unngå polymorfisme; alt er definert ett tydelig sted – vi har ingen virtuelle metoder som normalt gjør det vanskeligere å finne frem i koden.

"A program that follows the DCI paradigm exposes its inner workings to a reader of its code." 

I runtime derimot vil vi i DCI-arkitekturen gi objektene roller i ulike kontekster, som lar dem samhandle på nye måter. Account er et data-objekt (D'en i DCI). MoneySource og MoneyDestination er roller som definerer interaskjonen (I'en i DCI) mellom objekter i en gitt kontekst (C'en i DCI). Rollene arves altså inn når de behøves.., et gitt objekt kan bekle mange, ulike roller. Det er her multippel arv kommer inn i bildet.

Den siste modulen jeg trenger definerer selve konteksten: MoneyTransfer. Den har en execute-metode som tar tre parametre: et objekt som skal være kilde, et objekt som skal være destinasjon, og beløpet som skal overføres. Execute utvider kilden med MoneySource (linje 37: MoneySource-modulen mikses inn i source-objektet i runtime). På samme måte utvides destinasjonen med MoneyDestination. Source har nå fått en property recipient og en metode transfer (som tidligere definert i MoneySource). Disse benyttes i linje 39 og 40 til å utføre overføringen.

money_tranfer.rb (del 3):
35 module MoneyTransfer
36   def self.execute source, target, amount
37     source.extend MoneySource
38     target.extend MoneyDestination
39     source.recipient = target
40     source.transfer amount
41   end
42 end

I denne demoen er source og target Account-objekter, men de behøver ikke være det.

Her er et lite skript som bruker MoneyTransfer. De fleste detaljene er uvesentlige og er derfor utelatt.

tranfer.rb:
47 require 'account'
48 require 'money_transfer'
49
50 setup_accounts # details omitted
51 list_accounts # details omitted
52 source = get_account 'Select account to transfer money from'
53 target = get_account 'Select account to transfer money to'
54 amount = get_amount  'Specify amount to transfer'
55 MoneyTransfer.execute(source, target, amount)
56 list_accounts

Jeg setter opp noen kontoer, lister dem i konsollet, og ber brukeren om å spesifisere source, target og amount. Jeg kaller så MoneyTransfer.execute, og lister kontoene igjen. Her er et eksempel på en slik overføring:

Sample run #1:
C:\Users\tormar\ruby_projects\DCI>transfer.rb
0: Salery       $1000
1: Usage        $1000
2: Savings      $1000
Select account to transfer money from: 1
Select account to transfer money to: 2
Specify amount to transfer: 800
Tue Mar 16 16:24:07 +0100 2010: Removing $800 from Usage
Tue Mar 16 16:24:07 +0100 2010: Adding $800 to Savings
0: Salery       $1000
1: Usage        $200
2: Savings      $1800

Siden jeg implementerte transaksjonshåndtering vil jeg også demonstrere hva som skjer om target av en eller annen grunn skulle kaste et exception (selv om det ikke har så mye med temaet å gjøre). Den røde logge-linjen kommer fra MoneyDestination:

Sample run #2:

C:\Users\tormar\ruby_projects\DCI>transfer.rb
0: Salery       $1000
1: Usage        $1000
2: Savings      $1000
Select account to transfer money from: 0
Select account to transfer money to: 2
Specify amount to transfer: 1000
Tue Mar 16 16:28:11 +0100 2010: Removing $1000 from Salery
Tue Mar 16 16:28:11 +0100 2010: Error: not able to receive money right now
Tue Mar 16 16:28:11 +0100 2010: Aborting transfer of $1000 from Salery
0: Salery       $1000
1: Usage        $1000
2: Savings      $1000

DCI er altså en reaksjon på det "oppfinnerne" ser på som en gal bruk av objektorientering. De hevder at måten vi begynte å bruke polymorphism, coupling, cohesion, etc da vi oppdaget OO på 80- og 90-tallet strider mot objektorienteringens mål, som var å gi et bedre samsvar mellom software og folks mentale modell av virkeligheten. De vil gjøre systemets adferd mer eksplisitt ved å gjøre roller til fullverdige entiteter – løsrevet fra objektene.

Dette får meg til å tenke på Command Query Responsibility Segregation (CQRS) som er så i skuddet for tiden – er ikke det også på en måte en reaksjon på det samme? Der gjør man i alle fall adferden eksplisitt gjennom command-objekter, og skreddersyr øvrig domenemodell i forhold til dem.

En siste tankevekker: Coplien og Reenskaug er to av dem som (såvidt jeg forstod) hardnakket hevder at språk som Java og C# ikke er orntlige, objektorienterte språk. I C# dreier egentlig alt seg om design av klasser, mens objekter har en mere sentral posisjon i språk som Ruby – hvor de f.eks. kan endre karakter fullstendig under kjøring av programmet. Man bryr seg sjelden om hvilken type et objekt har i Ruby, bare hvilke egenskaper det tilbyr. Jeg vet ikke om det er så fruktbart å stå på den barikaden, men det er interessant å diskutere forskjellen.

"Roles are about objects and how they interact to achieve some purpose. For thirty years I have tried to get them into the into the main stream, but haven't succeeded. I believe the reason is that our programming languages are class oriented rather than object oriented. So why model in terms of objects when you cannot code them? And why model at all when you cannot keep model and code synchronized?" –Trygve Reenskaug

Til slutt må jeg få gjenta det jeg sa innledningsvis, ikke døm DCI ut fra det du har sett her. Dette har vært min tolkning, så gå til kildene for en dypere forståelse, som er: The DCI Architecture: A New Vision of Object-Oriented Programming | The Common Sense of Object Oriented Programming (pdf) | DCI på wikipedia | Trygve Reenskaug (UiO)


comments powered by Disqus