- A SOLID alapelvek öt objektumorientált tervezési irányelvet határoznak meg, amelyek javítják a kód érthetőségét, bővíthetőségét és tesztelhetőségét.
- Alkalmazzon SRP-t, OCP-t, LSP-t, ISP-t és DIP-t Piton Magában foglalja a felelősségek szétválasztását, az absztrakciókra való támaszkodást és koherens hierarchiák kialakítását.
- A SOLID tervet a DRY, KISS és YAGNI módszerekkel kombinálva, a projekt valós kontextusához igazított, karbantartható rendszerek jönnek létre túltervezés nélkül.

A következő sorokban részletesen megvizsgáljuk, hogy mi is az a SOLID, honnan származik, Mire használják a mindennapi Pythonban, és hogyan lehet valós példákon keresztül alkalmazni? ezen a nyelven. Ezenkívül integrálni fogunk más kulcsfontosságú elveket is, mint például a DRY, a KISS és a YAGNI, és megvitatjuk, hogy mely kontextusokban nem érdemes a SOLID-dal ultrapurista módon bánni.
Mi a SOLID és honnan származik?
Amikor SOLID-ról beszélünk, akkor arra gondolunk, hogy Az objektumorientált tervezés öt klasszikus alapelve amelyek arra összpontosítanak, hogyan kell strukturálni az osztályokat, modulokat és interfészeket, hogy a szoftverek könnyen bővíthetők, tesztelhetők és karbantarthatók legyenek.
Ezek az elvek népszerűvé váltak a következőknek köszönhetően: Robert C. Martin (Bob bácsi), az agilis fejlesztés egyik atyja és olyan könyvek szerzője, mint Tiszta kód o Tiszta építészetAz 90-es években és a 2000-es évek elején számos cikket publikált az objektumorientált tervezésről, többek között „Az objektumorientált tervezés alapelvei” és a „Tervezési alapelvek és tervezési minták” címűeket.
Később Michael Feathers mérnök javasolta a betűszót SZILÁRD hogy az ezekből a munkákból kirajzolódó öt legfontosabb alapelvre hivatkozzak. Az ötlet az volt, hogy legyen egy egyszerű mnemonikus szabály amire bármelyik fejlesztő emlékezhet és alkalmazhat az osztálytervezésében.
A betűszó a következőket jelenti:
- S - Egyetlen felelősség elve (SRP, Egységes Felelősség Elve).
- O - Nyitott-zárt elv (OCP, Nyitott/Zárt elv).
- L - Liskov-helyettesítési elv (LSP, Liskov-helyettesítési elv).
- I - Interfész szegregációs elve (ISP, interfész szegregációs elv).
- D - Függőség-inverzió elve (DIP, a függőség megfordításának elve).
Bár eredetileg erősen típusos nyelvekre tervezték őket, mint például Java vagy C#Ezek az elvek tökéletesen alkalmazhatók Python, C++, PHP és általában minden olyan nyelvre, amely támogatja az objektumorientáltságot.

Mire használják a SOLID alapelveket a valós projektekben?
Az elméleten túl a SOLID különösen akkor észrevehető, amikor a projekt növekszik és Több fejlesztő is dolgozik ugyanazon a kódon hónapokig vagy évekig.Ezen elvek alkalmazásának számos egyértelmű előnye van:
Egyrészt javítják a az architektúra olvashatósága és egyértelműségeAz osztályoknak meghatározott felelősségi köreik vannak, a függőségek szabályozottak, és könnyebb követni az üzleti folyamatot anélkül, hogy elvesznénk az infrastrukturális részletekben.
Továbbá előnyben részesítik a újrafelhasználhatóság és bővíthetőség a kódból. Ha az osztályok jól vannak megtervezve, az új funkciók hozzáadása nem jelenti a rendszer felének lebontását. A tendencia az, hogy új implementációkkal bővítik a rendszert, ahelyett, hogy újraírnák azt, ami már működik és tesztelve van.
Egy másik kulcsfontosságú pont az tesztelhetőségA csatolás csökkentésével és az absztrakciókra való támaszkodással egyszerűbbé válik. dupla teszt injekciója (próbaszerű tesztek, csonkok) és írjon gyors egységteszteket, amelyek nem függnek a következőktől: adatbázisok valós, hálózati hívások vagy más külső rendszerek.
Végül, egy SOLID elveket követő kódbázis kevésbé szenved a következőktől: kód szaga, kód rothadása és „spagetti kód”Más szóval, kevésbé bomlik le El TiempoKevesebb technikai adósságot halmoz fel, és biztonságosabb, stabilabb kritikus környezetekben.
S – Egységes felelősség elve Pythonban
Az egységes felelősség elve kimondja, hogy Egy osztálynak egyetlen oka kell legyen a változtatásra.Ez nem azt jelenti, hogy egyetlen mikroszkopikus dolgot csinál, hanem azt, hogy egy jól meghatározott funkcionális területért felelős.
Amikor egy osztály ötvözi az üzleti logikát, az adathozzáférést, a jelentésformázást és a megjelenítési logikát, akkor ezeken a területeken minden változtatás ugyanazon kódrészlet módosítását igényli. Ez növeli a kockázatát, hogy kereszthibák és a csapatok közötti konfliktusok.
Gondolj például egy osztályra használó Pythonban ez az osztály a felhasználó ábrázolása mellett felelős az adatbázishoz való csatlakozásért, a felhasználói adatok mentéséért és a szöveges jelentések generálásáért is. Ennek az osztálynak a módosítására több okból is szükség lehet: ha a felhasználó attribútumai megváltoznak, ha az adatbázis megváltozik, vagy ha a jelentés formátuma módosul.
Az SRP-vel összhangban lévő alternatíva a felelősségek szétválasztása: egy osztály vagy adatosztály a domain modell a felhasználónak, egy másik a kitartás (tárház vagy átjáró) és egy másik a jelentések vagy nézetek generálásaMinden változás a megfelelő részre esik, csökkentve a csatolást.
Valami hasonlót láthatunk egy klasszikus Python példában: egy osztály Kacsa amely meghatározza a kacsát, és egyúttal egy metóduson keresztül kezeli a kacsák közötti párbeszédeket greet()Ez a kommunikációs rész kinyerhető egy objektumba Communicatorígy a kacsa csak repülni, úszni és hangot kiadni tud, a kommunikátor pedig csak azt, hogyan kell párbeszédet lebonyolítani két madár között.
Ez a megközelítés nagyon jól illeszkedik más elvekhez, mint például DRY (a logika ismétlésének elkerülése több osztályban) és azzal a gondolattal, hogy fenntartsa a magas kohézió minden modulon belül: minden funkciója és attribútuma ugyanarra az üzleti felelősségre mutat.
O – Nyitott/Zárt elv viselkedésbővítésekre alkalmazva
A Nyitott/Zárt elv kimondja, hogy A szoftverentitásoknak nyitottnak kell lenniük a bővítésre, de zártnak a módosításraKöznapi értelemben: képesnek kell lennünk új viselkedési variációkat hozzáadni anélkül, hogy a már működő kódot szerkesztenünk kellene.
Egy tipikus anti-minta Pythonban, ha van egy osztályunk, amelynek egy metódusa egy if/elif óriás a típustól függően: például egy Területkalkulátor amely ellenőrzi, hogy az objektum Rectangle, Circle, Triangle és így tovább. Minden új geometriai alakzat egy újabb ág hozzáadását igényli a metódushoz, és egy már tesztelt osztály módosítását igényli.
Az OCP tiszteletben tartása magában foglalja programozás absztrakciók ellenEbben az esetben egy absztrakt osztályt definiálunk. Shape egy módszerrel area() és minden alakzatot (téglalap, kör, háromszög…) implementálunk ezzel a metódussal. A területkalkulátor egyszerűen meghívja a shape.area() anélkül, hogy tudná, konkrétan milyen típussal néz szembe.
A Pythonnal folytatva, egy másik példa, amelyet különböző források tárgyalnak, egy objektumé. Communicator amelyet madarak közötti beszélgetésekhez használnak. Ha a beszélgetés különböző formáit (egyszerű, formális, emojikkal stb.) szeretnénk támogatni, a módszer módosítása helyett communicate() minden alkalommal definiálunk egy beszélgetés absztrakció (például AbstractConversation) és olyan implementációkat készítettünk, mint például SimpleConversation amelyek generálják a kifejezéseket. A kommunikátor egyszerűen kinyomtatja a beszélgetés visszaadott eredményt, anélkül, hogy módosítaná a saját kódját.
Az üzleti világban az OCP kulcsfontosságú a kockázatok csökkentése érdekében a változtatások bevezetésekor: elkerüli a széles körben használt alaposztályok újbóli megnyitását, és elősegíti... kiterjesztés új alosztályokon vagy stratégiákon keresztül, a stabil kódot a lehető legsértetlenebbül tartva.
L – Liskov helyettesítési elve és helyes öröklődés
A Liskov-féle helyettesítési elv kimondja, hogy Egy alosztály objektumainak képesnek kell lenniük a szuperosztály objektumainak helyettesítésére a rendszer felborítása nélkül.Más szóval, ha egy kód az alaposztály egy példányát várja, akkor ugyanúgy kell működnie, ha egy tetszőleges gyermekosztály példányát adjuk meg neki.
A gyakorlatban ez azt jelenti, hogy az alosztálynak nem szabad megszegi az elvárásokat vagy a szerződést amely meghatározza az alaposztályt: a visszatérési típusoknak, kivételeknek, előfeltételeknek és utófeltételeknek konzisztenseknek kell maradniuk.
Egy jól ismert (és megtévesztő) példa a hierarchia Téglalap / NégyzetMatematikailag a négyzet egy téglalap, de kódban ez nem mindig kényelmes. Square közvetlenül örökölt RectangleHa a téglalapnak külön beállítói vannak a magassághoz és a szélességhez, egy tesztelési módszer feltételezheti, hogy csak a magasság megváltoztatása nem változtatja meg a szélességet. Ha a négyzet osztály mindkettő egyidejű megváltoztatására kényszeríti, a szuperosztály szerződése hirtelen felmondja a szolgálatot.
Valami hasonló történik a hierarchiákban, mint például Madár Pythonban. Ha az alaposztály tartalmaz egy metódust fly()De aztán vannak alosztályaink, mint például Ostrich vagy repülni nem tudó pingvinek, erőltesd a megvalósítást és indíts el egy NotImplementedError megszakítja az LSP-t. Bármely függvény, amely egy Bird és hívjon fly() Meglepetésmentesen kellene tudnom csinálni.
A megoldás a hierarchia újratervezését foglalja magában: egy általános alaposztály marad meg Madár a módszer nélkül fly(), és közbenső alosztályokat hoznak létre, például RepülőMadár y ÚszásMadárA repülő madarak az elsőt valósítják meg; az úszók a másodikat; és azok, amelyek mindkettőt képesek használni, mindkettőből örökölnek. Ily módon egyetlen alosztály sincs arra kényszerítve, hogy olyan viselkedést kínáljon, amelynek nincs értelme számára.
Az LSP tiszteletben tartása a Pythonban az öröklési kapcsolatok gondos mérlegelését igényli, és sok esetben előnyben részesítéshez vezet. egyszerű kompozíció és interfészek mély öröklődési fák esetén, ahol könnyű megszegni a szuperosztály szerződését.
I – Felület szegregációs elv és konkrétabb osztályok
Az interfész szegregációs elve kimondja, hogy Nem szabad arra kényszerítenünk az ügyfelet, hogy olyan módszerekre támaszkodjon, amelyeket nem használ.A gyakorlatban ez azt jelenti, hogy kis, specifikus interfészeket (vagy absztrakt osztályokat Pythonban) tervezünk egyetlen óriási interfész helyett, amely mindent tartalmaz.
Képzelj el egy interfészt Munkás Pythonban, amely metódusokat definiál work() y eat()Ez a felület egy dolgozó és evő ember számára értelmes, de egy csak dolgozó robot számára nem. Ha kikényszerítjük az osztályt Robot megvalósít eat(), egy üres metódussal vagy kivétellel fog végződni, jelezve, hogy „a robotok „Nem esznek”, ami egyértelműen az internetszolgáltató szabályainak megsértésére utal.
A megoldás magában foglalja az interfész felosztását Megmunkálható (valami, ami működhet) és Ehető (valami, amit megehetsz). Így az osztály Human mindkettőt megvalósítja, miközben Robot Csak a munka részét valósítja meg. Minden ügyfél csak azokra a metódusokra támaszkodik, amelyekre ténylegesen szüksége van.
Visszatérve a madarakra, valami hasonló történik egy olyan felülettel, amely olyan metódusokat kombinál, mint például fly() y swim()Nem minden madár teszi mindkét dolgot, ezért logikusabb szétválasztani őket RepülőMadár y ÚszásMadárAz így létrejövő terv rugalmasabb, és elkerülhető, hogy a lényegtelen metódusokat próbakóddal kelljen kitölteni.
Pythonban, bár nincsenek formális interfészeink, mint a Java-ban, ugyanazt a hatást érhetjük el a következő használatával: a modul absztrakt osztályai abc vagy gépelési protokollok. Az interfészek elkülönítése minden osztályt pontosabban definiál, és csökkenti a szükségtelen függőségek bevezetésének valószínűségét.
D – Függőségi inverzió elve és absztrakció-alapú tervezés
A függőség-megfordítás elve két fő gondolatot emel ki: A magas szintű moduloknak nem szabad az alacsony szintű moduloktól függeniük, És mindkettőnek absztrakciókon kell alapulniaTovábbá az absztrakcióknak nem szabad a részletektől függeniük; a részletek az absztrakcióktól függenek.
Egy nagyon gyakori hiba, hogy egy Pythonban található domain vagy üzleti logikai osztály közvetlenül egy adatbázis, HTTP szolgáltatás vagy üzenetküldő kliens konkrét példányát hozza létre: például egy Felhasználói adattár ami belsőleg csinálja self.database = MySQLDatabase()Ha holnap PostgreSQL-re vagy egy memórián belüli motorra szeretnénk váltani teszteléshez, akkor szerkesztenünk kell ezt az osztályt.
A DIP alkalmazása magában foglalja a következők bevezetését: adatbázis absztrakció (például egy absztrakt osztály) Database metódusokkal connect() y query()) és konkrét megvalósításokat hozzon létre, például MySQLDatabase o PostgreSQLDatabaseA repository leállítja az adatbázis példányosítását, és kívülről kezdi fogadni azt, általában a konstruktoron keresztül.
Ez a minta az úgynevezett függőség injekció És ez az egyik pillére a moduláris és tesztelhető kód elérésének: éles környezetben egy valódi implementációt injektálunk; tesztelés során egy teszt-duplát injektálunk, amely külső rendszerek érintése nélkül szimulálja a viselkedést.
Egy gazdagabb példában a kommunikációs csatorna absztrakcióját a következőképpen definiálhatjuk: AbsztraktCsatorna, és a kommunikátor absztrakciója, AbsztraktKommunikátor, aki együtt dolgozik AbsztraktBeszélgetésHelyette SMSCommunicator belsőleg létrehoz egy SMSChannelA DIP-hez igazított verzió biztosítja, hogy a kommunikátor egy absztrakt csatornát kapjon a konstruktorban. Ez azt jelenti, hogy nem egy konkrét osztálytól, hanem egy interfésztől függ, és bármilyen típusú csatornával képes együttműködni, amely teljesíti a szerződést.
Összefoglalva, a DIP olyan rétegek tervezését ösztönzi, ahol az üzleti logika általános interfészekre támaszkodik, és a keretrendszerek, konfigurációk vagy gyárak felelősek ezen interfészek összekapcsolásáért a környezettől függő konkrét implementációkkal.
Gyakorlati példa Pythonban: SOLID értesítési szolgáltatás
Egy nagyon szemléletes eset arra, hogyan kombinálódik több tervezési alapelv, egy értesítési rendszer amely különböző csatornákon, például e-mailben vagy SMS-ben képes üzeneteket küldeni, és amely könnyen bővíthető új eszközökkel (push, webhookok stb.).
A cél az, hogy meglegyen a fő logika, amely eldönti, hogy "értesítenem kell X dolgot". nem kell átírni minden alkalommal, amikor hozzáadunk egy csatornát, és hogy tesztelhetjük anélkül, hogy valódi e-maileket vagy SMS-eket aktiválnánk a tesztek során.
Ehhez definiálunk egy absztrakciót Értesítés egy módszerrel send(to, subject, body) amely egy logikai értéket ad vissza, amely a sikert vagy a sikertelenséget jelzi. Ebből kiindulva olyan implementációkat hoztunk létre, mint a Email-értesítő y SMS-értesítőmindegyik egy külső klienst fogad a konstruktorában (például smtp_client o sms_client) aki a királyi szolgálattal való beszélgetésért felelős.
Mindezek tetejébe egy Értesítési szolgáltatás amely fogadja az értesítők listáját (bármit, ami végrehajtja a szerződést), és módszert kínál a értesítést küldeni minden csatornán keresztülEz a szolgáltatás semmit sem tud az SMTP-ről, az SMS-szolgáltatókról vagy a HTTP API-król: csak iterál és hívásokat végez. send() minden egyes bejelentőről.
Ez a kialakítás egyértelműen bemutatja számos alapelv működését:
- SRPMinden osztálynak nagyon korlátozott a felelőssége: a
EmailNotifierCsak e-maileket kezel,SMSNotifierCsak az SMS, a zenekari szolgáltatás és a külső ügyfelek tudnak csak a szállításról. - DIP:
NotificationServiceAz absztrakciótól függNotifiernem meghatározott osztályokba tartoznak, az értesítők pedig a befecskendezett kliensektől függenek. - OCP: hozzáadni egy
PushNotifieroWebhookNotifierNem kell hozzányúlniNotificationServiceEgyszerűen hozzon létre egy új implementációt, és regisztrálja azt. - LSP és ISP: a következő összes implementációja
NotifierUgyanazt a szerződést tartják tiszteletbensend(), kötelező metódusok nélkül, amelyek egy adott csatornához nem logikusak.
Tesztelési szempontból elegendő egy vagy több létrehozása DummyNotifier amelyek rögzítik a hívásokat send() anélkül, hogy bármi mást tennénk. Azzal, hogy beillesztjük őket a szolgáltatásba, ellenőrizhetjük, hogy minden értesítés aktiválódott-e hálózatok, várólisták vagy külső rendszerek használata nélkül.
Ez a példa olyan elvekkel is kiegészíthető, mint például DRY (kerülje a közös formátumlogika ismétlését), KISS (ne vezessünk be újrapróbálkozásokat, sorba állítást vagy priorizálást, amíg az valóban nem szükséges) és YAGNI (Ne számítsunk olyan funkciókra, amelyek nem szerepelnek a közvetlen ütemtervünkben).
A SOLID kapcsolata más tervezési elvekkel
Bár a SOLID az objektumorientált tervezésre összpontosít, a gyakorlatban más általános szoftvertervezési elvekkel kombinálják, mint például a SZÁRAZ, CSÓK és YAGNIamelyek a tiszta kóddal foglalkozó irodalomban is visszatérően megjelennek.
SZÁRAZ (Ne ismételd magad) Ne feledjük, hogy a fontos üzleti logikának csak egyetlen reprezentációja lehet a rendszerben. Ez tökéletesen illeszkedik az SRP-hez és az OCP-hez: ha különböző osztályokban ismételjük a viselkedést, egy használati eset kiterjesztése vagy javítása sok helyen módosítást igényel, és növeli az inkonzisztenciák kockázatát.
KISS (Legyen egyszerű, hülye) Arra ösztönöz minket, hogy kerüljük a szükségtelenül bonyolult architektúrákat és absztrakciókat. Bár ellentmondásosnak tűnhet, a SOLID alkalmazása nem azt jelenti, hogy a projektet csak úgy feltöltjük interfészekkel, hanem csak annyit vezetünk be, amennyi a projekt működésének fenntartásához szükséges. érthető kialakítás a csapatért.
YAGNI (nem lesz rá szüksége) Ez a túltervezés ellenszere. A rendkívül változékony követelményekkel rendelkező projektekben kontraproduktív lehet megpróbálni előre látni az összes lehetséges jövőbeli változást rétegről rétegre történő hozzáadással. Az ideális megközelítés az egyensúly megtalálása: a SOLID-ot azokra a részekre alkalmazzuk, amelyekről tudjuk, hogy növekedni vagy változni fognak, és egyszerűbben tartjuk azokat, amelyekről tudjuk, hogy stabilak.
Összességében ezek az elvek egy olyan fejlesztési stílushoz vezetnek, ahol a prioritás a következő: világos, moduláris és együttműködés-orientált kódde anélkül, hogy az építészet magáért az építészetért való fetisizmusába esnénk.
Előnyök, korlátok és esetek, amikor a purista életmód nem kifizetődő
A SOLID-elveket szinte egy Aranyszabály közepes és nagy projektekbenDe ez nem jelenti azt, hogy minden kontextusban betű szerint kell alkalmazni őket. Fontos ismerni a korlátaikat is.
Nagyon kis alkalmazásokban, egyszeri szkriptekben vagy alacsony költségvetésű projektekben, amelyeket nem fognak hosszú távon fenntartani, a túl sok absztrakciós réteg bevezetése problémás lehet. drágább, mint amennyire hasznosA DIP vagy az ISP mindenre történő alkalmazásával járó mentális és kódolási többlet talán nem éri meg a fáradságot.
Vannak olyan helyzetek is, amikor nagyon szoros időkövetelmények vagy egy MVP fejlesztése, ahol a szállítás gyorsasága prioritás. Érdemes a SOLID finomításának egy részét későbbre halasztani, amikor a termék stabilizálódott, és tudjuk, mely részek fognak fennmaradni.
A sok régi rendszerben régi kódAz, hogy mindent egyszerre „megszilárdítsunk”, általában irreális. Ilyen esetekben jobb, ha az alapelveket fokozatosan alkalmazzuk a kód gyakran használt vagy az új funkciókat blokkoló területeire.
Végül vannak olyan területek, amelyek nagyon érzékenyek a következőkre: extrém teljesítmény (például bizonyos nagyfrekvenciás rutinok), ahol az absztrakciós rétegek nemkívánatos többletterhelést okozhatnak. Ezekben az esetekben néha indokolt a SOLID tisztaságának egy részét feláldozni nagyon specifikus blokkokban, feltéve, hogy a rendszer többi része jól megtervezett marad.
Összességében a SOLID alapelvek megértése és alkalmazása Pythonban lehetővé teszi, hogy a „csak úgy működő” kódról olyan kódra térjünk át, amely… Jól kiállja az idő próbájátJobban alkalmazkodik az üzleti változásokhoz, megkönnyíti a csapatmunkát és csökkenti a termelésben előforduló meglepetéseket; a kulcs az, hogy körültekintően használjuk őket, tudva, mikor érdemes bevezetni egy újabb absztrakciót, és mikor jobb egyszerűen és érthetően kezelni a dolgokat.
Szenvedélyes író a bájtok és általában a technológia világáról. Szeretem megosztani tudásomat írásban, és ezt fogom tenni ebben a blogban, megmutatom a legérdekesebb dolgokat a kütyükről, szoftverekről, hardverekről, technológiai trendekről stb. Célom, hogy egyszerű és szórakoztató módon segítsek eligazodni a digitális világban.
