DCI arkitekturen
- Thursday, March 18th, 2010
- Skriv en kommentar
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#).
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:
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:
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.
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.
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:
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:
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)
Kategorier: OO-design/clean code, Ruby.
RSS feed for kommentarene.
Tilbaketråkk.



March 18th, 2010 at 8:29 pm
Jeg har kommunisert med Trygve Reenskaug; han leste gjennom artikkelen min og kom med noen kommentarer. Jeg regner med det er greit for ham at jeg legger dem ut her:
Nå har jeg lest gjennom bloggen din og det ser for meg ut til at du har fått med det viktigste: Å forenkle dataklassene ved å trekke ut alle de metodene som har med samhandling å gjøre.
\”Merk at MoneySource ikke vet noe om Account, og Account vet ikke noe om MoneySource.\”
-> Bra!
\”MoneySource har også en property for å sette en mottager (@recipient)\”
-> Det hadde vært enklere hvis rollen (MoneySource) kunne spørre konteksten, da behøver ikke konteksten vite hvilke roller som aksesserer hvilke roller.
\”MoneyTransfer.execute(source, target, amount)\”
-> Flott. Implementasjonen av denne operasjonen er innkapslet i konteksten, jeg kan ikke se utenfra (fra skriptet) at dette er en DCI implementasjon.
PS: Reenskaug poengterte at han ikke kan nok Ruby til å forstå detaljene, men han trodde min løsning var relativ lik den han selv har implementert for samme use-case i Smalltalk.
PPS: Reenskaug anbefaler dem som er interessert i DCI å følge nyhetsgruppen object-composition på google groups.
March 20th, 2010 at 3:28 am
Dette virker da forferdelig tungvint. Har du et eksempel i ruby der det faktisk reduserer arbeid?
Jeg prøvde å implementere det i haskell med det jeg vil våge å påstå var mye bedre resultater:
http://hpaste.org/fastcgi/hpaste.fcgi/view?id=24152#a24152
March 20th, 2010 at 8:16 am
Første gang noen har pastet haskell-kode i kommentar på bloggen min. Takk! :D
Om DCI er tungvindt – vel, god objektorientering kan fort virke tungvindt. Helt til systemet vokser over en viss størrelse, mens man kontinuerlig må legge til mer funksjonalitet. Da svarer det seg å ha en god struktur. Fordelen med DCI slik jeg ser det er at den er lettere å finne frem i enn mere “mainstream” oo-design, og at klasser/moduler er løsere koblet, slik at endringer forhåpentligvis er enklere å gjennomføre uten å måtte refakturere så mye og rissikere å snik-innføre subtile bugs.
DCI handler om å implementere slik at det er best mulig samsvar mellom use-case og kode.
Om du derimot forsøker å si at det er tungvindt å bruke objektorientering fremfor funksjonell programmering, så gjerne det. Ofte er det vel slik at det man kan best er enklest. Få kodelinjer er ett mål på enkelhet, lesbarhet og evne til å forstå den totale løsningen et annet.
Har såvidt begynt å se på funksjonell programmering selv. Begynte med Haskell, men da jeg fikk øynene opp for Erlang så begynte jeg med det i stedet. Så kanskje jeg vil sette bedre pris på koden din etterhvert ;)
March 20th, 2010 at 8:30 am
PS til Ameth: Du har ikke implementert transaksjonsstøtten jeg har (såvidt jeg kan se). Du håndterer negativ balanse, og kaster feil fra “source”-kontoen.., det jeg har lagt opp til er tilbakerulling om noe annet skulle feile (tenkt database-aksess i “target”-kontoen). Hadde vært morsomt om du kunne demonstrert dette i Haskell også.
Ikke at det har noe spesielt med DCI å gjøre (det gjorde eksempelet mitt unødvendig tungvidt, men også mer realistisk).
March 20th, 2010 at 9:29 am
Nok en til Ameth: Siden du tok deg bryet å kode dette i Haskell, og siden du spurte om en enklere løsning i Ruby – la meg demonstrere hvordan det ser ut om jeg stripper bort det som ikke er helt nødvendig, og står igjen med samme funksjonalitet som det du hadde:
http://hpaste.org/fastcgi/hpaste.fcgi/view?id=24155#a24155
Virker det enklere nå?
Her er den enkleste Ruby-løsningen jeg kommer opp med om jeg ikke bruker DCI (eller objektorientering i det hele tatt): http://hpaste.org/fastcgi/hpaste.fcgi/view?id=24158#a24158 Dette er ikke noe jeg vil brukes i en real-life løsning.., men det er i alle fall en del enklere enn Haskell-implementasjonen.
March 20th, 2010 at 3:10 pm
Det virker som om bloggen din spiste kommentaren min
March 20th, 2010 at 3:11 pm
Eeeh, og det forsvant alt etter en < … Igjen:
Jeg skjønner hvordan objekt-orientering gjør kodebaser mer skalerbare og slikt, ja, og hvis det må bli et par tusen linjer før du kan vise til redusert arbeid skal du få slippe. Jeg bare forestiller meg at hvis du skal ha en klasse til hver rolle blir det en del slike etterhvert.
Her er et nytt eksempel i Haskell som kanskje er litt mer DCI-ete: http://hpaste.org/fastcgi/hpaste.fcgi/view?id=24161#a24161
Forskjellene er at jeg delte opp Balanceable i MoneySource og MoneyDestination og flyttet det over i STM-monaden (Software Transactional Memory) for atomisk oppdatering, beskyttet fra andre tråder og whatnot. STM lar seg dog ikke blande med databasetransaksjoner da disse krever IO, men det er relativt enkelt å legge til “Database.HDBC.withTransaction” på det forrige eksempelet.
Det enkleste Haskell-eksempelet jeg kom på var http://hpaste.org/fastcgi/hpaste.fcgi/view?id=24162#a24162
March 20th, 2010 at 5:54 pm
Haskell, eller funksjonell programmering generelt, er fasienerende, det må jeg si. Spennende å få helt nye synspunkter og perspektiv på det vi driver med.
Når det gjelder MANGE klasser.., i utgangspunktet ikke noe problem. Mange klasser er langt å foretrekke fremfor få og store klasser – det hjelper på å holde koblingen mellom ting til et minimum. Forestill deg et system som 10 utviklere har jobbet på i et års tid: Du åpner en vilkårlig fil og finner en klasse på kanskje 10-15 linjer, og på få sekunder skjønner du akkurat hvorfor klassen eksisterer og hvordan den fungerer, uten å måtte se i noen andre filer. Det er målet med god objektorientering!
Det finnes desverre ikke mange slike, men idealer er bra :)
March 26th, 2010 at 1:29 am
Du har sikkert sett den, men her er en liten pointer til Rickard Öbergs nylige artikkel om DCI vha Qi4J i Java: http://java.dzone.com/articles/implementing-dci-qi4j
Så med litt AOP så får man til DCI i Java, og sikkert i C# også :)
March 26th, 2010 at 10:36 am
@Thomas: I C# får du til DCI ved å bruke intefaces som roller og knytte logikk til disse rollene v.h.a extention methods på interfacene.
@Torbjørn: Spennende artikkel som satt igang mye lesing her i gården :) Et spørsmål: Er det mening at all logikken skal være knyttet til rollene (MoneySource,MoneyDestination), eller kan man tenke seg at logikken ligger i konteksten (MoneyTransfer) ?
I ditt eksempel kunne man tenkt seg at “transfer” logikken kunne vært plassert i MoneySource (slik som du gjorde), eller MoneyDestination. Den kunne også vært plassert i MoneyTransfer som da ville hatt transaksjonslogikken o.s.v.
Hva om man hadde tre, fire eller fem roller som skulle spille sammen ? Ville det ikke da vært natulig å ha transaksjonslogikken i konteksten og ikke nøvendigvis i rollene. Rollene er bare deltagere i det store spillet (e.g. algoritmetn) i konteksten.
Hva sier DCI om dette ? Noen tanker ?
March 28th, 2010 at 8:13 pm
Bjarte: Slik jeg tolker det skal logikken være i rollene ja, konteksten er miljøet som gjør at rollene kan spille sammen, men deltar ikke aktivt. Om vi sier at rollene som inngår i en transaksjon er en pengekilde og en pengemottager, så sier DCI at er det disse rollene som inneholder logikken nødvendig for å synkronisere og gjennomføre transaksjonen.
Jeg tenker at spørsmålet ditt egentlig dreier seg mer om DDD. Hvordan ser forretningsfolkene i dette domenet på verden? Når de snakker om en transaksjon, fokuserer de da på de to kontoene, eller ser de på selve transaksjonen som en entitet i seg selv? Er det siste mest riktig kan det være naturlig å si at transaksjonen er en egen rolle. Etter at vi har avgjort det kan vi bruke DCI for å implementere, og har da tre roller: MoneySource, MoneyDestination og MoneyTransaction. MoneyTransaction er nå ikke konteksten, men en fullverdig rolle – konteksten kommer i tillegg.
Jeg har aldri jobbet i bank, og vet ikke hva som vil være mest forståelig for en i det domenet, men for meg virker det mest naturlig å ikke modellere MoneyTransaction som en egen rolle. Den “orginale” OO-ideen var at objekter snakker sammen for å oppnå adferd, mens å lage et transaksjons-objekt for å inneholde transaksjonslogikken egentlig er prosedyre-tankegang i forkledning. Men det finnes mange måter å designe software på, og få fasiter.
Men for å svare helt klart på spørsmålet ditt (i forhold til min forståelse av DCI): Poenget med rollene DCI er å samhandle. Samhandlingen skal ikke implementeres “på utsiden” (i konteksten).
Den virkelige verden består bare av deltagere (roller). Algoritmer oppstår når de kommuniserer. Dette er det Dr. David West kaller Object Thinking (anbefaler boken). I imperativ programmering fokuserer vi ofte på å implementere algoritmer steg for steg, og dette er altså ikke, i følge en del kloke hoder, god bruk av objektorientering. Fantastisk software oppstår når objekter “snakker sammen” som i den virkelige verden.
March 28th, 2010 at 8:33 pm
… og jeg har flere tanker. La meg forsøke å utdype det jeg sa i forrige svar …
Fra min anbefaling av Object Thinking: “Objekt-tenking går rett og slett ut på å tenke som et objekt. Tradisjonelle utviklere tenker som datamaskiner. De konsentrerer seg om den tekniske løsningen, om implementasjon.”
Når vi designer algoritmer tenker vi at det finnes en sentral enhet som koordinerer ting. Jeg tror dette er en veldig menneskelig måte å se verden på. Vi tenker f.eks. at vi har en sentral, imperativ hjerne som styrer oss. Virkeligheten er anderledes – hjernen forstås bedre som en stor samling paralelle prosesser (med mye fuzzy logic) som kommuniserer og kommer opp med forslag og forståelse i felleskap.
Vi snakker også om at livet har mening, og at det er en mening bak ting. Vi ser på historien som en lineær hendelsesrekke som gir mening, selv om det ofte egentlig preges mer av kaos. Vi dikter opp overnaturlige krefter som står over oss og koordinerer – den ultimate algoritmen. Vi snakker om fri vilje, selv om begrepet ikke gir rasjonell mening.
(Mine innlegg om fri vilje: her, her og her.)
Mennesker setter ting i system for å forsøke å foutse hva som skjer siden. Vi gir systemene vi “ser” navn og tyngde, men systemene eksisterer egentlig ikke på den måten. De er samhandling mellom ulike entiteter. Helheten er større enn delene. På samme måte ønsker jeg å designe software som er større enn objektene det består av. Algoritmen er måten objektene kommuniserer på!
March 29th, 2010 at 1:04 pm
Takk for mye tilbakemelding. Jeg har studert DCI mer siden sist, og har også komment frem til at det er meningen at logikken skal være knyttet til rollene. Fra et DCI synspunkt er det da ikke diskutabelt slik jeg ser det.
Sitat: “Den virkelige verden består bare av deltagere (roller). Algoritmer oppstår når de
kommuniserer.”
Veldig interessant kommentar, og observasjonen er nok korrekt. Likevel tror jeg at dersom mennesker tenker på algortimer som separate “ting” så kan det være verdifullt å bruke den ideen i koden. DCI-tanken om å få utviklerens (og brukerens) mentale modell inn i koden er årsaken til det. Dette er jo litt DDD tankegang. Allestedsnærverende språk, o.s.v.
Ett av argumentene jeg ser når Trygve og James (later som jeg kjenner de på fornavn) snakker om DCI er at i tidligere modeller var algortimen (“what the system does”) fragmentert på tvers av domene objektene (“what the system is”). For å løse dette lar de ulike objeter spille ulike roller i en gitt kontekst. Algoritmen er nå knyttet til rollen i den gitte kontekten, men er ikke algoritmen som tidligere var fragmentert på tvers av objektene, nå fragmentert på tvers av rollene? Jeg sier ikke at denne tankegangen er korrekt OOD, men det gir kanskje grunnlag til litt småfilosofering :) Å knytte algoritmene til rollene er uansett fremskritt.
Etter å ha undersøkt DCI nærmere ser jeg at det egentlig bare er en reformulering av Udi Dahans “Making roles explicit” presentasjon. Det er i alle fall en del av puslespillet. Hvem som kom først, skal jeg ikke uttale meg om.
Trygves ide om å lage vertøy som støtter DCI utvilkingsmodellen er spennende. Blir interessant å se hvilke verktøy som dukker opp i .NET verden i fremtiden.
Når det gjelder fri vilje, så tror jeg ikke at jeg skal gå inn på det. Det ville blitt for mye tekst for en kommentar. Jeg har ikke landet på “hard determinism”, men jeg er ikke langt unna.
December 10th, 2011 at 8:09 am
[...] å utvide objektene på måter Java/C# ikke kan (for eksempel sånn som jeg presenterte i posten om DCI arkitekturen i mars [...]