Olen koettanut etsiä yksinkertaista suomenkielistä opasta, jossa selitettäisiin miten abstrakteja luokkia ja staattisia funktioita ja muuttujia käytetään, mutta ei tunnu löytyvän. Minulla on jonkinlainen hatara käsitys niistä, mutta haluaisin tarkempaa tietoa niiden käyttämisestä. Jotain sellaista olen abstrakteista luokista lukenut, ettei niitä voi "instantioida" mitä ikinä se sitten tarkoittaakaan. Mistään kun ei löytynyt atk-sanastoa, joka selittäisi tuon termin. Jos olen oikein käsittänyt, niin abstrakteja luokkia voi käyttää, vaikkei loisi niitä käyttäviä olioita. Voivatko ne silloin sisältää __construct funktiota ja jos voivat, niin miten se silloin toimii?
Voisiko joku selittää lyhyesti ja yksinkertaisesti abstraktiuden ja staattisuuden perusideat. Entä miten mahtavat toimia __get, __set, __autoload ja __toString funktiot?
Minkähän takia tätä ei Putkan omassa oppaassa ole käsitelty?
Abstrakti luokka on luokka, josta ei voi suoraan luoda instansseja eli "yksilöitä" new-operaattoria käyttäen. Sen sijaan niihin voi kirjoittaa metodeja ja lisätä muuttujia, joita abstraktin perusluokan perivät luokat voivat käyttää sellaisinaan, jolloin koodia ei tarvitse kopioida luokasta toiseen, kun toteutus pysyy muuttumattomana.
Muutoin abstraktit luokat käyttäytyvät kuten tavallisetkin luokat, eli ne voivat periä muita luokkia ja muut luokat voivat periytyä niistä. Abstrakti luokka B voi periä abstraktin luokan A, mutta edelleenkään B:stä ei voi tehdä suoraan uusia instansseja (koska se on abstrakti), vaan täytyy tehdä kolmas luokka C, joka ei ole abstrakti.
Staattiset funktiot ovat luokkien jäsenfunktioita, joita voi kutsua ilman luokan instanssia (notaatio: MyClass::staticFunction()). Ne eivät voi myöskään suoraan käyttää luokan ei-staattisia metodeja tai jäsenmuuttujia. Vanhemmat php-kehysympäristöt käyttävät runsaasti staattisia funktioita eri asioihin, mutta sittemmin niiden käyttöä on selvästi vähennetty, koska ne eivät vastaa nykyaikaista käsitystä hyvästä olio-ohjelmoinnista.
Staattisten funktioiden ongelma oli aikoinaan se, ettei perusluokassa A määritettyä ja siitä periytyvässä luokassa B ylikirjoitettua staattista funktiota voinut kutsua enää itse perusluokassa (A), koska self-avainsana näkee vain kyseisen luokan tasolla näkyvät jäsenet. Sittemmin on esitelty toinen avainsana static, joka toimii juuri päin vastoin, eli se käyttää "viimeisintä versiota" jäsenestä. (Late static bindings)
Abstrakti metodi / funktio taas on sellainen abstraktin luokan funktio, joka täytyy toteuttaa kaikissa kyseisen abstraktin luokan perivissä luokissa, mutta jota ei ole toteutettu itse abstraktissa luokassa.
Yleensä tässä vaiheessa esitetään geometrian perusteita kertaava esimerkki:
<?php $a = new Rectangle(4, 9); $b = new Circle(5); print $a . PHP_EOL; print $b . PHP_EOL; abstract class Shape { abstract public function getArea(); abstract public function getCircumference(); public function __construct() { } public function __toString() { return "I am a " . get_class($this); } } class Rectangle extends Shape { private $width; private $height; public function __construct($width = 0, $height = 0) { parent::__construct(); $this->setWidth($width); $this->setHeight($height); } public function __toString() { $msg = parent::__toString(); $msg .= " and I am {$this->height} units tall and {$this->width} units wide"; return $msg; } public function getWidth() { return $this->width; } public function setWidth($w) { $this->width = max($w, 0); } public function getHeight() { return $this->height; } public function setHeight($h) { $this->height = max($h, 0); } // Toteutetaan Shape-luokan abstraktit metodit -> public function getArea() { return $this->getWidth() * $this->getHeight(); } public function getCircumference() { return ($this->getWidth() + $this->getHeight()) * 2; } } class Circle extends Shape { private $radius; public function __construct($radius = 0) { parent::__construct(); $this->setRadius($radius); } public function __toString() { $msg = parent::__toString(); $msg .= " and my radius is {$this->radius} units"; return $msg; } public function getRadius() { return $this->radius; } public function setRadius($r) { $this->radius = max($r, 0); } // Toteutetaan Shape-luokan abstraktit metodit -> public function getArea() { return $this->getRadius() * pow(pi(), 2); } public function getCircumference() { return $this->getRadius() * 2 * pi(); } }
Magic methodsit kuten __get, __set ja __toString() selviävät kyllä ihan php.netin opasta lukemalla.
Funktiota __autoload ei pidä käyttää, sillä sen tilalle on tuotu universaalimpi funktio spl_autoload_register(). Autoloadaamisen idea on lyhykäisyydessään se, että luokat sisältävät kooditiedostot voidaan ladata automaattisesti vasta luokkaa tarvittaessa, jolloin niitä kaikkia ei tarvitse ladata kerralla käyttämällä include- tai require-kutsuja.
Havainnollisestetaan näillä kahdella esimerkillä, jotka vastaavat toisiaan toiminnallisuutensa osalta:
*** Shape.php <?php class Shape { // toteutus } *** Rectangle.php <?php require_once 'Shape.php'; class Rectangle extends Shape { // toteutus } *** index.php <?php require_once 'src/Shape.php'; require_once 'src/Rectangle.php'; $rect = new Rectangle(3, 5);
*** Shape.php <?php class Shape { // toteutus } *** Rectangle.php <?php class Rectangle extends Shape { // toteutus } *** index.php <?php // Lataa automaattisesti luokat tiedostoista Shape.php ja Rectangle.php spl_autoload_register(function($class) { $file = "src/{$class}.php"; if (is_file($file)) { include $file; } }); $rect = new Rectangle(3, 5);
Abstrakteja luokkia käytetään silloin, kun halutaan mallintaan jotain korkeamman tason käsitettä ja siksi abstrakteista luokista ei pystytä luomaan instansseja eli olioita. Esimerkiksi Kuvio-luokan pitäisi olla abstrakti, koska se ei vielä itsessään määrittele konkreettisesti, että mistä kuviosta on kysymys; kuvio voi olla niin ikään ympyrä, nelikulmio, kolmio... Jokaisella kuviolla on kuitenkin yhtäläisiä piirteitä, kuten mm. pinta-ala sekä piiri. Näin ollen voisimme toteuttaa Kuvio-luokkaan valmiit abstraktit eli toteuttamattomat metodit näiden ominaisuuksien laskemista varten, jotka sitten voisimme toteuttaa tietyllä tavalla kuviosta riippuen esim. ympärän kohdalle 2*pii*r sekä pii*r^2 tai neliokulmion kohdalla 2*w + 2*h sekä w*h...
Sehän selvensi mukavasti. Kiitoksia The Alchemist ja Triton.
Voisiko tuota The Alchemistin antamaa esimerkkiä tehdä niin, etteivät Rectangle ja Circle luokat peri Shape luokkaa? Vai pitäisikö siinä vaiheessa kikkailla jotenkin staattisten funktioiden kanssa?
En oikein käsittänyt, että mitä hyötyä on olla abstrakteja funktioita abstraktien luokkien sisällä. Olisiko sivu toiminut, vaikkei getArea ja getCircumference funktioita olisi ollut Shape luokassa? Tai olisiko tullut virheitä, jos niitä ei olisi löytynyt Rectangle ja Circle luokista? Eli määräsivätkö ne Shape luokan abstraktit funktiot, että niitä piti käyttää Shapen perivissä luokissa. Eikö se kuulosta vähän rajapintojen toiminnalta. En ihan täysin niitäkään ymmärrä, mutta tämä kuulostaa minun korvissani rajapintojen korvaamiselta. Pitänee vain alkaa kokeilla näitä käytännössä ja opiskella lisää yrityksen ja erehdyksen kautta..
AkeMake kirjoitti:
Voisiko tuota The Alchemistin antamaa esimerkkiä tehdä niin, etteivät Rectangle ja Circle luokat peri Shape luokkaa?
PHP:ssä abstraktien luokkien käyttö ei ole teknisesti välttämätöntä, koska PHP ei tarkista ennen kutsuvaihetta, onko tiettyjä funktioita olemassa. En tähän hätään keksi kuin yhden tilanteen, jossa periytymisellä on PHP:ssä välttämätöntä merkitystä: type hinting. Jos parametrin tyyppi on Shape, Rectangle-olio kelpaa vain, jos se periytyy Shape-luokasta. Kuitenkin jos parametrin tyyppi jätetään ilmoittamatta, täysin sama koodi alkaa toimia ilmankin periytymistä.
// Tämä funktio hyväksyy Rectangle-olion vain, jos se periytyy Shape-luokasta. function f(Shape $a) { return $a->getArea(); } // Tämä funktio hyväksyy Rectangle-olion joka tapauksessa. function f($a) { return $a->getArea(); }
AkeMake kirjoitti:
En oikein käsittänyt, että mitä hyötyä on olla abstrakteja funktioita abstraktien luokkien sisällä.
Kyse on sopimuksesta: jos luokka haluaa olla Shape, sen pitää täyttää tietyt ehdot. PHP:ssä merkittävin hyöty on, että PHP antaa virheilmoituksen, jos jokin funktio puuttuu. (Kokeile poistaa Circle-luokasta getArea-funktio, niin näet.) Tiedetään siis takuuvarmasti, että jokaisessa Shape-tyyppisessä oliossa on funktio getArea riippumatta siitä, mikä olio tarkalleen on kyseessä. Jos abstraktia funktiota ei olisi esitelty Shape-luokassa, olisi tuurista kiinni, onko sitä myöhemmissä luokissa, ja asia selviäisi vasta silloin, kun joku yrittää kutsua sitä.
AkeMake kirjoitti:
Olisiko sivu toiminut, vaikkei getArea ja getCircumference funktioita olisi ollut Shape luokassa?
PHP:ssä koodi toimisi joka tapauksessa, koska funktioiden olemassaoloa ei tarkisteta etukäteen vaan vasta kutsuhetkellä. Kuitenkin esimerkiksi Javassa jäsenet tarkistetaan koodia käännettäessä, jolloin vastaava koodi ei toimi.
// PHP: function f(Shape $a) { $a->getRadius(); // Toimii, jos $a sisältää funktion getRadius. }
// Java: void f(Shape a) { a.getRadius(); // Ei käy, koska Shape ei sisällä funktiota getRadius. }
AkeMake kirjoitti:
Voisiko tuota The Alchemistin antamaa esimerkkiä tehdä niin, etteivät Rectangle ja Circle luokat peri Shape luokkaa? Vai pitäisikö siinä vaiheessa kikkailla jotenkin staattisten funktioiden kanssa?
Voisi toki, mutta silloin sillä ei olisi enää mitään tekemistä abstraktien luokkien kanssa eikä siis tämän kysymyksnekään. Staattiset funktiot ei liity tähän mitenkään.
AkeMake kirjoitti:
En oikein käsittänyt, että mitä hyötyä on olla abstrakteja funktioita abstraktien luokkien sisällä. Olisiko sivu toiminut, vaikkei getArea ja getCircumference funktioita olisi ollut Shape luokassa? Tai olisiko tullut virheitä, jos niitä ei olisi löytynyt Rectangle ja Circle luokista? Eli määräsivätkö ne Shape luokan abstraktit funktiot, että niitä piti käyttää Shapen perivissä luokissa. Eikö se kuulosta vähän rajapintojen toiminnalta. En ihan täysin niitäkään ymmärrä, mutta tämä kuulostaa minun korvissani rajapintojen korvaamiselta. Pitänee vain alkaa kokeilla näitä käytännössä ja opiskella lisää yrityksen ja erehdyksen kautta..
Olen vähän oikeilla jäljillä siinä että abstraktit luokat ovat osittain lähellä rajapintoja. Rajapintoihin nähden niissä on kuitenkin se etu että abstrakteissa luokissa voi myös toteutta oikeita funktioita ja ne voivat periä toisia abstrakteja luokkia. Alchemistin Shape-esimerkki on siinä mielessä ehkä vähän heikohto että siinä on ainoastaan yksi oikeasti toteutettu funktio joka sekin on melko keinotekoisen oloinen. Usein abstraktissa luokassa saattaa olla vaikkapa 10 oikeaa funktiota jotka sisältävät jotain logiikkaa ja sitten 1 tai 2 abstraktia funktiota, jotka perivien luokkien tulee toteuttaa. Tällöin etu rajapintoihin nähden on jo selkeämmin nähtävissä kun nuo 10 funktiota pitää kirjoittaa vain kerran. Toki nekin voi perivissä luokissa ylikirjoittaa jos on tarve.
Tavallisiin luokkiin nähden abstrakteilla luokilla taas on se etu että abstrakti luokka voi pakottaa perivän luokan toteuttamaan jonkin funktion jollain tietyllä rajapinnalla. Joskus tilanne on se että halutaan tehdä perusluokka, joka tarjoaa joukon yhteisiä funktioita perivien luokkien käytettäviksi mutta joillekin oleelliselle funktiolle ei ole olemassa mitään järkevää "perustoteutusta". Tällöin abstrakti perusluokka tarjoaa mahdollisuuden pakottaa perivät luokat toteuttamaan tietty abstraktiksi määritelty funktio tietyllä rajapinnalla. Jokainen perivä luokka voi toteuttaa sen kuitenkin omalla tavallaan.
Olisin maininnut esimerkissäni rajapinnoista mutta oletin niiden olevan AP:lle uutta asiaa ja siten melko turhia mainintoja. En silti korvaisi Shape-luokkaa pelkällä rajapinnalla vaikkei sillä olisikaan kuin abstrakteja funktioita. Rectangle- ja Circle-luokat ovat loogisesti saman abstraktin Shape-käsitteen alaisia, joten tuntuu järkevältä, että ne myös periytyvät samasta kantaluokasta.
Sen sijaan Shape-luokan abstraktit metodit olisi voinut esitellä ShapeInterface-rajapinnassa, jonka Shape-luokka olisi toteuttanut. Näin myös toimivat uusimmat php-kehysympäristöt, eli type hinttejä käyttäessä yleensä pakotetaan yhteensopivuus tietyn rajapinnan kanssa luokkien sijaan.
Aihe on jo aika vanha, joten et voi enää vastata siihen.