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.
|
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.
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!
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
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.)
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.
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. |
Om vi först tänker oss klassen som en mängd, blir den helt enkelt samlingen (eller mängden) av alla personer:
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:
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:
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.class Person { public String namn; public int skonummer; public double längd; public void ät() { /* ... */ } public void sov() { /* ... */ } public void prata() { /* ... */ } }
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.
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.
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. |
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.
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.
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.
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.
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.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() { /* ... */ } }
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.
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.
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.
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?
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.
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.
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