En millään ymmärrä, tai siis ymmärrän mutta en osaa soveltaa interface:ja. Osaisiko joku avata hieman.
Interfacella esitellään yhteinen rajapinta esimerksi palveluille.
Tämä on usein kätevää, "pakottaen" sen että samantyyppiset otukset toimii samalla arkkitehtuurilla.
Otetaan esimerkkinä vaikka järjestelmä, missä jokainen taulu tietokannassa on oma luokkansa. Jokaiselle luokalle on oma service, joka tarjoaa haun ja talletuksen kyseiseen tauluun toteuttamalla interfacen, jossa funktiot on esitelty:
interface, jota servicet käyttävät
interface iDataAccess { public function search($targetColumn,$data); public function save($item); }
Modelit:
class person { public $id; public $name; public $age; } class address { public $id; public $personID; public $address; public $city; public $country; public $postalCode; }
Servicet, jotka toteuttaa interfacet (eli servicet toteuttaa ne funktiot, jotka on esitelty interfacessa)
class personService implements iDataAccess { public function search($targetColumn, $data) { //SELECT * FROM persons WHERE $targetColumn = $data } public function save($item) { // INSERT INTO persons... } } class addressService implements iDataAccess { public function search($targetColumn,$data) { //...SELECT * FROM addresses WHERE $targetColumn = $data } public function save($item) { // INSERT INTO addresses... } }
ja käyttö:
$PersonService = new personService; $Person = $PersonService->search("id",2); $Person->name = "Pekka"; $PersonService->save($Person); $AddressService = new addressService; $address = $AddressService.search("personID",$Person->id); $address->address = "Heinäkatu 4"; $AddressService.save($address);
toisena esimerkkinä merkkijonon käsittelyä:
Interface
interface iString { public function toString(); }
luokat
class person implements iString { public $name; public $id; public $age; public function toString( return "Name: " . $name . " Age: " . $age; ) } class address implements iString { public $id; public $personID; public $address; public $city; public $country; public $postalCode; public function toString( return "Address: " . $address . ", " . $postalCode . " " . $city . " " . $country; ) }
Lyhyemmin sanottuna interfacejen kautta voit kuluttaa juuri tietynlaisia koodia mikä taatusti tarjoaa ennalta määrätyt toiminnot.
Esimerkkinä jokin osa systeemiä suunnitellaan käyttämään tiettyä toiminnallisuutta. Tätä "tiettyä toiminnallisuutta" käytetään systeemin sisällä, esim. $x->something($a, $b).
$x toteuttaa tämän tietyn toiminnallisuuden, joten voit olla varma että koodissa voidaan kutsua something() -metodia.
Tässä voit myös korvata $x:n jollain täysin muulla (mikä toteuttaa saman interfacen) ja sinun ei tarvitse muuttaa systeemiäsi pätkääkään, sama $x->something($a, $b) toimii edelleen.
Itse perustan interfacejen päälle hyvin paljon, konkreettisena esimerkkinä:
https://github.com/timoh6/TCrypto/blob/master/
(eli kaikessa yksinkertaisuudessa tavara viedään interfacejen pohjalta sisään, ja käytetään sen jälkeen sen mukaan mitä mikäkin interface määrittää).
Interfacet sekä typehinting, niin avot.
Interfacejen eli rajapintojen suurin hyöty tulee esille silloin, kun halutaan tehdä luokista tai, abstraktimmalla tasolla, komponenteista mahdollisimman riippumattomia. Rajapinnoilla voidaan siis määritellä ns. käytäntö tai protokolla, minkä avulla eri luokkien tai komponenttejen palveluita voidaan kutsua. Rajapinta ei määrää sitä, että miten varsinainen palvelu on toteutettu eli se ottaa kantaa vain ns. "käyttöliittymään". Interfacejen suurimmat hyödyt tulee esille etenkin DI -suunnittelu mallissa.
Muutama tyhmä kysymys... Vaikka esimerkki ja selitykset ovat varmasti hyvät, niin voiko joku yrittää vielä selittää muutaman asian, koska henkilökohtaisesti itse en vieläkään ymmärtänyt kaikkea.
Jos servicessä tehdään rajapinnan toteutus, kuten esimerkissä:
class personService implements iDataAccess { public function search($targetColumn,$data) { //...SELECT * FROM addresses WHERE $targetColumn = $data } public function save($item) { // INSERT INTO addresses... } }
Niin miksi ei voi unohtaa rajapintaa ja tehdä suoraan:
class personService { public function search($targetColumn,$data) { //...SELECT * FROM addresses WHERE $targetColumn = $data } public function save($item) { // INSERT INTO addresses... } }
Itse en juuri tätä asiaa ymmärrä aloittelijana, että mitä arvoa se rajapinta tuo tähän vaikka timoh sitä tuossa selitti.
Request kirjoitti:
Muutama tyhmä kysymys... Vaikka esimerkki ja selitykset ovat varmasti hyvät, niin voiko joku yrittää vielä selittää muutaman asian, koska henkilökohtaisesti itse en vieläkään ymmärtänyt kaikkea.
Itse en juuri tätä asiaa ymmärrä aloittelijana, että mitä arvoa se rajapinta tuo tähän vaikka timoh sitä tuossa selitti.
Esimerkit ovatkin todella huonoja, joten niitä ei kannata hirveän tarkasti analysoida.
Request kirjoitti:
Niin miksi ei voi unohtaa rajapintaa ja tehdä suoraan:
Rajapinta tarvitaankin siinä vaiheessa kun halutaan abstraktoida silti kyetä luottamaan tietyn luokan rajapinnan signatuuriin. Rajapinnat kuten myös abstraktit luokat tarjoaavat myös hyvän tavan käsitellä luokan riippuvuuksia läpinäkyvästi.
Rajapintaa voi käyttää silloin, kun johonkin asiaan on eri ratkaisuja, jotka toimivat periaatteessa samalla tavalla (yhteinen rajapinta) mutta jotka pitää toteuttaa eri tavalla (erilliset luokat).
Esimerkiksi tietokantayhteydellä tehdään yleensä samoja asioita, joten se sopii rajapinnaksi. Tietokantoja on kuitenkin erilaisia, esimerkiksi MySQL ja SQLite, ja jokaista tietokantaa varten voi olla oma luokka. Rajapinnan ansiosta funktiossa tiedetään, että luokka täyttää tietyt vaatimukset, vaikka ei tiedettäisi, mikä luokka tarkalleen on kyseessä.
function hae_tietoa(Tietokantayhteys $t) { // Käytetään tietokantayhteyttä; ihan sama, mikä tietokanta on kyseessä. } interface Tietokantayhteys { // Rajapinta määrää, miten tietokantayhteyttä voi käyttää. } class MySQL_yhteys implements Tietokantayhteys { // Tämä luokka toteuttaa MySQL-tietokantaa käyttävän version rajapinnasta. } class SQLite_yhteys implements Tietokantayhteys { // Tämä luokka toteuttaa SQLite-tietokantaa käyttävän version rajapinnasta. }
Nyt sovelluksen asetuksista voitaisiin valita, käytetäänkö MySQL- vai SQLite-tietokantaa, ja funktiolle hae_tietoa voitaisiin antaa kumpi tahansa olio. Sen sijaan vääränlaisesta oliosta tulisi virheilmoitus.
$kanta1 = new MySQL_yhteys(); hae_tietoa($kanta1); // Ok, parametri on Tietokantayhteys. $kanta2 = new SQLite_yhteys(); hae_tietoa($kanta2); // Ok, parametri on Tietokantayhteys. $kanta3 = new SplStack(); hae_tietoa($kanta3); // Virhe! SplStack ei ole Tietokantayhteys.
On totta, että koodi toimisi ilmankin rajapintaa. Kuitenkin rajapinnasta on kolme hyötyä: Koodista näkee yksiselitteisesti, että funktiolle hae_tietoa pitää antaa Tietokantayhteys, joten ei ole mitenkään epäselvää, mikä parametri pitää antaa. Jos silti tulee annettua väärä parametri PHP:n virheilmoitus kertoo heti, mistä on kysymys (”Argument 1 – – must be instance of Tietokantayhteys”), sen sijaan, että tulisi jokin yleispätevämpi virheilmoitus (esim. ”Call to a member function – – on a non-object”). Luokan toteutuksessa taas on pakko toteuttaa kaikki rajapinnan metodit, tai PHP antaa virheilmoituksen.
interface Esiintyja { public function esiinny(); // Kaikkien esiintyjien on toteutettava } class Laulaja implements Esiintyja { public function esiinny() { print "LaLaLaLaLaLaaa"; } } class Soittaja implements Esiintyja { public function esiinny() { print "DanDanDaaDaaDaa"; } } class Konsertti { protected $kuoro = array(); public function lisaaEnsiintyja(Esiintyja $esiintyja) { // Tässä tehdään type hint rajapinnalle ei yksittäiselle toteutukselle $this->kuoro[] = $esiintyja; } public function konsertoi() { foreach($this->kuoro as $esiintyja) { $esiintyja->esiinny(); // Rajapinnan avulla voimme olla varmoja että kaikki esiintyjät voivat esiintyä } } } $konsertti = new Konsertti(); $konsertti->lisaaEnsiintyja(new Soittaja()); $konsertti->lisaaEnsiintyja(new Laulaja()); print $konsertti->konsertoi();
Eikös tuo qeijon esimerkki olisi ihan samoin toimiva, jos sen tekisi abstraktilla luokalla?
Joten kysymys on mikä ero abstraktilla luokkalla vs. interface? Tehkääpä esimerkki jossa se ero tulee selville.
ps. Kettuilevat C++-hemmot voisivat väittää että interface on olemassa vain sen takia että moniperintä puuttuu (C++:ssa on moniperintä).
pss. Jänskästi Go-kielessä ei ole perintää, eikä siten luokkahierarkiaa, vaan polymorfismi saavutetaan pelkällä interfacella. IMO tämä yksinkertaistus olisi loppujen lopuksi toivottava kehityssuunta, sillä luokkahierarkian saaminen "oikein" on usein aikamoista brainfuckia.
Minä meinasin kirjoittaa, että rajapinnat ovat olemassa, koska php ei salli luokkien periä kuin yhden toisen luokan. Ja niinhän se onkin. Toinen tapa kiertää perimisrajoitetta ovat php 5.4:ssä lisätyt traitit.
Olennainen käytännön ero abstraktilla luokalla ja rajapinnalla on tosiaan se, että PHP:ssä voi periä vain yhden luokan mutta rajapintoja voi toteuttaa monta. C++-hemmoille kettuilevat voisivat väittää, että C++ sisältää moniperinnän vain siksi, että siinä ei ole erikseen rajapintoja. C++-koodari on helposti pulassa moniperinnän kanssa, jos kahdessa luokassa on osittain päällekkäisiä toimintoja (samoja funktioita) tai jos luokilla on yhteinen kantaluokka, ks. the diamond problem.
Ideologisella tasolla abstrakti luokka antaa minusta ymmärtää, että luokkien toteutuksilla olisi yhteinen perusta: abstrakti luokka voi sisältää osan metodeista ja jättää vain osan abstrakteiksi. Näin siis abstrakti luokka on enemmän ohjelmoinnin työkalu; rajapinta on se varsinainen olio-ohjelmoinnin konsepti, josta abstrakteissa luokissakin on oikeastaan kysymys.
Javassa on pitkä perinne rajapinnoilla, joten jos esimerkit kiinnostavat, voi selailla Javan kirjastoja. Esimerkiksi MouseListener on rajapinta ja MouseAdapter on abstrakti luokka, jossa rajapinnan funktiot on toteutettu tyhjinä.
Nykyään pitäisin PHP:ssä monissa tapauksissa loogisempana rajapinnan ja traitin yhdistelmää kuin abstraktia luokkaa, koska usein abstrakteihin luokkiin tulee tungettua yhteisiä ”helppereitä”, joilla ei ole kuitenkaan sellaista peruuttamatonta merkitystä luokan kannalta, että ylimääräinen luokkahierarkian taso olisi vain niiden takia mielekäs.
Toisaalta myös ohjelmistosuunnittelussa käytettävän periaatteen mukaan "favor composition over inheritance" osoittaa, että tavanomaista perintää kannattaakin kenties välttää...
Ymmärtääkseni tuo Tritonin quote tarkoittaa sitä, että kun perimistä usein käytetään metodien ylikirjoittamiseen, niin se on lopulta tyhmä peruste. Sen sijaan pitäisi jakaa bloatit luokat pienempiin komponentteihin, joista kootaan tällainen isompi "pääluokka".
Kun toiminnallisuutta pitää muuttaa, niin kirjoitetaan se yksi komponentti uusiksi ja syötetään se pääluokalle, jolloin muuta koodia ei tarvitse muokata. Kuitenkin tällaisetkin komponentit joutuvat perimään jonkin abstraktin luokan tai interfacen, joten kyse ei ole suoraan perinnän korvaamisesta jollain toisella tekniikalla.
Metabolix kirjoitti:
C++-hemmoille kettuilevat voisivat väittää, että C++ sisältää moniperinnän vain siksi, että siinä ei ole erikseen rajapintoja. C++-koodari on helposti pulassa moniperinnän kanssa, jos kahdessa luokassa on osittain päällekkäisiä toimintoja (samoja funktioita) tai jos luokilla on yhteinen kantaluokka, ks. the diamond problem.
Tähän kettumainen C++-hemmo vastaisi, että myös yksinkertaisen perinnän kanssa voi joutua helposti pulaan: http://en.wikipedia.org/wiki/Circle-ellipse_problem
Tuon Go-kielen ja sen "only interfaces"- ratkaisun takana on muuten harvinaisen kova ryhmä: Robert Griesemer, Rob Pike ja Ken Thompson. Kyllä, se sama risuparta Ken Thompson, joka väsäsi Unixin ja vaikutti C-kielen syntyyn.
Tällä hetkellä näyttäisi että Go ei saa tarpeeksi ilmaa siipien alle, mutta tuo suunnitteluratkaisu pitäisi ainakin käydä tarkkaan läpi, että kuinka hyvin toimiva se on ja mitä mahdollisia ongelmia siitä seuraa.
Kuten tämäkin keskustelu omalta osaltaan todistaa, niin OO-paradigma selvästi leijuu tukevasti liian abstraktilla tasolla. Guruja ei varmaan haittaa, mutta ei-guruille toivoisi jotain simppelimpää tapaa ohjelmoida. Kun OO on jo pitkään ollut vahvasti valtavirtaa, niin pakko se on tietysti osata "vaikka tekisi kipeää".
Varmaan tiesitkin että C++:ssa rajapinta tehdään näin:
class RajapintaDemo { public: virtual void foo() = 0; virtual int bar(float) = 0; }
Rajapinta on siis C++:n termein luokka, jossa on vain ja ainoastaan aitoja virtuaalifunktioita. Tämä C++:n tapa ehkä auttaa hieman hahmottamaan mistä rajapinnoissa on kyse.
JaskaP kirjoitti:
Myös yksinkertaisen perinnän kanssa voi joutua helposti pulaan: http://en.wikipedia.org/wiki/Circle-ellipse_problem
Aika epärelevantti vastaus sikäli, että moniperintä ei mitenkään ratkaise tuota pulmaa (kun taas moniperinnän poistaminen tavallaan ratkaisee diamond-ongelman). Vai pitäisikö poistaa koko olio-ohjelmointi, kun siinä voi tulla virheitä? ;)
JaskaP kirjoitti:
Varmaan tiesitkin että – – Rajapinta on siis C++:n termein luokka, jossa on vain ja ainoastaan aitoja virtuaalifunktioita.
Varmaan tiesitkin, että silloin pitää muistaa virtual-sana myös perinnän yhteydessä, ettei tule ongelmia. Alla A on tehty oikein, B väärin (naiivisti) ja C väärin (sekoillen). Mukana on pari esimerkkiä seurauksista.
struct I {}; // rajapinta struct A1: virtual I {}; struct A2: virtual I {}; struct A: A1, A2 {}; struct B1: I {}; struct B2: I {}; struct B: B1, B2 {}; struct C: A1, B1 {}; #include <iostream> int main() { A a; B b; C c; std::cout << "A: " << (&(I&)(A1&)a == &(I&)(A2&)a ? 1 : 2) << " x I\n"; // 1 x I std::cout << "B: " << (&(I&)(B1&)b == &(I&)(B2&)b ? 1 : 2) << " x I\n"; // 2 x I std::cout << "C: " << (&(I&)(A1&)c == &(I&)(B1&)c ? 1 : 2) << " x I\n"; // 2 x I (I&) a; // ok (I&) b; // virhe: ”I” is an ambiguous base of ”B” (I&) c; // virhe: ”I” is an ambiguous base of ”C” }
PHP:ssä ongelma ei ole mahdollinen, vaan aina pätee tapaus A. Tämä estää joitain kummallisia luokkarakenteita, mutta en ole törmännyt vielä yhteenkään järkevään malliin, jossa saman kantaluokan toistaminen olisi tarpeen; yleensä silloin kantaluokan sijaan pitäisi käyttää jäsentä, jotta rakenne olisi järkevä ja jottei rikottaisi Tritonin mainitsemaa periaatetta ”favor composition over inheritance”.
Metabolix kirjoitti:
Aika epärelevantti vastaus sikäli, että moniperintä ei mitenkään ratkaise tuota pulmaa (kun taas moniperinnän poistaminen tavallaan ratkaisee diamond-ongelman).
Ei ollut tarkoitus ratkaista moniperinnällä pulmaa, vaan kettumainen C++-hemmo kuittasi takaisin että ongelmia on *myös* "yksin"perinnässä.
Metabolix kirjoitti:
Vai pitäisikö poistaa koko olio-ohjelmointi, kun siinä voi tulla virheitä? ;)
Ei, mutta joku vähemmän sekava olisi monen mielestä "kiva ylläri" ja sitähän tuossa Go-kielessä yritetään.
JaskaP kirjoitti:
Metabolix kirjoitti:
Aika epärelevantti vastaus sikäli, että moniperintä ei mitenkään ratkaise tuota pulmaa (kun taas moniperinnän poistaminen tavallaan ratkaisee diamond-ongelman).
Ei ollut tarkoitus ratkaista moniperinnällä pulmaa, vaan kettumainen C++-hemmo kuittasi takaisin että ongelmia on *myös* "yksin"perinnässä.
Aika epäedullinen kuittaus C++:n kannalta, kun C++ tukee moniperinnän ohella myös yksinperintää. Silloinhan C++ sisältää kaksin verroin ongelmia verrattuna PHP:hen, jossa on pelkästään yksinperinnän ongelmat. ”Turha lopettaa tupakointia, kun kuitenkin juon myös viinaa”, vai?
JaskaP kirjoitti:
Joku vähemmän sekava olisi monen mielestä "kiva ylläri" ja sitähän tuossa Go-kielessä yritetään.
Mikään ei estä noudattamasta Go-kielen tyyppistä ajattelua nykyisissä kielissä. Oikeastaan voisi sanoa, että PHP ”perinteiseen tapaan” eli ilman tyyppimääreitä kirjoitettuna toimiikin muuten ihan samalla tavalla, mutta kielen dynaamisen luonteen vuoksi virheilmoitukset eivät tule käännösvaiheessa vaan vasta silloin, kun ensimmäisen kerran kutsutaan olematonta metodia. Eräs mahdollinen C++-toteutus taas on sellainen, että kaikki funktiot ovat templaatteja.
Tavallaan Go menee kivaan suuntaan, mutta itse en välttämättä pidä siitä, miten idea on kielessä toteutettu.
Metabolix kirjoitti:
Aika epäedullinen kuittaus C++:n kannalta, kun C++ tukee moniperinnän ohella myös yksinperintää. Silloinhan C++ sisältää kaksin verroin ongelmia verrattuna PHP:hen, jossa on pelkästään yksinperinnän ongelmat. ”Turha lopettaa tupakointia, kun kuitenkin juon myös viinaa”, vai?
Tai että PHP-valtakunnassa on kaikki hyvin kun röökaaminen on kielletty, mutta armoton dokaaminen jatkuu.
Metabolix kirjoitti:
Mikään ei estä noudattamasta Go-kielen tyyppistä ajattelua nykyisissä kielissä.
No ei, mutta se olisi silti suurinpiirtein yhtä nerokasta kuin OO:n vääntäminen C:llä.
Metabolix kirjoitti:
Tavallaan Go menee kivaan suuntaan, mutta itse en välttämättä pidä siitä, miten idea on kielessä toteutettu.
Voitko tarkentaa?
JaskaP kirjoitti:
Metabolix kirjoitti:
Tavallaan Go menee kivaan suuntaan, mutta itse en välttämättä pidä siitä, miten idea on kielessä toteutettu.
Voitko tarkentaa?
Mielestäni on selvempää, että ohjelmoija ilmoittaa suoraan, mitä rajapintoja luokka toteuttaa. Tuntuu kummalliselta, että vain kirjoitetaan kasa funktioita ja niistä maagisesti muodostuu minkä tahansa sopivan rajapinnan toteutus. Varmasti on tilanteita, joissa rajapinnat sisältävät aivan samat metodit mutta eivät ole keskenään vaihdannaiset, ja näissä tilanteissa Go sallii silti saman luokan käytön minkä tahansa rajapinnan kohdalla. Esimerkiksi jono ja pino muodostuvat klassisesti metodeista push ja pop, ja kuitenkin yhden vaihtaminen toiseen ei usein käy päinsä.
Tietenkin ongelman voi kiertää joko koodaamalla huolellisesti tai vaihtamalla metodien nimiä. Oikeastaan kyse on siis vain siitä, että Go asettaa uusia vaatimuksia metodien nimeämiselle: yht'äkkiä pinon ja jonon perinteiset rajapinnat ovatkin yksi ja sama rajapinta ohjelmoijan tahdosta riippumatta, ja ainoa tapa erottaa ne on käyttää niissä erinimisiä metodeita.
Tämä on edelleenkin vain oma mielipiteeni selvästä koodista eikä mikään absoluuttinen Go-kritiikki tai periaatekysymys.
Itse näen luokan perimisen ja rajapinnan toteuttamisen välillä sen eron, että kun peritään jokin luokka, niin silloin näiden luokkien välille pitäisi muodostua hierarkinen ja looginen perimissuhde, kuten nyt vaikka Eläin - Kissa -luokkien välissä on. Rajapintojen tapauksessa en näe välttämätöntä syytä, että asioiden välillä pitäisi olla tällaista samanlaista loogista hierarkiaa vaan, että jos halutaan lisätä johonkin luokkaan tietynlaista toiminnallisuutta, niin sitten implementoidaan siihen vain tarvittava(t) interface(t).
Triton kirjoitti:
Itse näen luokan perimisen ja rajapinnan toteuttamisen välillä sen eron, että kun peritään jokin luokka, niin silloin näiden luokkien välille pitäisi muodostua hierarkinen ja looginen perimissuhde, kuten nyt vaikka Eläin - Kissa -luokkien välissä on. Rajapintojen tapauksessa en näe välttämätöntä syytä, että asioiden välillä pitäisi olla tällaista samanlaista loogista hierarkiaa vaan, että jos halutaan lisätä johonkin luokkaan tietynlaista toiminnallisuutta, niin sitten implementoidaan siihen vain tarvittava(t) interface(t).
Juuri näin, eli "is a" vs. "has behavior off" suhde.
Aihe on jo aika vanha, joten et voi enää vastata siihen.