OOP - vffå det då?
Förord
Det har redan gått en tid sedan VB.NET kommit ut, och de flesta VB-programmerare har nog åtminstone kikat lite på det här med objektorienterad programmering. Men många har fortfarande kvar att på allvar ta steget över till OOP. Därför kan det kanske komma väl till pass med en artikel om bakgrunden till OOP. Det vill säga inte så mycket ”hur” man skriver objektorienterat, utan snarare ”varför”.
OOP – för vem?
Låt oss börja med att slå fast att det inte är för datorns skull som OOP har utvecklats. När väl kompilatorn har gjort sitt, och man är nere på CPU-instruktionsnivå, är det inget som fungerar annorlunda för att man OOP:at. Istället är det för människans skull, i synnerhet för programmerarna, men även kravställare, systemarkitekter och liknande, som OOP vuxit fram. Och det handlar i grunden om hur vi tänker, det vill säga hur våra hjärnor är funtade.
Sju, plus/minus två
Har du hört talas om 7±2-regeln? Enligt kognitionspsykologer kan den mänskliga hjärnan som mest hålla 5-9 informationsbitar i huvudet samtidigt, beroende då på begåvning, dagsform, motivation och så vidare. Om man vill dra liknelsen med datorn har vi alltså 5-9 register. Låter det nedslående? Detta är ändå den optimistiska varianten – andra menar att det snarare är 5±2! Behöver vi ta in andra faktorer i problemlösandet måste vi radera någon befintlig informationsklump, och hämta in den nya faktorn från annat, långsammare minne.
Inom parantes sagt, finns det något jobbigare än programmerare vars främsta egenskap är att de ständigt kodar för att utnyttja så många ”register” som möjligt? Alla funktioner blir tre sidor långa, och balanserar hela tiden på gränsen för vad som är möjligt att hålla i huvudet på en gång.
Kvalitét
Oavsett vad man anser om just denna teori visar den ändå på något vi alla inser – att människan har en begränsad problemlösningsförmåga. Däremot, vilket är en stor skillnad jämfört med datorn, är informationsmängden i ett ”register” inte så bestämd och begränsad. Tänk dig en musiker som läser noterad musik. Han läser inte enskilda noter, kollar tonarten i början av raden för att se om tonen är höjd eller sänkt, hittar tonen på sitt instrument, kollar tonens längd, och försöker sedan spela den. Han ser hela fraser.
Eller schackspelare - undersökningar har visat att erfarna schackspelare är bättre på att komma ihåg uppställningar än nybörjare eftersom de inte ”ser” enskilda pjäser, utan strategiska mönster. När de memorerar speluppställningar kan de nästan alltid placera ut de spelmässigt centrala pjäserna rätt, medan mindre aktiva pjäser kan placeras fel. Om man däremot slumpar ut pjäserna på brädet har inte schackspelarna alls så stor glädje av sina kunskaper och erfarenheter, och deras resultat börjar likna amatörernas.
Vad har då detta med programmering att göra? Jo, en av de viktigaste poängerna med OOP är att det låter utvecklarna skapa bra abstraktioner. Istället för enstaka instruktioner, rad för rad, eller enstaka funktioner, kan vi knyta ihop dessa till informationspaket till abstrakta enheter som bara upptar ett ”hjärnregister”.
Första klass
Grundregeln för att skapa klasser som blir bra abstraktioner är att de ska ha ”low coupling & high cohesion”. På svenska blir det ungefär låg beroendegrad och hög sammanhållning, vilket, om möjligt, låter ännu mer intetsägande. Så en förklaring är nog på sin plats.
Parvis!?
Låg beroendegrad syftar på klassens externa beroenden, det vill säga i vad mån klassen står på egna ben. Kan man använda klassen separat, eller måste den användas tillsammans med vissa andra klasser? Tänk dig att du ska skriva ett litet testprogram som går igenom klassens hela publika gränssnitt. Varje ytterligare klass som du måste ta med i ditt testprogram är ett externt beroende. Den typ av klasser som inte har några externa beroenden alls kallas löv-klasser (leaf classes), eftersom de befinner sig längst ut i ditt programs (förhoppningsvis) trädliknande struktur. Denna klasstyp är den enklaste att skriva separata testprogram för. Några tänkbara kandidater är klasser för geometriska punkter, personnummer, datum, och liknande.
Nu blir det inte mycket till program om man bara har lövklasser. Den regel som gäller är alltså så låg beroendegrad som möjligt, med betoningen på ”som möjligt”. I OOP finns det många typer av beroenden, som är-relationen (arv), har-en relation (aggregering), använder-relationen (association), så det får vi ta en annan gång.
En varning är dock på plats för det man verkligen vill undvika, och det är ömsesidiga beroenden, det vill säga att klass A är beroende av klass B, och B är beroende av A.
Om vi säger att klass A är någon sorts fönsterhanterare, och B är en geometrisk figur, säg en färgglad kub, och A innehåller en B så har vi ett ensidigt beroende som är helt OK. Om däremot A skulle skicka in sig själv som parameter till Draw-funktionen i B uppstår ett ömsesidigt beroende.
Ett problem vi nu hamnar i är att vi inte längre kan testa varken A eller B separat. Det finns även mer komplexa varianter på detta där flera klasser är involverade, men det gör bara saken ännu värre.
Ännu värre är att program som har många ömsesidiga beroenden skapar en spaghettistruktur i huvudet på programmerarna. Hjärnan har lätt att förstå och memorera trädliknande strukturer med enkelriktade beroenden, medan strukturer med beroenden både på korsan och tvärsan mest liknar slumpmässigt utställda schackpjäser.
En enkel lösning vårat exempel är att klass A skickar in ett ”device context” till Draw-funktionen i B. Då blir både A och B beroende av device contexten, men B är inte längre beroende A. För att testa eller använda B behöver vi en device context, men det är knappast möjligt att undvika. Nu kan vi börja med att testa igenom B, för att sedan röra oss uppåt i programmets struktur och testa A.
Hög standard?
Hög sammanhållning betyder att klassen som begrepp är entydigt och väl avgränsat. Det stora litmustestet för detta är att man kan hitta ett bra namn på klassen som är både begrepligt och faktiskt speglar vad klassen är och gör. Ett extremt exempel på en klass med dålig sammanhållning är en klass ”Cube” som, förutom kuborienterade funktioner, även räknar ut kontrollsiffran i personnummer och skriver ut självtestsidan på HP LaserJet-skrivare. I det här fallet är namnet ”Cube” enkelt och relativt entydigt, men det speglar inte vad klassen faktiskt gör.
Om vi istället döper om den till ”CubeAndPersonummerAndPrinterSelfTest” får vi ett namn som stämmer med vad klassen gör, men begreppet är så luddigt och osammanhängande att det knappast får plats i ett ”hjärnregister”.
Ett mer realistiskt exempel på hur man som OOP:are kan lockas att förstöra sin goda sammanhållning :-) är om vi har en klass ”Personnummer” som kollar validitet, räknar ut kontrollsiffra, kön, ålder och födelseort. Sedan förändras kraven så att vi även ska hantera företags- och organisationsnummer.
Om vi i detta läge helt enkelt lägger till nya funktioner i klassen har vi både sett till att vissa funktioner inte längre fungerar som de ska – som när vi försöker kolla könet på ett aktiebolag – och förstört våran fina abstraktion. Personnummer betyder inte längre samma sak, utan har blivit ett luddigt begrepp som vi inte längre kan lita på. Det minsta man bör göra i detta läge är att döpa om klassen till JuridiskPersonIdentitet eller liknande.
But, uuuh, shouldn’t there be some kind of structure?
Om C++ är en vidareutveckling av C kan man på motsvarande sätt säga att OOP är en sorts strukturerad programmering++. Kostnaden för att gå över till OOP är att man måste tänka mer på programmets design och struktur när man kodar. Man tvingas hoppa mellan att lösa vanliga programmeringsuppgifter, typ modula-10 valideringar av personnummer, och designfrågor. Behöver man en en separat klass för personnummer, eller rentav av liten klasshierarki med JuridiskPersonIdentitet som basklass till Personnummer och Organisationsnummer? Så, ena stunden programmerarkepsen, och i nästa designerhatten!
Om man mestadels skriver små program, som man ändå kan hålla i huvudet, och som inte är tänkta att vara särskilt långlivade, är kanske inte denna kostnad motiverad.
Fördelarna med OOP märks mer i större program. Genom att skapa bra klasser förser man sig med entydiga och klart avgränsade begrepp som låter oss förstå och tala om programmet på en högre abstraktionsnivå. Istället för att vara ett lite luddigt försteg innan själva kodningen har man i OOP möjlighet att lägga in designen direkt i koden. Ja, det går nästan inte att undvika.
0 Kommentarer