10. Grunderna i Objekt­Orienterad Design

 

 

·     Grunderna i objektorienterad design.

 

·     Abstraktion.

 

·     Inkapsling.

 

·     Klasshierarkier.

 

·     Konsten att arbeta objektorienterat.

 

·     Komposition kontra arv.

 

·     Multippelt arv.

 

 

 

 

 

 

 

 

 

 


Grunderna i objektorienterad design.

 

Vi har nu gått igenom de grundläggande reglerna för programmeringsspråket C++ ur rent teknisk synvinkel. I detta och nästa kapitel kommer vi att diskutera objektorienterad programmering, vilket C++ egent­ligen är avsett för. Detta ka­pitel diskuterar principerna i objektorienterad pro­grammering, och hur man till­ämpar dem. Nästa kapitel innehåller ett exempel som tillämpar dessa principer.

 

Det första avsnitten i detta kapitel ger en överblick om objektorienterad pro­gram­mering. Resten av kapitlet utvecklar detta till en diskussion om hur man designar ett objektorienterat program.

 

I traditionell procedurorienterad programmering innehöll programmet ett antal bearbetningssteg som skulle utföras, en logik av något slag. I det objektorien­terade synsättet beskriver programmet hur ett antal objekt arbetar tillsammans. Det går visserligen att skriva procedurorienterade program m.h.a. C++, men om man arbetar objektorienterat utnyttjar man språket till fullo.

 

Det finns ett par viktiga begrepp i objektorienterad programmering. Det mest grundläggande är abstraktion, vilket underlättar själva programmeringsarbetet. Ett annat är inkapslingen (encapsulation), vilket underlättar ändringar och sys­tem­underhåll. Slutligen har vi klasshierarkin, ett kraftfullt verktyg som under­lättar tillägg och överblick. Ett objektorienterat språk innehåller just de verktyg som behövs för att implementera dessa begrepp.

 


Abstraktion.

 

Abstraktion handlar om att man ska kunna ignorera detaljerna när man arbetar med helheten. Ett programmeringsspråk anses generellt vara ett högnivåspråk om det möjlighet till en hög nivå av abstraktion. Ta t.ex. två program som utför samma uppgift, ett skrivet i assembler, ett i C. Assemblerprogrammet innehål­ler varje liten detalj datorn måste göra för att utföra uppgiften, trots att program­merare normalt sett inte är intresserade att påverka sådant som sker på den niv­ån. C-programmet beskriver datorns bearbetning på en mycket mer ab­strakt ni­vå, och abstraktionen gör programmet tydligare, och lättare att förstå.

 

När man programmerar procedurorienterat har man viss möjlighet att skriva ab­strakt, men objektorientrade programmeringsspråk innehåller mycket effektiv­a­re mekanism för abstraktion. För att inse detta behöver man betrakta olika slags abstraktion:

 

·     Procedurabstraktion.

·     Dataabstraktion.

·     Klasser.

 

Procedurabstraktion.

 

Den vanligaste formen av abstraktion är procedurabstraktion, vilket låter programmeraren slippa tänka på de detaljer som ingår i en procedur när han använder den.

 

Proceduriell abstraktion kan implementeras på många olika nivåer. Man kan t.ex. beskriva ett program på så låg nivå som mnemonics, det som man använ­der i assembler, och betrakta t.o.m. varje bearbetningssteg i mikroprocessorn. Man kan å andra sidan beskriva bearbetningen i form av makroinstruktioner, vilket kan resultera i en abstraktionsnivå som ligger t.o.m. högre än C.

 

När man använder ett programmeringsspråk är man inte bunden till den abstrak­tionsnivå programmeringsspråket är gjort för. De flesta programmeringsspråk tillåter nämligen att man själv skapar funktioner (procedurer, subrutiner etc.), vilka utgör en abstraktion av det som de utför. I stället för att fundera över hur en uppgift löses varje gång, kan man ha löst den i en separat funktion, varvid abstraktionen går ut på att man anropar funktionen i stället för att koncentrera sig på den kod funktionen innehåller.

 

Man kan t.ex. ha behov av att jämföra två textsträngar med varandra, men om man programmerar i C finns inte den möjligheten inbyggd i språket. Ska man i så fall skriva en programslinga som jämför de i texterna ingående tecknen par­vis varje gång man ska jämföra två texter blir programmet ganska detaljerat. Om man i stället deklarerar en funktion som jämför två valfria strängar och ger den ett lämpligt namn, kan man i sin programlogik använda detta namn i stället för att skriva programslingan. En jämförelse av två strängar kan då t.ex. göras på detta sätt:

 

if(Jamfor(cText1, cText2)

{

    // Texterna är lika.

    ...

}

else

    // Texterna är olika.

    ...

}

 

De flesta kompilatorleverantörer har ‘samlat upp’ programmerarnas olika funk­tioner och levererar dessa med kompilatorn. Således levereras ett stort antal funk­tioner med C-kompilatorn, varav en del är skrivna i C medan andra är skriv­na i assembler. Dessa funktioner höjer möjligheterna till abstraktion väs­entligt. Det finns t.ex. jämförelsefunktioner som är ‘bättre’ än ovanstående, dels strcmp(), vilken levererar en lexikografisk jämförelse mellan strängarna, dels stricmp(), vilken gör samma jämförelse med den skillnaden att den bortser från skillnader mellan versaler och gemener.

 

Användning av funktioner handlar inte bara om att spara på skrivarbetet, det handlar även om att göra programmen mer överskådliga och lättlästa, och där­igenom underlätta systemunderhåll och rättningar. Ett väl valt funktionsnamn, som förklarar vad som händer vid en viss punkt i programmet, är mycket lättare att förstå än ett antal programsatser som utför arbetet ‘på plats’. Tyvärr är inte alla funktionsnamn väl valda, C-programmerare tenderar att spara på skriv­ar­bet­et genom att förkorta namn till oigenkännlighet. Ovan nämnda funktioner borde hetat ‘StringCompare()’ respektive ‘StringCompareCaseInsensitive()’.

 

Användning av funktioner, procedurabstraktion, gör det lättare att skriva större program genom att det låter programmeraren tänka i termer som logiska opera-t­ioner i stället för att bara använda programmeringsspråkets inbyggda standard­programsatser.

 


Dataabstraktion.

 

En annan sorts abstraktion är dataabstraktion, vilket friställer programmeraren från uppgiften att hålla reda på detaljer om variabler och datatyper. En dator arbetar t.ex. alltid med binära tal, vilka klumpas ihop till bytes, word och long­words. Alla dessa typer, utom de binära, representeras i assembler som hexa­decimala tal. (Det finns dock assemblatorer som har bättre hantering.) I C har man tillgång till såväl heltal som flyttal, vilket är betydligt lättare att arbeta med än hexadecimala tal (om man inte är en dator). Assembler och C har inte några programinstruktioner för texthantering, medan Basic har en speciell datatyp för just detta ändamål. Jämför nedanstående operation, där man skapar en text­sträng, och sedan förlänger den, så som den ser ut i C respektive Basic:

 

// C

char cNamn[31] = ”Karl”;

strcat(cNamn, ” Svensson”);

 

REM Basic

Namn$ = ”Karl”

Namn$ = Namn$ + ” Svensson”

 

Tack vare att vi utnyttjat procedurabstraktion i C-exemplet blev det inte mer skrivarbete än i Basicexemplet, men dataabstraktionen har givit oss en associa­tivt tydligare syntax i Basicexemplet. Som synes behövde vi heller inte deklare­ra variabeln i Basic, vilket i och för sig också utgör en dataabstraktion, men det är inte säkert att den gör programmeringen tydligare för det. Generellt sett arbe­tar man med variabler i en mer teknisk synvinkel i C, och mer intuitivt i Basic.

 

Dataabstraktion medför alltid en viss grad av procedurabstraktion. När man ar­betar med variabler utan att veta exakt hur de representeras i datorn, sköter pro­gramspråket om detaljerna kring lagring och bearbetning. Detta utgör i sig en procedurabstraktion. Det skulle t.ex. vara mycket komplicerat att utföra mate­matiska operationer på flyttal om man var helt hänvisad till att arbeta med bin­ära eller hexadecimala värden. Detta slipper man i C.

 

De flesta programmeringsspråk har mycket mindre möjlighter att utöka graden av dataabstraktion än att utöka graden av procedurabstraktion. I C kan man an­vända strukturer och typdefinitioner för att skapa nya datatyper vilka har en viss grad av abstraktion. Vi kan t.ex. via en struktur skapa flera sammanhörande fält vid en enda deklaration. Personuppgifter kan grupperas:


struct person {

                  char cNamn[31];

                  char cAdress[31];

                  char cPostNummer[7];

                  char cOrt[15];

                  int  iAlder;

              };

 

Denna nya datatyp, ‘struct person’ kan användas för att skapa alla fem variabel­deklarationer i en enda deklarationssats:

 

struct person Medlem[100];

 

...varvid vi har tillgång till 100 stycken cNamn, 100 stycken cAdress etc.

 

Datatypen heter ‘struct person’, men även det namnet kan göras mer abstrakt m.h.a. C-instruktionen ‘typedef’:

 

typedef struct person PERSON;

 

...varvid vi har en ny datatyp ‘PERSON’ med samma egenskaper. Vi kan nu i stället deklarera våra medlemmar på följande sätt:

 

PERSON Medlem[100];

 

...med exakt samma resultat. I detta fall slipper vi t.o.m. att tänka på nyckelordet ‘struct’. Detta utnyttjas t.ex. när man ska arbeta med filer. Stdio.h innehåller en struktur med tekniskt intressanta variabler som används när man pratar med dos om den fil man arbetar med. Strukturen heter ‘struct _iobuf’, och vi är egent­li­g­en inte intresserade av vad den innehåller, eftersom stdio.h innehåller alla funk­tioner vi behöver för att manipulera data i strukturen, och därmed behöver vi in­te förstå hur den används. Vi behöver bara deklarera den, och för att vi ska slip­pa det tekniska namnet på strukturen finns det en typedef som deklarerar nam­n­et ‘FILE’ åt oss, och det är mycket tydligare att se i ett program. Så här ser dek­larationen ut (läggmärke till att typedef ‘vävts in’ i strukturdeklarationen):

 

typedef struct _iobuf {

                          char __far *ptr;

                          int _cnt;

                          char __far *_base;

                          char _flag;

                          char _file;

                      } FILE;

 

Nu kan vi i vårt program lätt deklarera det vi behöver för att kommunicera med dos:

 

FILE *InFil;

 

...vilket är betydligt mer abstrakt än strukturdefinitionen ovan. Nu kan man ska­pa en FILE-pekare utan att veta hur den egentligen fungerar. Procedur­abstrak­tionen vi får via de funktioner som använder pekaren, t.ex. fopen(), fgetc() etc. innebär att vi inte behöver veta något alls om hur den fungerar.

 

Observera att det går att skapa en struktur utan att skapa funktioner som använ­der dem. På så sätt kan man betrakta procedurabstraktion och dataabstraktion som två separata tekniker, trots att de tekniskt sett är två integrerade tekniker.

 

Klasser.

 

Det är här som objektorienterad programmering kommer in i bilden. Objekt­ori­enterade språk kombinerar procedurabstraktion med dataabstraktion i form av klasser. När man deklarerar en klass definierar man allt om ett objekt av hög­re nivå samtidigt, såväl data som procedurer. När man använder ett objekt som byg­ger på klassen kan man bortse från detaljer om inbyggda datatyper och operationer på dessa.

 

Betänk en enkel klass, som kan användas som mall för att skapa objekt, vilka representerar polygonala figurer. Man kanske tänker sig ett polygon som ett antal punkter representerade av talpar, punkternas koordinater. Ett polygon är dock mycket mer än ett antal punkter. Det har bl.a. storlek och form. Man skul­le kanske vilja flytta det eller rotera det. Har man två polygon kanske man vill se om de skär varandra, beräkna union och snitt, eller jämföra deras form med varandra etc. Alla dessa egenskaper och operationer är betydelsefulla utan att man för den skull har behov av att i detalj studera hur det skulle utföras. Man kan tänka sig ett polygon utan att för den skull tänka på koordinater och hur de operationer som kan utföras på dem går till.

 

Genom att kombinera strukturabstraktion med dataabstraktion kan objekt­orien­terade programmeringsspråk skapa en extra nivå mellan datorn och program­met. De högnivåobjekt man skapar har samma fördelar som flyttal och t.ex. printf() har över t.ex MOV-instruktioner i assembler, de gör det lättare att skriva långa, komplexa program.

 

Klasser kan också representera objekt som man normalt sett kanske inte skulle kalla datatyper. En klass kan t.ex. representera ett binärträd eller en länkad lista. Varje objekt behöver inte bara vara ett enkelt ‘löv’ på trädet, såsom det blir när man använder strukturer, varje objekt kan vara själva binärträdet, eller den länk­ade listan. Det är lika lätt att skapa flera binärträd och länkade listor som att ska­pa en enda. Det är bara att deklarera den med dess klass som modell, som da­ta­typ. Observera att man inte alls behöver tänka på detaljerna kring dess kon­struk­tion. Vilka egenskaper hos ett binärträd eller en länkad lista är man egent­ligen intresserad av? Möjligheten att snabbt hitta ett ‘löv’, att lägga till eller ta bort ‘löv’ etc. (‘Löv’ kan alltså vara vad som helst som kan lagras i ett binärträd eller en länkad lista.) Man är egentligen inte intresserad av binärträdets eller den länkade listans struktur, så länge som man har den funktionalitet objektet för­väntas förse en med. Datat kan lagras som en äkta länkad lista, binärträd, vanlig vektor eller någon ny metod som vi inte känner till, det berör inte oss så länge vi kan använda objektet så som det är tänkt.

 

En dylik klass borde inte heta t.ex. ‘CBinaerTraed’ eller ‘CLaenkadLista’ efter­som dessa namn beskriver klassens interna metoder. Namn som i stället beskri­v­er dess egenskaper utåt, som t.ex. ‘CSorteradLista’ eller ‘CObjektLista’ passar bättre.

 

Genom att använda abstrakta objekt i ett program i stället för att använda pro­gram­meringsspråkets inbyggda datatyper och instruktioner blir det oberoende av de detaljer som möjliggör bearbetningen. Detta leder till nästa fördel och grund­sten hos objektorienterad programmering, nämligen inkapsling (encapsu­lation).


Inkapsling.

 

Att gömma den interna bearbetningen och de interna variablerna i en klass i syf­te att möjliggöra eller framtvinga abstraktion kallas ‘inkapsling’ (encapsu­lati­on). Detta kräver att man drar en tydlig linje mellan klassens egenskaper utåt (inter­face) och dess interna bearbetning och data (implementation). De först­nämnda är tillgängliga för den programmerare som använder ett objekt (public) de sistnämnda är tillgängliga endast för den programmerare som skriver klassen (private). Klassens egenskaper utåt beskriver vad man kan göra med ett objekt, medan dess interna bearbetning beskriver hur den gör det. Denna åtskillnad möj­liggör abstraktion genom att utåt uppvisa endast de egenskaper som är rele­v­anta när man använder ett objekt baserat på klassen. Den interna bearbetningen och de interna variablerna har alltså kapslas in i klassen. Den programmerare som använder klassen ser alltså en abstrakt modell av dess bearbetning i stället för själva bearbetningen.

 

Ska inkapslingen göra skäl för namnet krävs att det data och de operationer som ska betraktas som inkapslade verkligen är deklarerade som privata medlemmar, eller skyddade dito (protected). Det förekommer att man ‘tummar’ på denna de­finition, men här diskuterar vi endast äkta vara.

 

Inkapsling förekommer inte bara i objektorienterad programmering. Man göm­mer data även i funktioner och procedurer när man deklarerar lokala variabler. En del språk stöder även lokalt deklarerade funktioner (funktioner i funktioner), dock inte den kompilator vi använder här. Man brukar dela upp ett program i flera funktioner, s.k. procedurorienterad programmering, där varje procedur har ansvar för en viss bearbetning. Dessa procedurer har ett väldefinierat gränssnitt utåt, och man strävar efter att göra dem så självständiga som möjligt. Idealfallet är när modulerna inte har minsta kännedom om varandras interna struktur, utan använder endast gränssnittet för att kommunicera med varandra. Man strävar att minimera användning av globala variabler och datastrukturer i syfte att förhind­ra modulerna att påverka varandras interna arbete.

 

Att gömma data har sina fördelar. En av dem är som vi sett tidigare att man höj­er abstraktionsnivån. En annan är att lokala förändringar i en modul eller klass inte föranleder att man måste införa ändringar på andra ställen i programmet. Ett program som inte har tydliga gränser mellan modulerna är känsligt för för­ändringar. Ändrar man på ett ställe kan detta göra att andra delar av programmet eller systemet upphör att fungera, och att man måste rätta och testa även dessa, vilket i sin tur kan skapa problem på ytterligare andra ställen etc. Ett program med ‘täta skott’ mellan modulerna är däremot lättare att rätta och underhålla. Ändringar har genomslag inom en begränsad modul, och påverkar inte övrig bearbetning. Om vi t.ex. använder en sorterad lista i vårt program, och den innehåller en länkad lista, så behöver vi inte ändra vårt program alls om man ändrar den sorterade listan till att använda ett binärträd i stället. Vårt program uppträder exakt likadant som tidigare, bortsett från att binärsökningen kanske blir snabbare än den länkade listans sökning, vilket ju inte har med program­mets funktionalitet att göra.

 

Om man använder en modul (procedur eller funktion tillsammans med t.ex. en datastruktur) som hanterar t.ex. en lista med datauppgifter har man skaffat sig en begränsning. Man kan bara ha en lista. Detta går att lösa om man deklarerar datastrukturen lokalt, och ändrar proceduren eller funktionen till att använda pekare mot listan i stället. Härigenom blir naturligvis vår lista betydligt mer an­vändbar. Denna vinst kostar dock en hel del extra arbete, tillsammans med att man måste ändra alla program som använder listan. Utan att behöva gå närmare in på alla konsekvenser av detta genomförande kan vi lungt konstatera att det blir mycket lättare att genomföra i C++. Här deklarerar vi bara en klass. Sedan kan vi skapa så många objekt vi vill, såväl lokala som globala. Varje objekt ska­pas och förstörs vid rätt tillfälle, och vi behöver t.ex. inte tänka på att frigöra re­serverat minne innan vi lämnar en funktion eftersom lokala objekt automa­tiskt kör sin destruktor när funktionen går ur sitt sammanhang.

 

Utan att höja några pekfingrar eller starta en diskussion om rättvisan i att en pro­gram­merare får förtroendet att ‘pilla’ på vissa variabler medan en annan inte får det, ska vi ändå diskutera eventuella effekter av ‘obehörigt in­trång’ i data­strukturer. Låt oss tänka oss att en programmerare använder en objektlista som någon annan har skrivit funktionerna till. Vår programmerare råkar känna till att objektlistan använder en vanlig vektor, d.v.s. en lista där alla element ligger i en lång rad i minnet. När man begär ett element ur listan gör man det via en funk­tion som returnerar adressen till objektet. Programmeraren ‘vet’ nu storleken på den struktur som representerar ett objekt i listan och vill nu ha ett objekt som lig­ger x antal steg längre fram i listan. För att slippa anropa någon funktion som hämtar nästa post x gånger tar han genvägen att öka pekarens värde med x och sedan hämta det objekt som ligger där. Det fungerar ju bra, och verkar oskyl­digt. Nu är det bara det att han även bundit upp den andre program­mer­ar­ens möj­ligheter att ändra i sin modul. Skulle han t.ex. vilja förbättra modulen gen­om att använda ett binärträd, kommer ‘vår’ programmerares program fort­fa­rande att hop­pa x poster fram i minnet, och där ligger det något helt annat. Kon­sekvens­er­na kan bli vad som helst.

 

Det är inte ovanligt att man söker sina egna genvägar, speciellt som det ibland kan vara tidskrävande att hitta, förstå och implementera de legala vägarna att manipulera datat. Är det bråttom att få programmet klart, och det är det ju alltid, så kan den mest nitiske programmeraren börja snegla på kända genvägar.

 

Om ovannämnda två programmerare dessutom är en och samma person, kan han snabbt ta beslutet att införa förbättringarna senare. Dock måste han då gå igenom alla program där han inte är helt säker på att han inte ‘fuskat’. Detta blir synnerligen tidskrävande.

 

Detta problem kan helt elimineras när man använder klasser, tack vare inkaps­lingen. Även om man inte kan garantera att klassens gränssnitt kommer att vara likadant för all framtid, kan man i de flesta fall införa förändringar i form av til­l­ägg till gränssnittets komponenter i stället för att ändra i de befintliga. Däri­gen­om kan de gamla programmen fortsätta att använda de gamla kompon­en­ter­na medan de nya utnyttjar de nya finesserna.

 

Observera att inkapslingen inte helt garanterar dataintegriteten, vilket hela tiden påståtts i detta häfte. Avsikten är att förhindra oavsiktligt intrång. Man kan all­tid använda pekare för att komma åt vad som helst i datorns minne. Det är där­för bättre om de delar som kapslats in inte blir kända för dem som inte har med dem att göra. Man dokumenterar sina klassers gränssnitt när man ‘levererar’ dem till någon, medan man behåller dokumentationen som beskriver deras in­terna arbete ‘hemma’.


Klasshierarkier.

 

En finess med objektorienterade programmeringsspråk vilken helt saknas i alla procedurorienterade språk är möjligheten att definiera klasshierarkier. I C++ kan man ju definiera en klass som ärver av en annan klass. Datatyperna i C har däremot inget gemensamt, de är helt oberoende av varandra.

 

Att deklarera en gemensam basklass för flera andra klasser kan betraktas som en form av abstraktion. Basklassen blir ett sorts högnivåbegrepp för de andra klasserna. Den beskriver vad de andra klasserna har gemensamt så att man kan koncentrera sig på endast det, och bortse från de detaljer som skiljer objekten. Denna abstraktion går alltså ut på att minska antalet typer genom att generalis­era dem, vilket man gör genom att endast betrakta det som de annars olika typ­erna har gemensamt. Det underlättar att tänka på t.ex. en personallista, som i övningsexemplet ‘firma’ än en lista med alla chefer, alla jobbare samt alla sälj­are. Vi vill ju i en sådan lista bara ha namn och anställningsnummer, och de ligger i basklassen.

 

En basklass är alltså en generalisering av en grupp klasser, medan en ärvande klass är en specialisering av en förälderklass och/eller en basklass. En ärvande klass identifierar en känd klasstyp och beskriver sina egna tilläggsegenskaper. Om vi åter tänker på exemplet ‘firma’ kan vi konstatera att en jobbare är en per­son, men har egenskaper som inte beskrivs i klassen CPerson, och därmed inte nödvändigtvis finns för alla anställda personer.

 

Man vinner två praktiska fördelar när man deklarerar en klasshierarki. Den ärv­ande klassen kan antingen använda förälderklassens/basklassens operationer, el­ler använda förälderklassens/basklassens gränssnitt. Dessa två fördelar utesluter visserligen inte varandra, men vanligtvis bygger man sin hierarki antingen för att använda operationerna, eller för att använda gränssnittet.

 

Ärva operationer.

 

Om man skriver en klass och vill ha med funktionaliteten från en existerande klass kan man helt enkelt ärva från den existerande klass som innehåller den funk­tionalitet man är intresserad av. Det gjorde vi t.ex. när vi ärvde från CJob­bare till CSaljare i syfte att beräkna timlönen m.h.a. en funktion i CJobbare, var­efter provisionen beräknades i en funktion i CSaljare. Man kan på detta sätt spara onödigt upprepande av funktioner som utför samma sak, vilket man bör tänka på när man formger sin klasshierarki. Operationer som t.ex. är gemensam­ma för alla klasser skrivs en gån för alla i basklassen. Det blir därigenom lättare att införa rättningar, ändringar och tillägg i den funktionaliteten eftersom man bara behöver ändra på ett enda ställe.

 

Om man t.ex. skapar ett eller flera formulär där användaren ska fylla i olika typ­er av data i olika fält, kan man t.ex. skapa en klass för varje typ av datafält som motsvarar den typ av data fältet är avsett för, d.v.s. en klass för ett datafält av­sett för namn, t.ex. CNamnFalt, en klass för ett datafält avsett för datum, t.ex. CDatumFalt, en klass för ett datafält avsett för valuta, t.ex. CKronOrenFalt etc. Man kommer dock ganska snart att se hur vissa grundläg­gan­de funktioner be­hövs i alla dessa klasser, t.ex. redigering, markörhantering etc. Då vore det en klar fördel att först deklarera en klass CFalt, vilken inne­hål­ler dessa funktioner. De andra klasserna kan ärva av CFalt, och lägga till sina specialfunktioner, t.ex. indatamask, gränsvärdeskontroller, automatisk redigering etc.

 

En klasshierarki avsedd för återanvändning av funktioner har de flesta funktion­er i basklass och förälderklasser. Funktionerna ligger så nära hierarkiträdets rot som möjligt.

 

Ärva gränssnitt.

 

En anna strategi är att ärva gränssnittet i stället för funktionerna. Då ärver man endast funktionernas namn, inte koden. Basklassen innehåller då funktioner av­sedda att bytas ut, ofta virtuella sådana. Den ärvande klassen innehåller sedan den kod som den behöver. På så sätt får den ärvande klassen samma gränssnitt som basklassen/förläderklassen, men har olika funktionskod. De olika klasserna gör olika saker med samma funktioner.

 

På detta sätt har vi likhet på hög nivå. Den viktigaste fördelen är dock polymor­f­ismen. Alla våra olika personer i projektet ‘firma’ kunde behandlas likadant med avseende på t.ex. löneutbetalning. Vår lista refererade till objekten med en pekare av basklassens typ, men när vi via pekaren anropade löneutbetalnings­funktionen reagerade objekten olika beroende på deras egentliga typ. Anropet var dock identiskt för alla objekt. Vi har vunnit en likhet i objektens gränssnitt för en funktionalitet som skiljer sig mellan de olika klasserna.

 

Exemplet med inmatningsfälten ovan kunde t.ex. ha en virtuell funktion Get­Va­l­ue() som implementeras olika i de olika objekten trots att alla fält anropas på samma sätt. Man kan därigenom lätt implementera en funktionalitet där använd­aren kan ‘hoppa’ fram och tillbaka mellan olika fält medan programmet hela ti­d­en anropar GetValue() på samma sätt via samma pekare, trots att de olika obj­ek­ten sedan genomför arbetet på olika sätt. Själva formuläret behöver då endast administrera de olika inmatningsfälten som en lista med objekt, i vilka GetVa­lue() anropas för aktuellt objekt. Objekten behandlas då som CFalt-objekt, oav­sett vad de egentligen har för typ. CNamnFalt, CDatumFalt eller CKronOren­Falt t.ex. behandlas alla som CFalt.

 

Vårt exempel med formuläret ovan använder liksom exemplet ‘firma’ båda meto­derna, arv av funktioner och arv av gränssnitt. Man kan dock deklarera en basklass för endast arv av gränssnitt genom att deklarera den som en abstrakt basklass. De ärvande klasserna blir då som en fungerande version av den ab­strakta modell som definierats i basklassen.

 

Vi kan alltså konstatera att klasser understöder abstraktion, inkapsling och hier­arkier. Klasserna är det verktyg vi använder för att definiera en abstrakt datatyp, vilken även innehåller de operationer man kan utföra på ett objekt av datatypen. Klasser kan inkapslas, vilket delar upp programmet i avdelningar och ökar dess ‘lokalitet’, d.v.s. att ingående variabler och operationer är så isolerade att de kan ändras utan att påverka andra delar av programmet. Till sist ser vi även att klas­ser kan organiseras i hierarkier, vilket definierar deras inbördes förhållanden och minimerar upprepning av kod.

 

Det räcker alltså inte bara med att använda programmeringsspråket C++ för att det ska bli ett objektorienterat program. Man måste även utnyttja de ovan dis­kuterade möjligheterna i språket. I nästa avsnitt ska vi se vad man bör tänka på när man skapar ett äkta objektorienterat system.


Konsten att arbeta objektorienterat.

 

När man förr programmerade enligt den linjära metoden, ‘top-down’, började man med att specificera programmets avsedda funktion. Man började med att ställa sig frågan om vad programmet skulle utföra.

 

Sedan kom det procedurorienterade tänkandet där man delade upp denna funk­tion i mindre bearbetningssteg. Dessa beskrev man ofta med någon sorts pseu­do­kod på en högre nivå. Processen fortsatte genom att man bröt ned dessa i allt mindre procedurer till man kom ned i nivå med de programinstruktioner pro­gram­meringsspråket tillhandahöll. Denna metod kallas procedurorienterad. Den behandlar ett program som en beskrivning av en process vilken bryts ned i un­der­processer.

 

Objektorienterad design skiljer sig totalt från denna metod genom att man först struntar i vad programmet ska göra. Man intresserar sig inte ens för det data pro­grammet ska bearbeta. Man börjar i stället med att analysera problemet med utgångspunkt från att det är ett system bestående av olika typer av objekt, och hur dessa sammarbetar och är beroende av varandra. Den första frågan blir allt­så: ”Vad har vi för objekt här?” eller ”Vilka aktiva komponenter ingår i detta program?” Sedan fortsätter man med att fråga sig vilka relationer som gäller mellan dem, och hur de samarbetar.

 

Skillnaden är inte bara den totalt annorlunda utgångspunkten, man fortsätter också på ett helt annat sätt. Man börjar inte med en stor klass som sedan bryts ned i mindre klasser. Vid objektorienterad design brukar man arbeta på såväl hög som låg abstraktionsnivå samtidigt, genom hela projektet. Arbetet invol­verar följande punkter, vilka alltså inte är att betrakta som separata steg i ut­vecklingen:

 

·     Identifiera vilka objekt som ska beskrivas, och därmed vilka klasser som behövs.

·     Tilldela klasserna egenskaper i form av attribut och metoder.

·     Identifiera objektens relation till varandra.

·     Arrangera klasserna i en eller flera hierarkier.

 

Man brukar börja projektet med att utföra ovanstående punkter ett steg i taget, men man slutför dem inte fullt ut. Arbetet utgör i själva verket en iterativ pro­cess där man hela tiden återkommer till de olika punkterna för att arbeta vidare på dem, och de är alltså aktuella hela projekttiden igenom. Om man skulle för­söka göra färdigt ett steg innan man tittade på de övriga skulle man inte ha stora chanser att producera en vettig klasshierarki. Man behöver helt enkelt ‘input’ från arbetet med de övriga punkterna. Första genomgången blir alltså endast en ‘grovskiss’. Det är dock viktigt redan där att man verkligen gör ett grundligt ar­bete om det fortsatta iterativa arbetet inte ska innehålla för många obehagliga öv­er­raskningar. Vi ska här nedan titta litet närmre på varje punkt för sig:

 

Identifiera vilka objekt som ska beskrivas, och därmed vilka klasser som behövs.

 

Den första punkten innefattar att man försöker bestämma vilka objekt program­met ska representera, och vilka klasser man behöver för att representera dem. Detta är inte lika lätt som det var att bestämma programmets funktion, vilken man ju gjorde tidigare. Det går inte att helt enkelt bryta ner problemet i proce­durer, och definiera de funktioner och datastrukturer som behövs, för att till sist bygga klasserna på dem. Gör man detta har man inte arbetat objektorienterat, ef­tersom man återigen utgår från problemet, inte objekten. Klasserna måste vara de centrala enheterna i programmet.

 

En metod att skapa en lista med de klasser som behövs är att skriva en beskriv­ning av syftet med programmet. Sedan stryker man under alla substantiv, och för in dem i listan. Detta blir den första versionen av lista med ingående klasser. Denna enkla metod är helt beroende av hur väl beskrivningen utformats, men kan ibland vara den rätta metoden, eller åtminstone en bra utgångspunkt, spec­i­ellt för den som är ny på objektorienterat tänkande.

 

Det är lättare att identifiera klasserna om de ingående objekten är av konkret na­tur. Om man t.ex. vill skapa ett program för platsreservationer på ett flyg­bo­lag, skulle man lätt identifiera objekten flygplan och passagerare, och dek­l­ar­era klasserna ‘CFlygplan’ och ‘CPassagerare’. Om man skapar ett operativ­sys­tem vill man säkert skapa klasser för olika enheter: diskar, skrivare etc.

 

Många program arbetar dock med mera abstrakta objekt, och då kan dessa vara svår­are att identifiera. En kompilator kan t.ex. behöva en klass för syntaxkon­troll. Denna kontroll kanske sker hierarkiskt och kan då heta t.ex. ‘CSyntax­Traed’. Ett operativsystem kan behöva en klass för att instansiera varje process som ett objekt: ‘CProcess’. Dessa klasserna kan vara både svåra att identifiera och att beskriva attribut och funktionalitet för.

 

Ännu mindre uppenbara klasser är sådana som t.ex. hanterar händelser i t.ex. ett händelsestyrt system. Man kan ha en klass ‘CTransaktion’ i ett banksystem, vil­ket kan representera att någon tar ett lån, gör en insättning eller någon annan van­lig transaktion i en bank. Transaktionen i sig själv kan sedan initiera andra bearbetningar. En klass ‘CKommando’ kan hantera användarens interaktion i ett operativsystem etc.

 

Ibland kan man se möjligheten till en hierarki för sina klasser. Om man funnit att man behöver en klass ‘CBinaerFil’ och en klass ‘CTextFil’ kanske man kan tänkas ärva dessa från en klass ‘CFil’. Det är dock inte alltid uppenbart när det är lämpligt med sådant arv. Ett banksystem kan t.ex. tänkas använda en enda klass ‘CTransaktion’ för alla händelser, eller skapa separata klasser ‘CLaan’, ‘CInsaettning’ etc. och låta dessa ärva från klassen ‘CTransaktion’. Liksom klasserna är sådana hierarkier föremål för eventuella framtida förändringar el­ler för att helt och hållet förkastas.

 

Meningen med alla ovanstående klasser är att de ska stå som modell för olika element i det problem man ska lösa. Programmet kan behöva en annan typ av klasser, nämligen klasser som implementerar de klasser vi redan identifierat. Det kan vara t.ex. en klass som administrerar en lista med de objket vi skapar från en av våra klasser. ‘CSorteradLista’ vi diskuterade förut kan t.ex. användas för att lagra objekt skapade från de olika klasserna. Vi skulle kunna skapa en lånelista för CInsaettning-objekt, en passagerarlista för CPassagerare-objekt etc. Vi ser nu att det finns ett behov, men själva listan var så abstrakt att vi kanske inte tänkte på den när vi identifierade objektet ‘passagerare’. När man går ige­n­om ovanstående punkter för första gången är det i regel för tidigt att tänka på den­na typ av relation, med undantag för de fall då man använder i förväg dekla­rerade standardklasser.

 

Tilldela klasserna egenskaper i form av attribut och metoder.

 

När man väl identifierat en klass blir nästa uppgift att avgöra dess ansvarsom­råde. De olika ansvarsområdena kan indelas i två kategorier:

 

1     Den information ett objekt baserat på denna klassen måste innehålla. (Vad ‘vet’ ett objekt som baseras på denna klass?)

2     De operationer ett objekt baserat på denna klassen kan utföra eller som kan utföras på objektet. (Vad kan detta objekt göra?)

 

Varje klass har ‘attribut’ vilka är de egenskaper eller karakteristika som besk­river den. Klassen CRektangel har t.ex. bredd och höjd, en bild har ett bildin­nehåll, en CPerson har namn och anställningsnummer etc. för att ta bl.a. från några tidigare exempel. Varje objekt baserat på klassen måste själv ‘minnas’ sitt innehåll. Objektets ‘inställning’ är de värden dess medlemsvariabler innehåller i ett visst ögonblick. Ett CFile-objekt kan t.ex. innehålla filnamnet ‘minfil.txt’, till­gänglighet ‘read-only’ och positionen ‘43 bytes från filens början’. En del att­ribut kanske inte ändrar värde under objektets levnad, medan andra ständigt upp­dateras. Ett attribut kan antingen vara lagrat i en medlemsvariabel eller be-räk­nas från andra medlemsvariabler etc. när det behövs.

 

Blanda inte ihop attribut och klasser. Det är meningslöst att definiera en klass för att beskriva ett enda attribut. En ‘CRektangel’ kan vara användbar, men klas­serna ‘CLangd’ och ‘CBredd’ är antagligen onödiga. Om en klass’ ‘CForm’ enda uppgift är att hålla reda på vilken muspekare som ska vara synlig är det bät­tre att använda ett attribut för detta i den klass som hanterar fönstret. Om den däremot innehåller en bitmap som beskriver muspekarens utseende, och even­tu­ellt diverse operationer som kan utföras på den kan det vara vettigt att deklarera den som en klass, men då vore det bättre att kalla den ‘CMusPekareBild’ eller ‘CGraphCursor’, eftersom ‘CForm’ kan vara ett namn man behöver till andra former i ett program.

 

Varje klass ‘beter’ sig på ett visst sätt. Detta beskriver hur de objekt som bas­eras på klassen samverkar med andra av objekt och hur dess ‘tillstånd’ ändras under denna samverkan. Klasser kan bete sig på många olika sätt. Ett CTime-objekt kan t.ex. visa nuvarande tillstånd, tiden den innehåller, utan att ändra det. En an­vändare kan hämta eller lagra värden i en klass CStack, varvid dess interna till­stånd förändras. Ett polygon kan delvis sammanfalla med ett annat varvid ett tred­je polygon uppstår, d.v.s. det skapas ett nytt objekt.

 

När man bestämmer vad en klass bör ‘veta’ och vad den bör kunna göra måste man betrakta den i sammanhang med programmet den ska användas i. Vilken roll spelar klassen? Programmet har i sig ansvar för diverse information och op­erationer, och dessa bör tilldelas olika klasser. Om det finns information eller op­erationer som ingen klass hanterar behövs det antagligen en ny klass. Det är ock­så viktigt att programmets uppgifter fördelas någorlunda lika mellan de oli­ka klasserna. Om en enda klass handhar lejonparten av ett programs uppgifter bör man antagligen dela upp den i mindre klasser. Omvänt gäller naturligtvis för en klass som inte gör någon egentlig nytta, den bör man nog ta bort.

 

När man håller på att tilldela en klass dess attribut och metoder ger det ofta en djupare insikt i vad som utgör en användbar klass. Om det är svårt att definiera en klass’ ansvarsområde representerar den antagligen inte en tillräckligt väl def­inierad del av programmet. Många av de klasser man tänkte sig i det första steg­et, när man första gången gick igenom den ovanstående listan, kanske kasseras på det här stadiet. Om vissa attribut och/eller metoder upprepas i flera klasser kan det hända att de representerar en användbar abstraktion man inte lagt märke till tidigare. Man kanske ska skapa en ny klass med bara dessa attribut och/eller metoder för att sedan använda den i de andra klasserna, genom arv eller kompo­sition.

 

De programmerare som är ovana vid obejktorienterad programmering begår of­ta misstaget att skapa klasser som egentligen bara är inkapsling av processer. I stället för att klasserna representerar objekt kommer de att representera ett block med funktioner i ett procedurorienterat program. Dessa ‘falska’ klasser kan lätt identifieras på detta stadium helt enkelt genom att de saknar attribut, medlems­funktioner. En sådan klass har inget ‘tillstånd’, den innehåller inget data, bara funktionalitet. Om, när man definierar en klass ansvarsområde skulle be­skriva den på samma sätt som man beskriver en funktion, så är det också en funk­tion, ingen klass. Man tänker kanske att ‘Denna klass tar emot ett argument och svarar med...’ och då beskriver man en funktion. En klass som bara har en metod, medlemsfunktion, är inte heller en klass.

 

När man väl definierat en klass attribut och dess beteende ser man ungefär vilka metoder som behövs i klassens gränssnitt. Dess beteende motsvarar vanligen dess medlemsfunktioner. Vissa attribut kräver metoder i gränssnittet för att läsa av eller ändra dem. Andra attribut märks endast på klassens beteende.

 

Exakt vilka metoder som behövs,  vilka argument de tar och vilken returtyp de har blir inte helt bestämt förrän i slutet av utvecklingsarbetet. Dessutom koncen­trerar man sig inte så mycket på klassernas användning i detta stadium. Det hand­lar t.ex. om att bestämma om ett attribut ska lagras i en medlems-variabel eller om det ska beräknas när det behövs, hur data ska representeras och hur man ska skriva metoderna.

 

Identifiera objektens relation till varandra.

 

Därefter är det dags att bestämma hur klasserna ska använda sig av varandras fär­digheter. En del klasser kan klara sig på egen hand, medan de flesta är bero­ende av varandra. Klasser bygger på varandra respektive samarbetar med var­andra.

 

Många klasser är helt beroende av andra klasser, och kan inte alls användas om de andra klasserna inte finns tillgängliga. Om vi tänker oss en klass ‘CTid’, som har till ansvar att lagra klockslag och datum, så kan den innehålla en metod som skapar en textsträng där datum och klockslag formaterats snyggt, så att det kan skrivas ut på skärmen. Om man då använder ett CText-objekt för detta än­damål, är man beroende av att den klassen finns tillhands. Klassen CTid kan då t.ex. in­nehålla en metod som tar en referens till CText-objekt som argument  och anro­p­ar dess medlemmar efter behov, så att anropande klass/funktion får sitt objekt uppdaterat.

 

En klass kan även ha andra klasser inbäddade, vilket innebär att klassen har ob­jekt av andra klasser som medlemsvariabler. En klass CCirkel kan t.ex. ha dels en medlemsvariabel som representerar cirkelns radie, dels använda ett objekt typ t.ex. ‘CKoordinat’ för att ta hand om cirkelns medelpunkt. Denna typ av rel­ation kallas ‘containing relationship’, d.v.s. ett förhållande där en klass inne­hål­ler objekt av en annan klass. Detta kallas för att ‘komponera’ ihop klasserna, ‘composition’, vilket är en helt annan sak än att utnyttja klass­arv.

 

De flesta förhållanden mellan klasser uppstår genom att en klass gränssnitt är beroen­de av en annan klass gränssnitt. Om man t.ex. i klassen CCirkel ovan vill imple­m­entera en metod ‘Medelpunkt()’ i gränssnittet, vilken returnerar ett CKo­or­di­n­at-objekt, måste den som använder klassen CCirkel även ha tillgång till klas­sen CKoordinat för att kunna använda CCirkels gränssnitt. Det kan också vara så att en klass metoder är beroende av en annan klass utan att det involver­ar gräns­snittet. Ta t.ex. en klass CTeleLista, som innehåller en privat medlem för själva listan. Det kan vara t.ex. ett objekt av klassen CSorteradLista. I detta fall behöver den som använder CTeleLista inte ha tillgång till CSorteradLista, i motsats till fallet med CCirkel och CKoordinat. Detta utgör i sig ytterligare en form av inkapsling, där man kan byta ut själva listan utan att det påverkar gräns­snittet.

 

Medan man definierar relationerna mellan de olika klasserna är det vanligt att man omprövar en del av de beslut som tagits tidigare. Data som lagrats i en klass, eller funktionalitet som finns i den, kan t.ex. visa sig vara mer lämpligt att placera i en annan klass. Ge inte en klass för mycket information om sin omgiv­ning. Har man t.ex. en klass ‘CBok’ och en klass ‘CBibliotek’ där man lagrar ob­jekt av typen CBok är det inte nödvändigt att klassen CBok innehåller infor­-m­a­tion om vilket bibliotek det rör sig om . Det finns i klassen CBibliotek. Genom att justera gränserna mellan de olika klasserna kan man förfina sin ursprungliga uppfattning om syftet med varje klass.

 

Man kan vara frestad att överutnyttja konceptet med vänklasser, speciellt när man i en klass behöver tillgång till innehållet i en annan, men denna mekanism bör utnyttjas sparsamt eftersom den förstör inkapslingen. Ändrar man sedan i den ena klassen kanske man blir tvungen att ändra i den andra också, och det är ju en av de saker vi vill undvika med objektorienterad programmering.

 

När man sedan funnit en klass relation till andra klasser kan man ytterligare be­s­täm­ma klassens gränssnitt. man vet vilka attribut som kräver accessfunktioner för att ändra dem, och vilka som bara behöver läsas. Man har en bättre uppfatt­ning om hur man ska dela upp klassens beteende i olika medlemsfunktioner.

 

Arrangera klasserna i en eller flera hierarkier.

 

Man skapar hierarkier som en förlängning av det första steget, där man identi­fier­ade vilka klasser som behövs, men för att kunna göra detta behövs att man gått igenom även ovanstående steg. Genom att tilldela klasser attribut och met­oder får man en tydligare bild av deras likheter och olikheter. Genom att finna hur klas­serna står i relation till varandra ser man vilka klasser som behöver imp­lementera funktionalitet från andra klasser.

 

En indikation på att det kan vara lämpligt att skapa en hierarki kan vara att man ser behovet att använda en switch på ett objekts typ. Om man t.ex. har en klass ‘CKonto’ vilken innehåller en datamedlem ‘typ’,  som talar om huruvida detta är ett lönekonto eller ett sparkonto, kan dess medlemsfunktioner behöva upp­träda olika beroende på denna datamedlem. Vi kan tänka oss att räntan räknas per månad för sparkontot, men löpande för lönekontot. Då tänker man sig kan­ske i första hand att man använder en switch på typ för att styra fram till rätt be­arbetning. Här skulle det kanske vara bättre att använda en klasshierarki med po­l­ymorfism. Vi kan från klassen CKonto ärva till klasserna ‘CLoneKonto’ res­pektive ‘CSparKonto’. Den metod som används vid ränteberäkning kan då dek­lareras som en virtuell metod i basklassen (CKonto), och implementera olika kod för motsvarande metoder i CLoneKonto och CSparKonto. Man kan sedan anropa den utan att behöva någon medlem för typ.

 

Det behöver å andra sidan inte vara nödvändigt med en hierarki bara för att man kan identifiera olika typer av objekt. Om dessa objekt inte skiljer sig med av­se­ende på attribut och metoder finns det ingen anledning att skilja dem. Ta t.ex. en klass för bilar ‘CBil’. Det kan finnas personbilar, minibussar, lastbilar etc. men man vill egentligen bara ha med vissa attribut och metoder som inte är ber­o­ende av vad det är för typ av bil. Det kanske i stället är ett av dess attribut.

 

 


Komposition kontra arv.

 

Såväl komposition som arv medför att en klass kan använda metoder från en annan klass, men de olika klasserna har inte samma relation vid komposition som vid arv. Många programmerare väljer klassarv när de vill få tillgång till en annan klass metoder utan att tänka efter om ett arv verkligen motsvarar rela­tion­en mellan de två klasserna. Komposition använder man när en klass har en ann­an klass medan arv används när en klass är ett specialfall av en annan klass. T.ex. en cirkel är inte en sorts punkt, den har en punkt, nämligen en medel­punkt. En CSaljare har inte en CJobbare, han är det.

 

Ibland kan det vara svårare att avgöra om det rätta förhållandet motsvarar ett arv eller en komposition. Är t.ex. en stack en sorts lista med en speciell upp­sättning metoder eller har den en lista. Är ett fönster en textbuffert som kan visa sig själv på skärmen eller innehåller fönstret en textbuffert? I sådana fall måste man se hur klassen passar ihop med andra klasser i projektet.

 

Om man ser ett behov av polymorfism är antagligen lösningen att använda arv. Här kan man använda basklasspekare för att kalla metoder i objekt av olika slag, vilket man inte kan när man använder komposition.

 

Vill man å andra sidan låna funktionalitet från en annan klass upprepade gånger är det möjligt att det är bättre med komposition. Om vi tänker oss en klass som sparar information om en fil, bl.a. datum då filen skapades, datum då filen sen­ast ändrades samt datum då filen senast lästes, så är det enklast att använda tre objekt av klassen CDatum.

 

Att skapa klasser avsedda för arv.

 

När man bygger sin klasshierarki är det vanligt att man upptäcker att man behö­v­er skapa nya klasser, ändra i befintliga samt att ta bort överflödiga klasser som tidigare deklarerats. De flesta klasser man identifierat i första steget är antag­lig­en sådana man tänkt använda för att skapa objekt. Det var ju genom att identi­fi­era objekten som man fann klasserna. I det vidare arbetet har det dock antag­lig­en framkommit klasser som inte är lämpliga att skapa objekt på, att instantiera, ‘instantiate’. Därför kan många av de klasser som framkommer när man skapar hierarkin vara lämpliga att deklarera som abstrakta klasser.

 

När man deklarerar abstrakta klasser ökar man möjligheterna att återanvända dem. Man kan t.ex. skapa en abstrakt klass som man låter att stort antal klasser ärva från direkt. Om två eller flera av dessa klasser har gemensamma medlem­mar som inte finns i de andra kan de dock inte implementeras i basklassen. Där­igenom måste de implementeras upprepade gånger i alla de klasser som behöver dem. För att undvika detta kan man skapa en mellanklass, vilken även den kan vara abstrakt. Låt mellanklassen ärva av basklassen, lägga till de extra medlem­marna som behövs i vissa klasser, och låt dessa klasser ärva av mellanklassen i stället för direkt från basklassen. Detta ökar även möjligheterna när man senare vill utöka hierarkin ytterligare.

 

Man bör dock inte skapa abstrakta klasser i onödan. I extremfall kan man tänka sig en större mängd klasser som ärver av varandra, och som var och en endast lägger till ett minimum av funktionalitet. Om man sedan inte verkligen behöver alla mellannivåerna kommer klasshierarkin visserligen att fungera, men det blir en klumpig hierarki.

 

Det är önskvärt att man placerar gemensam funktionalitet så högt upp i hierar­kin som möjligt, så att den kan användas överallt där den behövs. Man bör å andra sidan inte belasta en basklass med funktionalitet som endast få ärvande klasser utnyttjar. Attribut och metoder kan under utvecklingsarbetet flyttas upp eller ner i hierarkin, allteftersom man finjusterar den.

 

Arven påverkar inte bara klasshierarkins utformning. De kan även påverka ut­formningen av fristående klasser. Varje fristående klass kan senare komma att användas som basklass för andra klasser. Om det är någon annan program­mer­are som kan tänkas använda klassern som basklass är det viktigt att man varit noggrann när man bestämde vad som skulle ingå i gränssnittet och vad som skul­le vara inkapslat. En klass har egentligen två slags ‘användare’. Å ena sidan har vi klasser och funktioner som använder klassen genom att skapa ett objekt, å andra sidan har vi klasser som ärver av den. När man skapar en klass måste man bestämma huruvida man vill skapa olika sorters gränssnitt för dessa två olika användartyper. Nyckelordet ‘protected:’ möjliggör för ärvande klasser att använda medlemmar trots att de inte är tillgängliga på objekten. Därigenom kan de två olika sorters kunderna få olika tillgänglighet, men bara på så sätt att en ärvande klass har tillgång till mer än ett objekt.

 

Man får se upp så att man inte använder ‘protected:’ på ett sådant sätt att data­integriteten bryts. Om man i efterhand vill ändra från ‘protected:’ till ‘private:’ måste man ändra i alla ärvande klasser som refererade till dessa medlemmar, vilket kan bli ett omfattande arbete. Var därför försiktig med användningen av ‘protected:’.

 


Multippelt arv.

 

Multippelt arv kan vara användbart för att maximera återanvändning samtidigt som man undviker basklasser med onödig funktionalitet. Om man t.ex. har en abstrakt basklass för objekt som kan sorteras, ‘CSorterbartObjekt’, vilken har det gränssnitt en klass behöver för att kunna lagras i ett objekt av en klass för sorterad lista, låt oss kalla den ‘CSorteradLista’. Betänk sedan att det finns en liknande abstrakt basklass kallad ‘CSkrivbartObjekt’ som har ett interface användbart vid utskrifter. Man kan sedan ärva till klasser för såväl listor som  utskrifter, utan att få onödiga komponenter i gränssnittet. Vill man sedan ha en klass som har såväl listfunktionalitet som utskriftsfunktionalitet kan man ärva av både CSorterbartObjekt och CSkrivbartObjekt.

 

Detta skulle vara svårt att åstadkomma med enbart enkelt arv. För att undvika att upprepa samma funktioner i flera olika klasser skulle man deklarera en bas­klass ‘CSorterbartSkrivbartObjekt’ och låta alla underliggande klasser ärva från denna. Vissa klasser måste sedan undertrycka de funktioner som har med ut­skrift att göra och andra måste undertrycka de som handhar sortering. En sådan hierarki skulle bli ‘topptung’, d.v.s. ha för mycket funktionalitet längst upp i hier­arkin.

 

Virtuella klasser är vanliga i hierarkier med multippelt arv. En nackdel med vir­tuella basklasser, förutom den extra bearbetning som krävs, är att behovet av dem syns tydligt först när hierarkin är mer eller mindre färdig. Om vi tänker på exemplet ‘firma’ så var det ju så att vi inte behövde göra basklassen äkta vir­tu­ell förrän vi diskuterade hur en ‘CSaljChef’ skulle implementeras. Det var först då vi behövde multippelt arv. Om vi alltså skulle lägga till denna klass efter att vi skrivit ett antal program som använder de övriga klasserna blir man tvungen att modifiera hierarkin, och kompilera om alla färdiga program, och eventuellt modifiera dem. Om det blir praktiskt svårt att genomföra modifikationen kanske man måste söka en annan lösning än den med virtuella basklasser.

 

Såväl enkelt som multippelt arv används ofta på ett olämpligt sätt. Många lös­ningar som använder multippelt arv skulle de facto kunna lösas bättre m.h.a. kom­position, eller med en kombination av komposition och enkelt arv. Man bör alltid undersöka möjligheterna till en sådan lösning innan man inför multippelt arv.