ISP (Interface Segregation Principle) — a SOLID negyedik elve, amely kimondja: az kliensek nem függhetnek olyan metódusoktól, amelyeket nem használnak. Az elvet Robert Martin fogalmazta meg az objektumorientált rendszerek felületeinek tervezésével összefüggésben. Ahogy a Clean Architecture (2017) című könyv leírja, a felületek elkülönítésének elve szűken specializált felületek létrehozását követeli meg egy univerzális helyett, ami csökkenti a csatolást és egyszerűsíti a változtatások végrehajtását.
Főbb pontok
ISP (Interface Segregation Principle) — a felületek elkülönítésének elve, amely tiltja a „kövér” felületek létrehozását olyan metódusokkal, amelyeket nem minden kliens használ. Egy tíz metódusból álló felület helyett több kicsi felületet tervezünk, mindegyiket a saját klienscsoportjának.
Az elvet Robert Martin vezette be a „felületek szennyeződésének” problémájára megoldásként, amikor egy osztály kénytelen olyan metódusokat implementálni, amelyekre nincs szüksége, csak azért, mert azokat egy közös felületben deklarálták. Statikusan típusos nyelvekben ez üres implementációkhoz vagy kivételek dobásához vezet — az ISP megsértésének közvetlen jele.
ISP és SRP kiegészítik egymást: az SRP az osztály felelősségéről szól, az ISP a felületek szerződéseiről. Az SRP azt mondja: „egy osztály — egy ok a változásra”, az ISP azt mondja: „egy felület — egy kliens forgatókönyv”. Együtt moduláris architektúrát alkotnak, ahol a rendszer minden elemének világos határai vannak.
Fat Interface — olyan felület, amely több metódust tartalmaz, mint amennyire egy adott kliensnek szüksége van. Például a Worker felület work, eat, sleep metódusokkal. Egy robotmunkásnak nem kellene implementálnia az eat és sleep metódusokat, de kénytelen. Megoldás — bontás Workable, Eatable, Sleepable részekre. Minden kliens pontosan azt kapja, amire szüksége van.
A mobilfejlesztésben a kövér felületek a delegate és DataSource protokollokban fordulnak elő. Egy protokoll tartalmazhat metódusokat két különböző forgatókönyvhöz (szerkesztés + megjelenítés), bár egy adott képernyő csak az egyiket használja.
Az ISP megvalósítása az egyes felületek klienseinek elemzésével kezdődik. Ha két kliens ugyanazon felület különböző metóduskészleteit használja — a felületet fel kell bontani. Minden új felület azokat a metódusokat csoportosítja, amelyeket egy forgatókönyv keretében együtt hívnak meg.
A bontás mechanizmusa: az eredeti felületet több szűk felületre bontják, amelyek mindegyike örökli a közös részt (ha van). A kliensek átváltanak a szükséges szűk felülettől való függőségre az általános helyett. Az eredeti felületet implementáló osztályok most csak azokat a szűk felületeket implementálják, amelyekre valóban szükségük van.
Fontos pontosítás: a bontás mértékét a kliensek száma és forgatókönyveik határozzák meg. Az ISP nem követeli meg a maximális bontást (mikrofelületek egy metódussal). Ez túlzott komplexitáshoz vezetne. A cél a kliensek felesleges metódusoktól való függőségének megszüntetése, nem az egyes felületek méretének minimalizálása.
A fő jelek az ISP megsértésének a következők: osztályok, amelyek üres metódusokkal implementálnak egy felületet (fikciós implementáció), UnsupportedOperationException kivétel dobása az implementációkban, nagyszámú paraméter vagy visszatérési típus, amelyeket a kliensek egy része nem használ, és gyakori felületmódosítások, amelyek csak a kliensek egy részét érintik.
Az Android fejlesztésben az ISP megsértésének tipikus példája az OnItemClickListener felület, amely metódusokat tartalmaz kattintáshoz, hosszú kattintáshoz és húzáshoz. Ha egy adott képernyő csak kattintást használ — a többi metódus üres marad. Megoldás — bontás OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener részekre.
Az iOS fejlesztésben az ISP megsértése az UIKit delegáltakban nyilvánul meg: egy protokoll metódusokat tartalmaz egy komponens különböző állapotaihoz. Az UITableViewDelegate metódusokat tartalmaz megjelenítéshez, kijelöléshez, szerkesztéshez és swipe-műveletekhez. Gyakran a fejlesztők a teljes protokollt implementálják tíz üres metódussal. A felelősségi csoportok szerinti több protokollra bontás megoldja a problémát.
A probléma nem csak a kód esztétikájában rejlik. Amikor egy felület változik (új metódus kerül hozzáadásra), az összes implementáló osztályt frissíteni kell — még azokat is, amelyeknek nincs szükségük az új metódusra. A több tucat képernyővel rendelkező mobilfejlesztésben ez lépcsőzetes változásokhoz vezet. Az ISP elkülöníti minden klienst a rájuk nem tartozó változásoktól.
Az ISP hallgatólagos megsértése konfigurációs paramétereken keresztül történik. Ha egy metódus nagy számú mezővel rendelkező objektumot fogad, miközben a kliens csak 2-3-at használ belőlük — ez bontásra utaló jel. Alternatíva: több specializált metódus minimális paraméterkészlettel.
Az Android fejlesztésben az ISP megsértődik, amikor egyetlen SharedPreferencesManager-t használnak az alkalmazás összes beállításának olvasására és írására. Egy Fragment, amelynek csak a témát kell beolvasnia, függőséget kap a globális menedzsertől, amely több tucat metódussal rendelkezik különböző adattípusokhoz. Bontás ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider részekre — az ISP alkalmazása a konfigurációs szolgáltatások szintjén. Minden szolgáltató pontosan azokat a metódusokat tartalmazza, amelyekre a klienseinek szüksége van.
Vizsgáljuk meg az Android példát egy adatkezelő felülettel. Az ISP megsértése — egy felület az összes CRUD művelethez, bár nem minden kliensnek van szüksége az összes műveletre.
// ISP megsértése: kövér felület
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// Az ISP alkalmazása után: szűk felületek
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// A ReadOnlyViewModel nem függ az írási metódusoktól
class ReadOnlyViewModel(
private val reader: UserReader
)
iOS példa a protokollok bontásával a média kezeléséhez:
// ISP megsértése: egy protokoll a teljes média kezeléshez
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// ISP után: bontás protokollokra felelősség szerint
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// A PlayerViewModel nem függ a betöltési metódusoktól
class PlayerViewModel {
private let player: MediaPlayer
}
Gyakorlati következtetés: Az ISP megvédi a klienseket a felület tőlük független részeiben bekövetkező változásoktól. A UserRepository felbontása UserReader és UserWriter részekre azt jelenti, hogy a save változásai nem érintik a ReadOnlyViewModel-et, és fordítva. Minden kliens el van különítve az általa nem használt funkcióktól, és nem igényel változtatásokat a rendszer más részeinek továbbfejlesztésekor.
ISP és SRP — természetes pár. Az SRP meghatározza, hogy egy osztálynak egy oka legyen a változásra. Az ISP ugyanezt a logikát alkalmazza a felületekre: egy felületnek egy kliens forgatókönyvet kell kiszolgálnia. Egy osztály több szűk felületet implementálhat (mindegyik egy felelősségnek felel meg), ami tisztább, mint egy kövér felület több felelősséggel.
ISP és OCP szintén kapcsolódnak: a szűk felületeket könnyebb bővíteni. Új metódus hozzáadása egy szűk felülethez csak annak klienseit érinti. Metódus hozzáadása egy kövér felülethez az összes klienst érinti — potenciálisan megsértve az OCP-t, ha a kliensek kénytelenek megváltoztatni az implementációjukat.
ISP és DIP együtt működnek: a DIP az absztrakcióktól való függőséget követeli meg. Az ISP ezeket az absztrakciókat szűkké és fókuszáltá teszi. A széles felülettől való függőség továbbra is absztrakciótól való függőség, de „rossz” absztrakció az ISP szempontjából. Négy elv (SRP, OCP, ISP, DIP) alkotja a „modularitás piramisát”: az SRP és ISP határozza meg a határokat, az OCP és DIP — a bővítés és kapcsolódás módjait.
Komponens architektúra a mobil projektekben (modulok, funkciók, rétegek) profitál az ISP-ből a nyilvános API-k szintjén. Minden modul szűk felületeket exportál a fogyasztói számára, nem egy közös homlokzatot. Ez lehetővé teszi a modul belső implementációjának megváltoztatását anélkül, hogy befolyásolná a fogyasztókat, akik csak a funkcióinak egy részét használják.
A Clean Architecture-vel rendelkező Android projektekben az ISP a UseCase-re alkalmazódik: minden UseCase egy külön felület egyetlen invoke vagy execute metódussal. A kliens (ViewModel) csak attól a UseCase-től függ, amelyre szüksége van, nem a teljes adattárolótól. Ez átláthatóvá és tesztelhetővé teszi a függőségeket.
Gyakran Ismételt Kérdések
Igen, a túlzott bontás lehetséges. Az ISP nem követeli meg egy felület létrehozását minden metódushoz. Kritériuma: van-e olyan kliens, amelyik csak a felület metódusainak egy részére szorul? Ha az összes kliens az összes metódust használja — a felületet nem kell bontani. Az optimális bontási szintet a valós használati forgatókönyvek határozzák meg.
Az ISP paraméter szinten azt jelenti: egy függvény nem fogadhat nagy számú mezővel rendelkező objektumokat, ha csak azok egy részét használja. Ehelyett csak a szükséges adatokat kell átadni, vagy specializált felületeket kell használni (például a Renderable felületet a teljes User helyett).
Az LSP a helyes öröklésről és altípusok viselkedési kompatibilitásáról szól. Az ISP a felületek tervezéséről szól: a kliensek nem függhetnek olyan metódusoktól, amelyeket nem használnak. Az LSP arra a kérdésre válaszol, hogy „használható-e egy alosztály az alaposztály helyett?”, az ISP pedig arra, hogy „szüksége van-e a kliensnek a teljes felületre?”.
A szűk felületek egyszerűsítik a mock objektumok létrehozását: a teszt egy vagy két metódussal rendelkező mock-ot hoz létre, nem tízzel. Minél kevesebb metódus van egy felületben, annál könnyebb szimulálni a viselkedését. Ez csökkenti a tesztfejlesztő kognitív terhelését és csökkenti a mock logikában előforduló hibák valószínűségét.
Ha a felület stabil és az összes kliens az összes metódust használja — a bontás felesleges. Tipikus példa: az Apple által tervezett UIKit protokollok. Ezek bontása kockázatos, mert az UIKit a delegált teljes implementációját várja. Ilyen esetekben az ISP megsértése az API stabilitásával indokolható.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is