tirsdag 5. januar 2010 OOP
Konseptet objektorientering (OO) ble født allerede på 60-tallet; Simula, som ble utviklet på Norsk Regnesentral med Ole-Johan Dahl og Kristen Nygaard i spissen, var det første objektorienterte programmeringsspråket. Det var likevel ikke før på 90-tallet at OO ble mainstream. I dag foregår det meste av moderne systemutvikling i objektorienterte språk.
Men det at man bruker et slikt språk er ikke det samme som at man driver med god objektorientering.
Et ganske vanlig design som ikke er bra OO er systemer som inneholder to fundamentalt forskjellige typer objekter: (1) entiteter som først og fremst er bærere av data – som inneholder properties, og ikke så mye mer, og (2) objekter man typisk kaller controllere (eller handlere eller managere) som bruker den første typen, og som inneholder all logikken i systemet. Dette er et prosedyreorientert system, og det man bedriver kalles imperativ programmering – man instruerer hele tiden programmet om hva som skal gjøres, i stedet for å be objektene selv om å gjøre jobben.
Jeg forsøker hele tiden å lære både meg selv og andre hvordan man designer et system etter gode OO-prinsipper, og tror jeg har kommet frem til noen av de mest grunnleggende retningslinjene man bør følge – noen enkle huskeregler, eller knep - man bør ha i bakhodet når man programmerer. Prinsippene er ikke noe jeg selv har funnet på (langt i fra), men representerer de enkleste prinsippene man kan bruke hele tiden for å gå fra prosedyreorientering til god objektorientering.
Det første prinsippet er TELL, DON'T ASK! Si at objektet skal gjøre noe for deg, ikke spør det om informasjon. Implisit i dette er blant annet at man skal unngå getters og setters, dvs. properties i .net-klasser. Properties brukes til å spørre om et objekts tilstand, eller til å eksplisit endre tilstanden, og da skjuler man ikke lenger hvordan objektet fungerer.
Hvis du har kode som tar avgjørelser basert på tilstanden til et objekt, se om du i stedet kan flytte denne koden inn i objektet selv. Har du et slikt prosedyreorientert system som jeg nettopp beskrev, så forsøk å flytte kode ut fra handlerne (type 2) til dataobjektene (type 1). Gjør du det vil du snart se at propertiene blir overflødige.
Eller gå motsatt vei: Hvis du tar utgangspunkt i et objekt som nesten bare inneholder properties – undersøk hvor disse propertiesene brukes, og se om du kan flytte den koden til objektet.
Her følger et banalt eksempel. Du finner sjelden kode som er så tydelig feilplassert som dette her i ditt eget design, men det illustrerer i alle fall poenget mitt:
1 public class DogManager
2 {
3 private void Manage(Dog dog)
4 {
5 if (dog.IsSleepy) dog.Sleep();
6 else if (dog.IsHungry) dog.Eat();
7 else if (dog.IsRestless) dog.BegOwnerForWalk();
8 else dog.SniffSomething();
9 }
10 }
DogManager spør hunden om dens tilstand, og tar avgjørelser om hva den skal finne på basert på dette. Koden bør flyttes til Dog:
1 public class Dog
2 {
3 public void DoSomething()
4 {
5 if (IsSleepy) Sleep();
6 else if (IsHungry) Eat();
7 else if (IsRestless) BegOwnerForWalk();
8 else SniffSomething();
9 }
Et prinsipp som er nært knyttet til Tell, Don't Ask er LAW OF DEMETER (LoD). Denne "loven" sier at man skal begrense hva objekter vet om resten av systemet. Enheter (objekter/metoder) skal kun snakke med sine venner, ikke fremmede. Man begrenser altså koblingen mellom enhetene i systemet, og oppnår dermed blant annet færre bieffekter ved endringer.., slik som jeg var inne på tidligere.
LoD sier mer konkret at man i en gitt metode (M) i et objekt (O) kun kan kalle metoder på:
1) Objektet O selv
2) Parametre som sendes til metoden M
3) Objekter som opprettes i metoden M
4) Objekter som lever i O (objektets private variabler)
Man skal altså ikke kalle metoder på objekter man får returnert fra andre (via properties eller metodekall). Husker man hele tiden på dette når man koder vil man oppnå bedre OO. Her er en ny, banal illustrasjon:
1 private void WalkTheDog(Dog theDog)
2 {
3 theDog.Legs.Move(); // violates LoD
4 }
5
6 private void WalkTheDog2(Dog theDog)
7 {
8 theDog.Walk(); // makes more sense..
9 }
Det gir ikke mening at denne metoden skal vite noe om hvordan hunden fungerer – at bena må bevege seg for at hunden skal gå. Be heller hunden om å gå direkte, det er hunden som selv skal vite hvordan den fungerer (detaljene skal innkapsles og skjules).
Legg også merke til at brudd på LoD ofte kan gjenkjennes ved flere punktum: theDog punktum Legs punktum Move(). Dog er metodens nærmeste venn, Legs er en fremmed, og skal ikke kommuniseres med.
Den tredje og siste huskeregelen som vil gjøre deg til en bedre utvikler er at OO ikke nødvendigvis skal modellere virkeligheten! Dette er en vanlig misforståelse som mange av oss fikk innprentet da vi begynte å lære objektorientert programmering. Om vi begynner å designe et system ved å lage klasser av alle entitetene vi kan observere i det domenet vi skal modellere kommer vi ofte skjevt ut. Begynn i stedet med adferden du ønsker, og se hvilke objekter som "naturlig" kommer ut av det.
Skal vi for eksempel designe et kontrollsystem for en bil kan det være veldig feil å starte med å lage klasser for hjul, bremser, ratt, motor, bensintank, gir og bilen selv. Et system med klasser som drivstoffregulator, bremsekraftforsterker, oljenivå og servostyring vil kanskje bli enklere og mere oversiktelig. Testdreven utvikling (TDD) vil kunne hjelpe deg til å drive frem det beste designet.
De tre huskereglene jeg har presentert, sammen med de to kanskje aller viktigste programmeringsprinsippene – Single Responsibility (SRP) og Don't Repeat Yourself (DRY) – vil sørge for at du designer kode som er enklere å forstå, og lar seg vedlikeholde, endre og utvide.
Fortell objektene hva de skal gjøre på enklest mulig måte, ikke spør de hva de holder på med. Mange properties (eller andre typer gettere og settere) betyr sansynligvis at du kan gjøre designet bedre. Ikke la objektene dine snakke med fremmede. Og ikke begynn med å modellere virkeligheten, driv i stedet frem objekter du trenger gjennom å starte med implementering av adferd.
Helt til slutt: Jeg snakker her om hvordan ideell, objektorientert kode ser ut. OO er ikke den optimale løsningen for alle problemer. Lager du f.eks. en enkel CRUD-applikasjon er full objektorientering overkill. OO egner seg bedre jo mere adferd systemet skal ha. Se for eksempel posten om Greg Young's foredrag i Bergen, hvor han bruker OO på "command-siden" av systemet, men ikke på "query-delen".
Noen eksterne lenker: Law of Demeter | Tell, Don't Ask | Object-oriented programming | Single Responsibility Prinsiple | Don't Repeat Yourself
PS: Denne blogposten begynte som en intern ZipTalk jeg holdt for utviklingsavdelingen i PSWinCom. Tittelen var da "OO > O".