Tacsiazuma
Tacsiazuma A letscode.hu alapitója, több, mint egy évtized fejlesztői tapasztalattal. Neovim függő hobbi pilóta.

ISP – Interface segregation principle

ISP – Interface segregation principle

Amikor új applikáció tervezésébe kezdünk, akkor nem árt komolyan fontolóra venni, hogy milyen absztrakciókat is fogunk használni az egyes modulok és submodulok esetén. Ha ezen modulok egyike esetünkben egy osztály, akkor az absztrakció formája nem lesz más, mint egy interface. Tegyük fel, hogy ezt létrehoztuk és az adott osztályt implementáltuk is, minden klappol, megkaptuk érte a kis pénzünket és nem utolsó sorban úgy működik, ahogy a megrendelő elképzelte elképzeltük.
refactoring-to-solid-code-32-728

Akkor most jön az, hogy a kedves megrendelő szeretné, ha az adott modul működne.. egy kicsit másképp, hogy valamivel pontosabbak legyünk, egy lightweight verzió kellene neki egy másik mikroframework-be, ahol nem szükséges rommákonfigurálhatóvá tenni a működést.

Mi sem egyszerűbb? Egy kis copy paste és bumm cicomázás és máris kész. Azt már tudjuk, hogy mivel ez egy egyszerűbb verzió lesz, ezért az öröklődés nem jöhet szóba (mert akkor valószínűleg megsértenénk az LSP-t, viszont azt az interfészt használhatnánk, nemde? Mindig mindenki azt mondta, hogy az interfészek használata jó dolog. Az is. Lévén az adott osztály működéséhez szükséges összes publikus metódust felvettük abba a bizonyos interfészbe, ezért itt meg is kell azokat valósítani. De a gond ott kezdődik, hogy a metódusok felét itt nem tudjuk értelmezni, hiszen ez volt a kérés, hogy egy lebutított verzió kell.

Mit tegyünk ilyenkor?

Tegye fel a kezét az, aki szerint a helyes megoldás az lenne, ha üres method body-val vagy egy NotImplementedException-el oldjuk meg a dolgot? Utána pedig üljön le és gondolkozzon el az élet értelmén, mert ez nem a jó megoldás.

Amikor így teleszemeteljük az interfészt, azt fat, vagy polluted inteface-ként szokás emlegetni. A fenti megoldás jó esetben váratlan exceptionöket, rosszabb esetben pedig hosszú napokkal eltöltött álmatlan debuggolást eredményez és szerintem senki se kíván magának hasonlót. Akkor jöjjön újra a kérdés:

Mit tegyünk ilyenkor?

Ahogy az OO nagyatyai mondanák, szétcsapassuk’! Legalábbis kocsmanyelven így hangzana, ellenben a szakma tolvajnyelvén kicsit hosszabban:

Az ISP elv szerint a klienseknek ne kelljen implementálni olyan interfészeket, amiket nem használnak (ki). Az ilyen nagy interfészek helyett használjanak inkább apró interfészeket, metódusaik csoportosítva, mindegyik egy-egy alrendszert szolgálva.

Na de mit is jelent ez a gyakorlatban? Nézzünk rá valami példát, ami által remélhetőleg megmarad majd valami.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Controller {
 
 public function setServiceLocator(ServiceManager $manager);
 public function getServiceLocator();
 public function onApplicationContextLoaded(Event $event);
 public function onRequestDispatched(Event $event);
 public function getRouter();
 public function setRouter(Router $router);
 public function setPluginManager(PluginManager $manager);
 public function getPluginManager();
 public function __call($method, $parameters);
}

abstract class AbstractController implements Controller {
    // tök jó, ha abstract akkor meg se kell valósítanunk azt, how cool is that?
}

class HomeController extends AbstractController {
  // itt viszont már meg kell valósítanunk mindent ami az interfészben ki van kötve
}

A fentiekben deklaráltunk egy jó nagy böszme interfészt, amin a kontrollerünkben szükséges metódusok vannak felsorolva. A servicelocator beinjektálására és kikérésére, két eseményre is feliratkozhatunk elviekben, a routert is be lehet állítani, majd kikérni, valamint van itt egy pluginmanager is, ami lévén a nem létező metódusokat a __call segítségével át tudjuk passzolni egy pluginnek. Fasza, mi? Ilyet ha lehet ne használjunk, mert az életbe nem tudjuk majd kibogarászni a runtime ráaggatott “plugin” metódusokat, kész agyrém… Tegyük fel, hogy mindezt meg is valósítjuk az abstract controllerünkben, utána már csak le kell azt örökíteni és minden megy magától. Viszont mi van akkor, ha én akarok valahova egy ilyen pluginmanageres mókát, mert igazán szeretek szívni? Legyen ez valami repository, amire akarok “akasztani” egy loggert ilyen módon. Induljunk el a legszörnyűbb úttól a legjobb felé.

1
2
3
4
5
6
7
8
9
10
interface Repository extends Controller { // a fenti controller interfészt kibővítjük
 public function save(Model $model);
 public function find($id);
 public function findAll();
 public function update(Model $model);
}

class RucsokRepository implements Repository {
  // megvalósíthatjuk az egészet...
}

Na ez az a megoldás, amiért kevesebbért is perl script általi halálra ítélnek egyeseket. Kibővítjük az iménti böszme interfészt még több metódussal, amiket kivételesen még használunk is, hogy utána a megvalósítandó osztályban már csak egy hatalmas interfészt kelljen implementálni. No sir, no.

A következő kevésbé csúnya megoldás, ha egy fokkal jobban szétnyessük őket:

1
2
3
4
5
6
7
8
9
10
interface Repository {
 public function save(Model $model);
 public function find($id);
 public function findAll();
 public function update(Model $model);
}

class RucsokRepository implements Repository, Controller {
  
}

Na itt már nem bővítettük ki az eredeti interfészt, hanem két interfészt implementálunk, viszont ez megint 13 metódus összesen, amiből mi 7-et akarunk használni mindösszesen. Akkor jöjjön az, hogy szétbontjuk az eredeti interfészt, mégpedig az összetartozó metódusok alapján:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Pluginable {
 public function setPluginManager(PluginManager $manager);
 public function getPluginManager();
 public function __call($method, $parameters);
}

interface ServiceLocatorAware {
 public function setServiceLocator(ServiceManager $manager);
 public function getServiceLocator();
}

interface ApplicationEventListener {
 public function onApplicationContextLoaded(Event $event);
 public function onRequestDispatched(Event $event);
}

interface RouterAware {
 public function getRouter();
 public function setRouter(Router $router);
}

Akkor nézzük mi is történt az imént. Csináltunk egy Pluginable interfészt, amivel a pluginmanagert beállíthatjuk, kikérhetjük és a __call megvalósítását megköveteljük. Aztán jön még a ServiceLocatorAware, amivel a servicelocatort tudjuk beállítani és kikérni. Az ApplicationEventListener, amivel feliratkozhatunk eseményekre, valamint a RouterAware, amin át a Routert tudjuk beinjektálni és kikérni. Ezután az iménti AbstractController így néz ki:

1
2
3
4
5
6
7
8
9
10
abstract class AbstractController implements Pluginable, 
 ServiceLocatorAware, 
 RouterAware, 
 ApplicationEventListener {
 
}

class RucsokRepository implements Repository, Pluginable {
  // csak 7 metódust kell megvalósítani, semmi sallang meg NotImplementedException
}

Igen, több interfész is meg van valósítva, de mostmár a fentieket tudjuk darabolni. Ha valahol akarunk eseményekre feliratkozni, nem kell berántanunk egy hatalmas interfészt, aztán telerakni üres metódusokkal, hogy a fordító ne szóljon be.

Pedig ez utóbbi elég gyakori eset.

Vegyük Pistikét. Pistike leszarja az iménti szentbeszédet és az első megoldás szerint jár el. Tegyük fel, hogy a kedves olvasó valamilyen szörnyű balszerencse lévén együtt kell dolgozzék alulírott Pistikével. Találkozik hát az alábbi kódrészlettel:

1
2
/** @var Repository $repository */
private $repository;

Ezek utána a kedves IDE bizony felajánl bruttó 13 metódust. Nyomogatjuk a nyilat lefele és szemünk elé tárul az a bizonyos getServiceLocator, hogy itt biza elérjük az alkalmazás IoC containerét! Tegyük fel, hogy olyan kontextusban vagyunk, ahol nem elérhető a konténer, de tudjuk, hogy van benne egy Logger, amin keresztül szeretnénk kilogolni valamit, csak Demeter meg ne lássa.

1
2
// valamiféle kontext
$this->repository->getServiceLocator()->get("Logger")->info("OO is dead!");

Na amint ezt a rondaságot lefuttatjuk (később lesz szó Demeter törvényéről, aminek a fenti kód nagyon nem felel meg), akkor bizony csúnyán csecsre fut a dolog, mivel azt mondja majd a hibaüzenet, hogy bizony mi null értéken hívtunk metódust. Mi a fene? Hát ennek egy ServiceLocatort kellene visszaadnia. Áhh, bizonyára nem lett megfelelően inicializálva. Nyomunk egy Go to Definition-t az IDE-ben és instant bontunk egy sört a nagy izgalomra, ugyanis ott ez áll:

1
2
3
public function getServiceLocator() {
   // @TODO Automatically generated method stub
}

Aztán végiggörgetünk az osztályon és találunk másik 5 ilyen metódust, amiknek a megvalósítása egy kommentben merül ki. Ez az a pont, ahol a legtöbben a tettlegességig jutnak és programozok közt a második leggyakoribb halálok a perl után.

Remélem mindenki megértette miért is ajánlott szétdarabolni az interfészeket és csak azokat implementálni, amiket tényleg meg is valósítunk 🙂874c939233c387ca1a1aa84b727178c7585f1a8afbf385ed8484428695fa95ba

comments powered by Disqus