OOP - Synlighet

I denna artikeln går vi igenom hur man döljer information i klasser, och vad poängen med det är.

Om vi fortsätter på vår klass Datum, som vi hade i föregående artikel:

PHP - Datum.php
<?php class Datum{ public $år; public $månad; public $dag; public function __construct($år, $månad, $dag){ $this->år = $år; $this->månad = $månad; $this->dag = $dag; } public function tillSträng(){ $sträng = $this->år.'-'; if($this->månad < 10){ $sträng .= '0'; } $sträng .= $this->månad.'-'; if($this->dag < 10){ $sträng .= '0'; } $sträng .= $this->dag; return $sträng; } public function antalDagarIMånad($månad){ $allaMånadersDagar = array( 1 => 31, 2 => 28, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31 ); return $allaMånadersDagar[$månad]; } public function ökaEnDag(){ $dennaMånadensDagar = $this->antalDagarIMånad($this->månad); if($this->dag == $dennaMånadensDagar){ $this->dag = 1; if($this->månad == 12){ $this->år++; $this->månad = 1; }else{ $this->månad++; } }else{ $this->dag++; } } public function minskaEnDag($datum){ if($this->dag == 1){ if($this->månad == 1){ $this->år--; $this->månad = 12; }else{ $this->månad--; } $dennaMånadensDagar = $this->antalDagarIMånad($this->månad); $this->dag = $dennaMånadensDagar; }else{ $this->dag--; } } public function ändra($antalDagar){ if(0 < $antalDagar){ $ökaEllerMinska = true; }else{ $ökaEllerMinska = false; $antalDagar = -$antalDagar; } for($i=1; $i<=$antalDagar; $i++){ if($ökaEllerMinska){ $this->ökaEnDag(); }else{ $this->minskaEnDag(); } } } } ?>

När någon använder denna klassen, vad kan gå fel? Jo, tänk om någon idiot gör såhär:

PHP - PHP: Hypertext Preprocessor
<?php // Inkludera klassen include_once 'Datum.php'; // Skapa en ny instans av klassen $datum = new Datum(2011, 4, 30); // Notera, giltigt datum // Idioten sätter månaden till 2 $datum->månad = 2; // Nu är det den 30 februari! // Skriv ut datumet echo $datum->tillSträng(); ?>
2011-02-30

Ett annat exempel är om man sätter månaden till en månad som inte finns.

PHP - PHP: Hypertext Preprocessor
<?php // Inkludera klassen include_once 'Datum.php'; // Skapa en ny instans av klassen $datum = new Datum(2011, 4, 30); // Idioten sätter månaden till 15 $datum->månad = 15; $datum->ökaEnDag(); // Kommer ge felmeddelande! ?>

Rad 12 ovan kommer resultera i ett felmeddelande, för när den metoden anropas kommer bland annat koden på raderna 41-60 i filen Datum.php att köras, och på rad 58 försöker vi ta fram hur många dagar det är i månad 15. Månad 15 finns ju inte, så PHP vet inte hur den ska tolka vad vi försöker göra och kommer ge ifrån sig ett felmeddelande.

Fel som dessa kan vara grymt svårt att felsöka. Felet ligger ju egentligen på rad 10 i koden ovan, då vi sätter månaden till 15, men informationen vi som programmerare får av PHP är att felet ligger på rad 58 i filen klassen ligger i. Hur lätt är det att leta sig tillbaks från rad 58 i filen Datum.php till rad 10 i koden ovan? I metoden går det inte att se vartifrån den blev anropad, så man måste kolla igenom alla ställen i sin kod man anropar metoden på, och se om felet ligger kring det anropet. Det är inte så kul, och troligtvis är det kod i flera olika filer som har körts, så det är inte så lätt att ha koll på vilken kod som körs heller.

Men är detta ett problem egentligen? Om någon är så dum att den ändrar månaden utifrån klassen, ska den då inte få stå sitt eget kast? Man kan tycka det, men i OOP finns det ett skydd mot sånt här, och man kan likaväl använda det när det ändå finns. Detta kommer hjälpa de som använder vår klass i framtiden mer än vad det hjälper oss som gör klassen.

Skyddet går under namnen synlighet, informations döljning och inkapsling (på engelska går det under namnet "information hiding"). När vi deklarerar våra instansvariabler säger vi helt enkelt åt PHP att de inte får anropas utifrån på detta sättet. Då kan vi bara komma åt dem genom att skriva $this->instansvariabeln i klassen. Man får detta skydd genom att deklarera dem med hjälp av nyckelordet private istället för public på följande sätt:

PHP - Datum.php
<?php class Datum{ // Våra instansvariabler (är nu privata) private $this->år; private $this->månad; private $this->dag; // ...och så konstruktorn och alla metoder som innan } ?>
PHP - PHP: Hypertext Preprocessor
<?php // Inkludera klassen include_once 'Datum.php'; // Skapa en ny instans av klassen $datum = new Datum(2011, 4, 30); // Idioten sätter månaden till 15 $datum->månad = 15; // Kommer nu ge ett felmeddelande, och PHP avbryter exekveringen. ?>

Nu kommer PHP ge felmeddelande där felet faktiskt finns, och det blir mycket enklare att felsöka.

Egentligen borde vi även kontrollera att datumet faktiskt är ett korrekt datum i konstruktorn, men det kommer vi först göra i en senare artikel. Håll ut!

Det är inte bara instansvariabler man kan skydda på detta sättet, utan metoderna kan också göras det. De som använder klassen ska ju egentligen bara använda sig av metoderna ändra och tillSträng (och konstruktorn), så alla andra metoder kan vi lika gärna skydda från att anropas utifrån. Det gör med genom att byta ut nyckelordet public mot private, precis som med instansvariablerna.

PHP - Datum.php
<?php class Datum{ public $år; public $månad; public $dag; public function __construct($år, $månad, $dag){ $this->år = $år; $this->månad = $månad; $this->dag = $dag; } public function tillSträng(){ $sträng = $this->år.'-'; if($this->månad < 10){ $sträng .= '0'; } $sträng .= $this->månad.'-'; if($this->dag < 10){ $sträng .= '0'; } $sträng .= $this->dag; return $sträng; } // Kan ej anropas utifrån klassen längre private function antalDagarIMånad($månad){ $allaMånadersDagar = array( 1 => 31, 2 => 28, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31 ); return $allaMånadersDagar[$månad]; } // Kan ej anropas utifrån klassen längre private function ökaEnDag(){ $dennaMånadensDagar = $this->antalDagarIMånad($this->månad); if($this->dag == $dennaMånadensDagar){ $this->dag = 1; if($this->månad == 12){ $this->år++; $this->månad = 1; }else{ $this->månad++; } }else{ $this->dag++; } } // Kan ej anropas utifrån klassen längre private function minskaEnDag($datum){ if($this->dag == 1){ if($this->månad == 1){ $this->år--; $this->månad = 12; }else{ $this->månad--; } $dennaMånadensDagar = $this->antalDagarIMånad($this->månad); $this->dag = $dennaMånadensDagar; }else{ $this->dag--; } } public function ändra($antalDagar){ if(0 < $antalDagar){ $ökaEllerMinska = true; }else{ $ökaEllerMinska = false; $antalDagar = -$antalDagar; } for($i=1; $i<=$antalDagar; $i++){ if($ökaEllerMinska){ $this->ökaEnDag(); }else{ $this->minskaEnDag(); } } } } ?>
PHP - PHP: Hypertext Preprocessor
<?php // Inkludera klassen include_once 'Datum.php'; // Skapa en ny instans av klassen $datum = new Datum(2011, 4, 30); // Försöker öka datumet en dag med metoden ökaEnDag $datum->ökaEnDag(); // Kommer nu ge ett felmeddelande, och PHP avbryter exekveringen. ?>

Finns det någon mening med att göra metoder privata? Det gör det faktiskt. Man kanske vill att en metod enbart ska köras en gång, och det ser man själv till att den gör i konstruktorn. Om man gör den privat kan inte de som använder klassen anropa den metoden igen, och vi är säkra på att den aldrig körs mer.

Sedan blir det även enklare för dem som använder klassen vi har skapat. Om dem enbart ska använda sig av klassen, utan att ändra något i den, behöver dem bara lära sig hur man anropar de metoder som har synligheten public, eftersom de inte kan anropa dem som har synligheten private. I vårt exempel med klassen Datum behöver de bara lära sig metoderna tillSträng och ändra. Visst skulle man även kunna låta dem få använda metoderna ökaEnDag och minskaEnDag, men de kan ju åstadkomma samma sak med metoden ändra, så varför låta dem lära sig två metoder som inte bidrar något?

Det finns faktiskt ett tredje värde man kan använda som synlighetskydd, och det är protected. Det är ungefär som private, fast inte lika strängt. För att kunna förklara hur det fungerar mer i detalj behövs kunskaper om arv i OOP, så du får lära dig mer om detta skyddet när du läser om arv (vilket tas upp senare i artikelserien).


En vanlig strategi när man skriver klasser (oavsett språk) är att alltid låta instansvariablerna ha synligheten private. Då kan de som använder klassen enbart använda sig av de metoder med synligheten public, vilket har sina fördelar. Den största är att man kan skriva om klassen och byta ut instansvariabler mot en lämpligare representation, och så länge man ser till att samma metoder finns kvar kan alla som använt klassen fortfarande använda den, men nu med andra instansvariabler.

Ett exempel med klassen Datum är att istället för att ha instansvariablerna år, månad och dag låta ett datum representeras som antalet dagar sedan tidsräkningens början (0000-00-00). Exempel på det i vanlig imperativ programmering gavs i artikeln Exempel på problemlösning - Ändra representation. Nu kommer den som exempel i OOP.

PHP - PHP: Hypertext Preprocessor
<?php class Datum{ private $antalDagar; public function __construct($år, $månad, $dag){ // Räkna ut hur många dagar som passerat i föregående år $passeradeÅrIDagar = 365*$år // Räkna ut hur många dagar som passerat föregående månader detta året $passeradeMånaderIDagar = 0; for($i=0; $i<$månad; $i++){ $passeradeMånaderIDagar += $this->antalDagarIMånad($i); } // Räkna ut hur många dagar som passerat totalt fram tills datumet $this->antalDagar = $passeradeÅrIDagar + $passeradeMånaderIDagar + $dag; } // Metod som returnerar antalet dagar i angiven månad private function antalDagarIMånad($månad){ $antalDagarIMånader = array( 1 => 31, 2 => 28, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31 ); return $antalDagarIMånader[$månad]; } // Metod som gör om antalet dagar till en sträng public function tillSträng(){ $datum = $this->tillDatum(); $sträng = $datum['år'].'-'; if($datum['månad'] < 10){ $sträng .= '0'; } $sträng .= $datum['månad'].'-'; if($datum['dag'] < 10){ $sträng .= '0'; } $sträng .= $datum['dag']; return $sträng; } private function tillDatum(){ $datum = array(); $dagar = $this->antalDagar; $passeradeDagarIÅr = floor($dagar/365); $datum['år'] = $passeradeDagarIÅr; $dagar = $dagar - $passeradeDagarIÅr*365; $passeradeDagarIMånader = 0; for($i=1; $i<=12; $i++){ if(dagarIMånad($i) < $dagar){ $dagar = $dagar - dagarIMånad($i); }else{ $datum['månad'] = $i; break; } } $datum['dag'] = $dagar; return $datum; } // Metod som ändrar datumet det angivna antalet dagar public function ändra($dagar){ $this->antalDagar += $dagar; } ?>

Eftersom vi har sett till att instansvariablerna i de båda olika implementationerna av klassen har synligheten private är vi säkra på att inga som använder klassen har skrivit något i stil med:

PHP - PHP: Hypertext Preprocessor
<?php $datum->månad = 5; ?>

Så de som använder en av klasserna kan utan problem växla till den andra implementationen utan att behöva ändra något i sin kod. Sedan har även de två olika implementationerna exakt samma metoder som har synligheten public, vilket gör att de båda implementationerna används på samma sätt, så här finns det inte heller något som ställer till det. Kort sagt: Båda implementationerna av klassen används på samma sätt, men är olika effektiva. Om de som använder klassen upptäcker att de använder den på ett sätt som gör den andra implementationen mer lämplig kan de utan problem byta.