Det här är ett avsnitt i en webbkurs om databaser som finns fritt tillgänglig på adressen http://www.databasteknik.se/webbkursen/. Senaste ändring: 16 juli 2005.

Av Thomas Padron-McCarthy. Copyright, alla rättigheter reserverade, osv. Skicka gärna kommentarer till webbkursen@databasteknik.se.

Vi lär oss det där med data: Grunder om objektorientering

När man ska skriva ett datorprogram kan man göra det på flera olika sätt. Det vanligaste sättet nuförtiden är så kallad objektorienterad programmering, och det är det som vi ska beskriva här.

Objektorientering används inte bara när man skriver datorprogram, utan det kan användas även i andra sammanhang, till exempel när man organiserar data som ska lagras i en databas. Objektorientering handlar egentligen inte så mycket om programmering, utan just om hur man organiserar data. Därför handlar inte heller det här avsnittet mest om programmering, utan om att organisera data.

Vad objektorientering inte är

När man skriver ett datorprogram använder man ett av de särskilda programmeringsspråk som finns. Det finns många olika programmeringsspråk, och en del av dem kallas objektorienterade. De mest kända av de objektorienterade programmeringsspråken är Java, C++, C# och Smalltalk. Ett exempel på ett programmeringsspråk som inte är objektorienterat är C.

Objektorientering är ett sätt att organisera data i enlighet med ett särskilt sätt att tänka. De objektorienterade programmeringsspråken erbjuder stöd för det sättet att tänka, men de kan förstås inte tänka åt programmeraren.

Därför blir det inte automatiskt objektorientering, eller objektorienterad programmering, bara för att man använder sig av ett objektorienterat programmeringsspråk som Java eller C++. Det är ungefär som att man inte automatiskt blir en häst bara för att man bosätter sig i ett stall.

Objektorientering är ett sätt att tänka, inte en viss typ av programmeringsspråk!

Algoritmer + data = program

Ett datorprogram brukar bestå av steg-gör-steg-anvisningar om hur datorn ska göra något, ungefär som ett recept. ("Sätt ugnen på 250 grader. Häll 1 kilo bakpulver i en skål. Rör ner 2 liter Coca-Cola i bakpulvret. Spring.") Men objektorientering handlar egentligen inte så mycket om de där steg-gör-steg-anvisningarna, utan mer om de saker som programmet arbetar med. (Bakpulvret och Coca-Colan i receptet.)

Man brukar tala om algoritmer, som är ett finare namn för steg-för-steg-beskrivningar, och data, som är det som man gör saker med enligt den där steg-för-steg-beskrivningen.

De saker som programmet arbetar med är förstås inte Coca-Cola och bakpulver, utan det är data. Det kan vara enkla data som tal (till exempel 47) och textsträngar (till exempel Sven Bengtsson). Men man kan också sätta ihop flera såna enkla data till mer komplicerade saker, till exempel en kundpost som beskriver en kund med kundnumret 47 och namnet Sven Bengtsson.

Objektorientering handlar just om de där mer komplicerade sakerna: vad de är, vad de innehåller för data, och hur man arbetar med dem

Små små bitar, så blir det lättare: modularisering!

Innan vi kommer in på riktigt på objektorientering ska vi prata om en grundtanke, som är viktig inte bara i objektorienterad programmering utan i all programmering, och inte bara i programmering utan i all teknisk verksamhet, kanske till och med i all mänsklig verksamhet överhuvudtaget, nämligen modularisering.

Efter ett halvt århundrade eller så av programmering har vi nämligen lärt oss att saker blir lättare om man delar upp dem i mindre bitar och sen gör en bit i taget.

Det är det som kallas för modularisering. När man ska skriva ett datorprogram som löser en viss uppgift, så försöker man inte skriva hela programmet på en gång, utan man delar först uppgiften i flera små deluppgifter. Sen skriver man ett litet program, eller en bit av ett program, för varje deluppgift. Till sist sätter man ihop alla de små programmen till ett stort program, och det stora programmet kan då lösa den ursprungliga, stora uppgiften.

Grejen är att det är lättare att skriva tio små program (eller programdelar), än att skriva ett enda tio gånger så stort program på en gång. Det är ungefär som att det är lättare att hoppa en meter högt tio gånger, än att hoppa tio meter högt i ett enda hopp. (Jaja, man kan nog tänka sig en del invändningar mot den liknelsen, men det är samma idé både när det gäller höjdhopp och programmering. Små program är mycket, mycket lättare att skriva än stora.)

Att gömma sina hemligheter

Om modulariseringen ska fungera bra, så måste man tänka på en sak till, nämligen att modulerna ska vara ordentligt skilda från varandra.

Om vi bygger en bil i moduler, så vill vi bygga en modul i taget och sen skruva ihop dem. Vi bygger motorn för sig, växellådan för sig, bilradion för sig, och sen, när delarna är klara, sätter vi ihop dem till en färdig bil. På det viset kan vi koncentrera oss på radion när vi bygger radion, och behöver inte fundera på hur radion och till exempel motorn påverkar varandra. Men om det skulle vara så att kugghjulen och hävstängerna inuti en modul sticker ut, utanför modulen, så kan kugghjulen och hävstängerna från en modul fastna i de kugghjul och hävstänger som hör till en annan modul! Då fungerar inte modulariseringen: trots att vi hade delat upp bilen i moduler, måste vi fortfarande ta hänsyn till kugghjulen i de andra modulerna när vi bygger kugghjulen i den här modulen!

Det är likadant med de moduler som man delar upp ett datorprogram i. Modulerna har en massa saker inuti sig: variabler, loopar, datastrukturer. Allt det där ska vara undangömt inuti modulen, så att den som arbetar med en helt annan modul inte behöver tänka på det. En del programmerare tycker att de sakerna ska vara så väl gömda att det är omöjligt att se dem, medan andra tycker att det räcker med att man i alla fall inte måste titta på dem. I vilket fall som helst: Kugghjulen ska inte sticka ut utanför modulen!

Vi kan inte kan gömma undan all information. Modulerna måste samverka med varandra. Motorn i en bil måste vrida runt nån form av stång som är kopplad till växellådan. Modulerna i ett datorprogram måste samverka, till exempel genom att skicka data till varandra. Därför måste det finnas åtminstone några få saker på "ytan" av en modul, som andra moduler kan se och använda sig av. De saker som alltså finns tillgängliga på ytan av modulen, och som andra kan se och använda, brukar kallas för modulens gränssnitt (på engelska interface).

Faktaruta: Ett gränssnitt är en skiljelinje mellan två delar i ett system, eller mellan ett system och dess omgivning, till exempel mänskliga användare. Till gränssnittet hör både var gränsen dragits och hur kommunikationen över gränsen sker. All interaktionen mellan de två delsystemen sker genom gränssnittet. Gränssnittet mellan ett tekniskt system, som ett datorsystem, och en användare kallas användargränssnitt (på engelska user interface). Användargränssnittet till en bil består av ratt, växelspak och övriga reglage, och också av hastighetsmätaren och kontrollamporna.

Att gömma de saker som finns inuti en modul, och som andra moduler inte behöver ha tillgång till, brukar kallas information hiding, eller helt enkelt att gömma information.

Klasser

Så hur delar man upp ett program i moduler? Det finns flera sätt:
  1. Ett sätt är att titta på vad programmet gör, alltså algoritmerna. Man delar upp den uppgift som programmet ska utföra i mindre steg, och de stegen blir moduler. Sen kan man dela upp stegen i ännu mindre steg, och så vidare tills stegen är så små och enkla att det blir lätt att skriva de små programsnuttar som ska utföra stegen.
  2. Ett annat sätt är att titta på de data som programmet arbetar med. Det kan vara enkla saker som tal och strängar, men också mer komplexa saker som ofta motsvarar objekt i verkligheten: personer, flygplan, kurser vid ett universitet. Varje sådan sak, som till exempel den där personen där borta, och det där flygplanet som just flög förbi, kallas ett objekt, och varje typ av sak, till exempel "person" och "flygplan", kallas en klass.
Föga förvånande är det de där objekten (och klasserna) som är grunden för objekt-orienterad programmering. Objekten är data i datorn. Varje objekt brukar motsvara en sak ute i verkligheten.

Klasserna beskriver sakerna. Där ingår vad vi vet om varje enskild sak, men också vad vi kan göra med dem. Till exempel kanske vi vet att varje person har ett namn, ett skonummer och en längd, men också att en person kan äta, sova och prata.

Faktaruta: Objektorienterad programmering är ett sätt att programmera som går ut på att man delar upp sitt program i mindre, mer lätthanterliga delar som kallas klasser. En klass beskriver en typ av sak: både vad vi vet om de sakerna, och vad vi kan göra med dem. De enskilda sakerna kallas objekt eller instanser.

Tre helt olika sätt att se på klasser

Vi har talat om klasser som om de var moduler i ett datorprogram, men det finns faktiskt tre helt olika sätt att se på en klass: Vi tänker oss personer (i verkligheten), och särskilt de tre personerna Lotta, Lisa och Kalle. Vi kan skriva ett objektorienterat datorprogram som ska lagra data om personer. Då kommer vi antagligen att skapa en klass för personer, och det är inte omöjligt att vi väljer att kalla den Person. Vi kommer också att skapa tre objekt, som blir datorprogrammets uppfattning om de tre verkliga personerna Lotta, Lisa och Kalle.

Om vi först tänker oss klassen som en mängd, blir den helt enkelt samlingen (eller mängden) av alla personer:

Klassen Person sedd som en mängd av personobjekt

Om vi i stället tänker oss klassen som en datatyp, blir den den uppsättning av egenskaper som varje objekt i klassen ska ha, till exempel att varje person har ett namn, ett skonummer och en längd, men också att en person kan äta, sova och prata. En datatyp kan man beskriva på olika sätt, till exempel genom en sån här figur:

Klassen Person sedd som en datatyp i UML-notation

Om vi slutligen, på nytt, tänker oss klassen som en modul, så blir den i stället den del av datorprogrammet där programmeraren skrivit ner vilka egenskaper som varje objekt i klassen ska ha, och hur en person gör för att äta, sova och prata. Hur den ser ut beror förstås på vilket programspråk man skriver i, men med språket Java som exempel skulle det se ut nåt i den här stilen:

class Person {
    public String namn;
    public int skonummer;
    public double längd;
    public void ät() { /* ... */ }
    public void sov() { /* ... */ }
    public void prata() { /* ... */ }
}
Jag har klippt bort den programkod som beskriver hur en person faktiskt bär sig åt för att äta, sova och prata. Den programkoden kan vara både lång och komplicerad, och är bland det viktigaste i modulen. Men den ingår inte i modulens gränssnitt, utan ska hållas undangömd inuti modulen.

Tänk på att det alltså är klassen själv som innehåller beskrivningen av hur en person äter. Det är inte någon annan del av programmet som sköter ätandet, utan det gör klassen. Om man vill kan man säga att Person-objekten själva vet hur de äter. Det räcker med att säga till ett Person-objekt att det ska äta, så gör det det.

Egenskaper, som namn, skonummer och längd i exemplet, brukar kallas attribut eller medlemsvariabler. "Verben", som exemplets ät, sov och prata, brukar kallas metoder eller medlemsfunktioner.

Objektorientering på riktigt: arv och polymorfism

Det vi skrivit ovan, om att dela upp ett datorprogram efter vilka data det arbetar med och om att gömma information, går att använda även när man inte arbetar objektorienterat. Inte ens förekomsten av klasser räcker för att åstadkomma det som man faktiskt brukar kalla objektorientering. För att det ska räknas som riktig objektorientering, och för att kunna utnyttja de fördelar som objektorientering ger, krävs två nya mekanismer: arv och polymorfism.

De mekanismerna hjälper oss bland annat att återanvända programkod. Tack vare arv kan man jämförelsevis enkelt lägga till nya egenskaper och nya beteenden till en modul som redan är skriven, och tack vare polymorfism kan man använda andra, färdiga moduler för att arbeta med nya saker.

Arv

Om man behöver arbeta med fåglar i sitt program, och redan har en klass som heter Djur och som beskriver djur, kan man skapa en ny klass som heter Fågel och som ärver från Djur-klassen.

Genom det arvet kommer en fågel att ha alla de egenskaper som ett djur har, och den kan göra allt som ett djur kan. Sen lägger man bara till vad som är specifikt för fåglar, till exempel att de kan flyga. (Förutom att pingviner inte kan flyga, och fladdermöss kan, men det bryr vi oss inte om just nu.)

Faktaruta: När klassen Fågel ärver från klassen Djur, kallar man Djur för superklass eller basklass, och klassen Fågel för subklass eller härledd klass.

Polymorfism

Polymorfism innebär att man kan använda flera olika typer av objekt på samma sätt. När man håller på med objektorientering används arv för att visa vilka objekt som kan användas var.

Om man låter klassen Fågel ärva klassen Djur, så innebär polymorfism att allt som man kan göra med ett djur, det kan man också göra med en fågel. Om vi redan har skrivit programkod som räknar eller sorterar djur, så kan vi använda samma programkod för att räkna eller sortera fåglar.

Med arv kunde vi återanvända mycket av det arbete som man tidigare lagt ner på klassen Djur, men tack vare polymorfism kan vi också återanvända mycket av det arbete som man tidigare lagt ner på resten av programmet.

Tre olika sätt att se på arv

Vi talade tidigare om att det finns tre olika sätt att se på en klass: som en mängd, som en datatyp och som en modul. Beroende på vilket sätt av dem man väljer, kommer man att se ganska olika även på arv och polymorfism.

1. Arv med klasser sedda som mängder

Om vi betraktar klasserna som mängder av objekt, så visar arven hur de olika mängderna hänger ihop. Om klassen Fågel ärver klassen Djur, så vet man att alla fåglar är djur. Annorlunda uttryckt: fåglarna är en delmängd av djuren. Och om klassen Fisk ärver klassen Djur, så är alla fiskar djur:

Arv av klasser som mängder

Algot är en orm, och inte med i någon av subklasserna Fågel och Fisk. Han är bara ett djur.

Eftersom fåglarna ju faktiskt är djur, är det kanske inte så konstigt att en fågel har alla egenskaper som ett djur har. Det är just det som det innebär att fågel-klassen ärver djur-klassen!

Man talar ibland om specialisering och generalisering: klassen Fågel är en specialisering av klassen Djur, och klassen Djur kan ses som en generalisering av klassen Fågel.

2. Arv med klasser sedda som datatyper

Om vi betraktar klasserna som datatyper, som anger den uppsättning av egenskaper som varje objekt i klassen ska ha, så anger arven hur sådana egenskaper kopieras (eller ärvs) från en klass till en annan.

Varje djur har ett namn och en vikt, och eftersom fågel-klassen ärver djur-klassen, kommer även varje fågel att ha ett namn och en vikt.

Arv av klasser som datatyper i UML-notation

3. Arv med klasser sedda som moduler

Om vi betraktar klasserna som moduler i ett program, så anger arv vilka variabler och programdelar som är åtkomliga för användning i de olika klassernas programkod.

I programkoden här nedan kan till exempel algoritmerna i metoden simma inte komma åt variabeln vingspann, för Fågel och Fisk är olika klasser och därmed olika moduler. Men metoden simma kan komma åt variabeln vikt, för även om Fisk och Djur är olika klasser, och olika moduler, så ärver Fisk från Djur.

class Djur {
    protected String namn;
    protected double vikt;
    public void ät() { /* ... */ }
    public void sov() { /* ... */ }
}

class Fågel extends Djur {
    private double vingspann;
    public void flyg() { /* ... */ }
}

class Fisk extends Djur {
    private double maxdjup;
    public void simma() { /* ... */ }
}
Ur ett programmeringsperspektiv går det att använda arv för annat än specialisering, till exempel för att återanvända en implementation. Om man låter klassen Flygplan ärva klassen Fågel, så kan flygplanen använda fåglarnas flyg-metod. Men det är inte att rekommendera, för då har man egentligen också sagt att flygplan är fåglar.

Tre olika sätt att se på polymorfism

Polymorfism innebär att man kan använda flera olika typer av objekt på samma sätt. Det finns flera olika varianter på polymorfism, men den som vi pratar om här säger att allt som man kan göra med ett objekt av en viss klass, det kan man också göra med objekt av alla subklasser till den klassen. Om vi till exempel kan mata ett djur med smör, så att djurets vikt ökar, så kan vi göra samma sak med fåglar, om fågel-klassen ärver djur-klassen.

Vi skrev tidigare att man kan tänka sig vad en klass är på tre olika sätt: som en mängd, som en datatyp och som en modul. Vilket sätt man väljer att se på klasserna, påverkar hur man ser på polymorfism.

1. Polymorfism med klasser sedda som mängder

Om vi börjar med att tänka oss klasserna som mängder av objekt, så är det inte så konstigt att man kan göra djur-saker även med fåglar. Fåglarna är ju djur.

2. Polymorfism med klasser sedda som datatyper

Om vi sen tänker oss klasserna som datatyper, så handlar det mer om vilken sorts objekt som en variabel kan innehålla. Man kan tänka sig en variabel som en liten låda som man kan lägga data i. Varje variabel har en datatyp, och den datatypen bestämmer vilken sorts data som man kan lägga i lådan.

Varje variabel kan innehålla objekt av den klass som den har som datatyp, och även objekt av alla subklasser till den klassen. I en variabel av typen Fågel kan man lägga data av typen Fågel, alltså Fågel-objekt, men inte Fisk-objekt. I en variabel av typen Djur kan man förstås lägga Djur-objekt, men också Fågel-objekt och Fisk-objekt, eftersom klasserna Fågel och Fisk ärver klassen Djur.

3. Polymorfism med klasser sedda som moduler

Om vi till sist tänker oss klasserna som moduler, så ja, inte vet jag hur jag ska förklara hur polymorfism fungerar då. Det är nog bättre att tänka mer på objekten än på programkoden.

Arvshierarkier

Man kan ha arv i flera nivåer, till exempel genom att införa klasserna Haj och Strömming, som båda ärver klassen Fisk i vårt exempel ovan. Man talar om arvshierarkier:

En arvshierarki i flera nivåer

Multipelt arv

I exemplen ovan har varje klass ärvt från en enda superklass (även om den superklassen sen i sin tur kan ha ärvt från en annan klass). Varje "barnklass" har alltså en enda "förälder". Men många objektorienterade system tillåter att en klass ärver från flera olika klasser, så kallat multipelt arv. (Programmeringsspråket C++ tillåter multipelt arv, men Java gör det inte.)

Till exempel vill man kanske låta klassen Amfibiebil ärva både från klassen Bil och från klassen Båt. Då kommer amfibiebilar att kunna göra allt som bilar kan, och allt som båtar kan, plus att de kan köra ner i vattnet och tillbaka upp på land. Överallt där man kan använda en bil eller en båt kan man också använda en amfibiebil.

Multipelt arv

Det kan bli en del problem när man har mutltipelt arv. Titta till exempel i figuren ovan på metoden Sväng. Alla bilar kan svänga, och eftersom amfibiebilar är bilar så kan även amfibiebilar svänga. Alla båtar kan svänga, och eftersom amfibiebilar är båtar så kan även amfibiebilar svänga. Men när en amfibiebil svänger, hur bär den sig åt då? Svänger den som en båt eller som en bil?

Abstrakta klasser

En abstrakt klass är en klass där man inte kan skapa några objekt som bara tillhör den klassen. Däremot går det att skapa objekt av dess subklasser.

Ett exempel som brukar användas är geometriska figurer som ska ritas upp på en datorskärm. Man kanske skapar klassen Figur, och den får subklasser som Cirkel, Triangel, Kvadrat och Gotland. Figurerna ska kunna rita upp sig själva på skärmen, så vi låter klassen Figur få en metod som vi kallar Rita. En cirkel kommer att rita upp sig som en rund ring, en kvadrat ritar förstås upp sig som en kvadrat, och så vidare.

Men ett objekt som bara är en Figur, och inte tillhör någon av subklasserna, hur ska det göra för att rita upp sig? Hur ser en sån figur ut? Det finns ju inte någon generisk figur som varken är en cirkel, kvadrat eller någon annan specifik figur, och därför kan man inte skriva någon Rita-metod för Figur-klassen, bara för dess subklasser. Alltså är det ingen idé att skapa några objekt som bara är figurer.

En klass som det inte går att skapa objekt av kallas alltså abstrakt klass. En klass som det går att skapa objekt av kallas konkret klass.

Objektidentitet

En finess med objektorientering är att varje objekt går att särskilja från andra objekt, även om de skulle se precis likadana ut. Om klassen Person har attributen namn, skonummer och längd, så har alltså varje person ett namn, ett skonummer och en längd. Två olika personer kan ha precis samma egenskaper (till exempel att de heter Kajsa, har skonummer 39 och är 170 centimeter långa), och det går ändå att skilja dem åt.

Det fungerar så att varje objekt har en objektidentifierare, ibland kallad OID. (I en del programmeringsspråk, som C++, är det helt enkelt den minnesadress som objektet hamnat på i datorns minne.)

Det här med objektidentitet skiljer sig från en del andra datasammanhang. Ett datorprogram som arbetar med vanliga tal brukar till exempel inte ha något sätt att skilja på två olika förekomster av talet 2. Det finns, så att säga, bara en enda tvåa. Och i en relationsdatabas, där data lagras som rader i tabeller, finns det inte något sätt att skilja på två rader som har samma värden i alla kolumner.

De viktigaste begreppen

De viktigaste begreppen från det här avsnittet:

Objektorientering, objektorienterad programmering, algoritm, data, datatyp, modul, modularisering, gränssnitt, användargränssnitt, information hiding, klass, objekt, instans, attribut, metod, mängd, arv, subklass, superklass, polymorfism, multipelt arv, abstrakt klass, konkret klass, objektidentitet, objektidentifierare, OID

Litteratur

Vill man läsa mer om objektorientering bör man titta i grundböcker om programmering eller läroböcker om något objektorienterat programmeringsspråk, snarare än i en databasbok. Men en del databasböcker ger i alla fall en kort introduktion till grunder om objektorientering, även om det kan vara en ganska teoretisk genomgång.


Webbkursen om databaser av Thomas Padron-McCarthy.