- SOLID-principerna definierar fem objektorienterade designriktlinjer som förbättrar kodens tydlighet, utökningsbarhet och testbarhet.
- Tillämpa SRP, OCP, LSP, ISP och DIP i Python Det innebär att separera ansvarsområden, förlita sig på abstraktioner och utforma sammanhängande hierarkier.
- En SOLID-design kombineras med DRY, KISS och YAGNI för att uppnå underhållbara system utan överdesign, skräddarsydda för projektets verkliga sammanhang.
I följande rader ska vi titta närmare på vad SOLID är, dess ursprung, dess vardagliga användningsområden i Python och hur man tillämpar det med verkliga exempel. Vi kommer också att integrera andra viktiga principer som DRY, KISS och YAGNI, och diskutera sammanhang där strikt efterlevnad av SOLID kanske inte är nödvändig.
Vad är SOLID och var kommer det ifrån?
När vi pratar om SOLID syftar vi på fem klassiska objektorienterade designprinciper som fokuserar på hur man strukturerar klasser, moduler och gränssnitt så att programvara är enkel att utöka, testa och underhålla.
Dessa principer populariserades av Robert C. Martin (Uncle Bob) , en av pionjärerna inom agil utveckling och författare till böcker som Clean Code och Clean Architecture . På 90-talet och början av 2000-talet publicerade han flera artiklar om objektorienterad design, inklusive "The Principles of OOD" och "Design Principles and Design Patterns".
Senare föreslog ingenjören Michael Feathers förkortningen SOLID för att referera till de fem viktigaste principerna som framkom ur det arbetet. Tanken var att ha en enkel mnemonik som alla utvecklare kunde komma ihåg och tillämpa på sin klassdesign.
Förkortningen står för:
- S - Enkeltansvarsprincip (SRP, Principen om ensamt ansvar).
- O - Öppen-stängd princip (OCP, öppen/stängd princip).
- L - Liskovs substitutionsprincip (LSP, Liskovs substitutionsprincip).
- I - Gränssnittssegregationsprincip (ISP, Principen för gränssnittssegregering).
- D - Principen för beroendeinversion (DIP, Principen för beroendeomvändning).
Även om de ursprungligen formulerades med starkt typade språk som Java eller C# i åtanke , är dessa principer fullt tillämpliga på Python, C++, PHP och i allmänhet alla språk med objektorienterat stöd.

Vad används SOLID-principerna till i verkliga projekt?
Utöver teorin blir SOLIDs effektivitet tydligast när ett projekt växer och flera utvecklare arbetar med samma kod i månader eller år . Att tillämpa dessa principer har flera tydliga fördelar:
Å ena sidan förbättrar de arkitekturens läsbarhet och tydlighet : klasser har begränsade ansvarsområden, beroenden kontrolleras och det är lättare att följa affärsflödet utan att gå vilse i infrastrukturdetaljer.
Dessutom främjar de återanvändning och utökningsbarhet av kod . Om klasserna är väl utformade kräver det inte att man demonterar halva systemet för att lägga till nya funktioner. Utökningar tenderar att göras genom nya implementeringar snarare än att skriva om det som redan fungerar och är bevisat.
En annan viktig punkt är testbarhet . Genom att minska koppling och förlita sig på abstraktioner är det enklare att injicera testdubblar (mocktest, stubbtest) och skriva snabba enhetstester som inte är beroende av riktiga databaser , nätverksanrop eller andra externa system.

Slutligen lider en kodbas som följer SOLID-principerna mindre av kodlukt, kodröta och "spaghettikod ". Med andra ord, den försämras mindre över tid , ackumulerar mindre teknisk skuld och är säkrare och stabilare i kritiska miljöer.
S – Principen för enskilt ansvar i Python
Principen om enskilt ansvar säger att en klass ska ha en enda anledning att förändras . Detta betyder inte att den ska göra en mikroskopisk sak, utan snarare att den ska vara ansvarig för ett väldefinierat funktionsområde.
När en klass blandar affärslogik, dataåtkomst, rapportformatering och presentationslogik kräver varje ändring inom något av dessa områden att samma kodstycke modifieras. Detta ökar risken för tvärfunktionella fel och konflikter mellan team avsevärt.
Tänk dig till exempel en Python User- klass som, förutom att representera användaren, även ansvarar för att ansluta till databasen, spara deras data och generera textrapporter. Denna klass har många anledningar att ändras: om användarens attribut ändras, om vi ändrar databasen eller om rapportformatet ändras.
Alternativet i linje med SRP är att separera ansvarsområdena: en klass eller dataklass för användarens domänmodell , en annan för persistens (repository eller gateway) och en annan för att generera rapporter eller vyer . Varje ändring faller under motsvarande komponent, vilket minskar kopplingen.
Något liknande kan ses med ett klassiskt Python-exempel: en klass Anka som definierar ankan och samtidigt hanterar samtal mellan ankor genom en metod greet()Den kommunikationsdelen kan extraheras till ett objekt Meddelareså att ankan bara vet hur man flyger, simmar och gör ljud, och kommunikatören vet hur man formulerar en dialog mellan två fåglar.
Denna metod passar mycket väl ihop med andra principer som DRY (att inte upprepa logik i flera klasser) och med idén att upprätthålla hög sammanhållning inom varje modul: alla dess funktioner och attribut pekar mot samma affärsansvar.
O – Öppen/Stängd princip tillämpad på beteendetillägg
Öppen/stängd-principen anger att programvaruenheter ska vara öppna för utökning men stängda för modifiering . I praktiken betyder detta att vi ska kunna lägga till nya beteendevariationer utan att behöva redigera kod som redan fungerar.
Ett typiskt antimönster i Python är att ha en klass med en metod som bestämmer beteendet med en if/elif jätte beroende på typ: till exempel en Arearäknare som kontrollerar om objektet är Rectangle, Circle, Triangle och så vidare. Varje ny geometrisk form tvingar fram tillägg av ytterligare en gren till metoden och kräver modifiering av en klass som redan har testats.
Att respektera OCP innebär programmering mot abstraktionerI det här fallet definierar vi en abstrakt klass. Shape med en metod area() och vi får varje form (rektangel, cirkel, triangel…) att implementera den metoden. Areaberäknaren anropar helt enkelt shape.area() utan att veta vilken specifik typ han möter.
Fortsatt med Python, ett annat exempel som diskuteras i olika källor är ett objekt Meddelare vilket används för samtal mellan fåglar. Om vi vill stödja olika former av samtal (enkla, formella, med emojis, etc.), istället för att modifiera metoden communicate() varje gång definierar vi en konversationsabstraktion (till exempel, AbstractConversation) och vi skapade implementeringar som SimpleConversation som genererar fraserna. Kommunikatorn skriver helt enkelt ut det som konversationen returnerar, utan att modifiera sin egen kod.
I affärsvärlden är OCP avgörande för att minska risker vid införande av förändringar: det undviker att använda basklasser öppnas igen och uppmuntrar till utökning genom nya underklasser eller strategier , vilket håller den stabila koden så intakt som möjligt.
L – Liskovs substitutionsprincip och korrekt arv
Liskovs substitutionsprincip anger att objekt i en underklass ska kunna ersätta objekt i deras superklass utan att systemet förstörs . Med andra ord, om kod förväntar sig en instans av basklassen, ska den fungera på samma sätt om den får en instans av en underklass.
I praktiken innebär detta att underklassen inte får bryta mot de förväntningar eller det kontrakt som basklassen har satt: returtyper, undantag, förhandsvillkor och eftervillkor måste förbli konsekventa.
Ett välkänt (och vilseledande) exempel är hierarkin Rektangel / KvadratMatematiskt sett är en kvadrat en rektangel, men i kod är det inte alltid bekvämt. Square ärvt direkt från RectangleOm rektangeln har separata inställningar för höjd och bredd, kan en testmetod anta att om man bara ändrar höjden lämnas bredden oförändrad. Om kvadratklassen tvingar båda att ändras samtidigt, bryts superklassens kontrakt plötsligt samman.
Något liknande händer i hierarkier som Fågel I Python. Om basklassen innehåller en metod fly()Men sedan har vi underklasser som Ostrich eller pingviner som inte kan flyga, tvinga fram den implementeringen och lansera en NotImplementedError bryter LSP. Varje funktion som tar emot en Bird och ring fly() Jag borde kunna göra det utan några överraskande undantag.
Lösningen innebär att omforma hierarkin: en generisk basklass bibehålls Fågel utan metoden fly(), och mellanliggande underklasser skapas såsom Flygande Fågel y SimningFågelFåglar som flyger implementerar det första; de som simmar det andra; och de som gör båda ärver från båda. På så sätt tvingas ingen underklass att erbjuda beteenden som inte är logiska för den.
Att respektera LSP kräver noggrant övervägande av arvsrelationer i Python, och leder i många fall till att man föredrar enkel komposition och gränssnitt framför djupa arvsträd där det är lätt att bryta mot superklasskontraktet.
I – Principen för gränssnittssegregering och mer specifika klasser
Principen för gränssnittssegregering anger att vi inte bör tvinga en klient att vara beroende av metoder den inte använder . I praktiken innebär detta att man designar små, specifika gränssnitt (eller abstrakta klasser i Python) istället för ett enda, gigantiskt gränssnitt för allting.
Föreställ dig ett gränssnitt Arbetare i Python som definierar metoderna work() y eat()Det här gränssnittet är logiskt för en människa som arbetar och äter, men inte för en robot som bara arbetar. Om vi tvingar fram klassen Robot att genomföra eat(), kommer att avslutas med en tom metod eller ett undantag, vilket indikerar att "den robotar "De äter inte", vilket är ett tydligt tecken på internetleverantörsbrott.
Lösningen innebär att dela upp gränssnittet i Genomförbar (något som kan fungera) och Ätbar (något du kan äta). Således, klassen Human implementerar båda, medan Robot Den implementerar bara arbetsdelen. Varje klient förlitar sig endast på de metoder de faktiskt behöver.
Åter till fåglar, något liknande händer med ett gränssnitt som kombinerar metoder som fly() y swim()Inte alla fåglar gör båda sakerna, så det är mer meningsfullt att separera dem i Flygande Fågel y SimningFågelDen resulterande designen är mer flexibel och man undviker att behöva fylla irrelevanta metoder med dummy-kod.
I Python, även om vi inte har formella gränssnitt som i Java, kan vi uppnå samma effekt med hjälp av abstrakta klasser i modulen abc eller typprotokoll. Att separera gränssnitt gör varje klass mer definierad och minskar sannolikheten för att introducera onödiga beroenden.
D – Beroendeinversionsprincipen och abstraktionsbaserad design
Principen om beroendeinversion lyfter fram två huvudidéer: högnivåmoduler bör inte vara beroende av lågnivåmoduler , och båda bör vara beroende av abstraktioner . Dessutom bör abstraktioner inte vara beroende av detaljer; detaljer är beroende av abstraktioner.
Ett mycket vanligt misstag är att en domän eller affärslogikklass i Python direkt skapar en konkret instans av en databas, HTTP-tjänst eller meddelandeklient: till exempel en UserRepository som gör det internt self.database = MySQLDatabase()Om vi imorgon vill byta till PostgreSQL eller en minnesbaserad testmotor, måste vi redigera den klassen.
Att tillämpa DIP innebär att införa en databasabstraktion (till exempel en abstrakt klass) Database med metoder connect() y query()) och skapa konkreta implementeringar som till exempel MySQLDatabase o PostgreSQLDatabaseArkivet slutar instansiera själva databasen och börjar ta emot den utifrån, vanligtvis genom konstruktorn.
Detta mönster kallas beroendeinjektion och är en av grundpelarna för att uppnå modulär och testbar kod: i produktion injicerar vi en verklig implementering; i testning injicerar vi en testdubbel som simulerar beteendet utan att vidröra externa system.
I ett mer detaljerat exempel kan man definiera en abstraktion av en kommunikationskanal, som AbstraktKanal, och en abstraktion av kommunikatör, AbstraktKommunikatör, som arbetar med AbstraktKonversationI stället SMSCommunicator skapar internt en SMSChannelDen DIP-anpassade versionen säkerställer att kommunikatorn tar emot en abstrakt kanal i konstruktorn. Det betyder att den inte är beroende av en konkret klass, utan snarare av ett gränssnitt, och kan arbeta med vilken typ av kanal som helst som uppfyller kontraktet.
Sammanfattningsvis uppmuntrar DIP design av lager där affärslogik förlitar sig på generiska gränssnitt, och ramverk, konfigurationer eller fabriker ansvarar för att länka dessa gränssnitt med specifika implementeringar beroende på miljön.
Praktiskt exempel i Python: SOLID-notifieringstjänst
Ett mycket illustrativt exempel på hur flera designprinciper kombineras är ett aviseringssystem som kan skicka meddelanden via olika kanaler, såsom e-post eller SMS, och som också är enkelt att utöka med nya metoder (push, webhooks, etc.).
Målet är att den huvudsakliga logiken som avgör "Jag måste meddela X sak" inte behöver skrivas om varje gång vi lägger till en kanal, och att vi kan testa den utan att utlösa riktiga e-postmeddelanden eller SMS-meddelanden under testningen.
För att göra detta definierar vi en abstraktion anmäla med en metod send(to, subject, body) vilket returnerar ett booleskt värde som indikerar framgång eller misslyckande. Därifrån skapade vi implementationer som E-postmeddelande y SMSNotifiervar och en tar emot en extern klient i sin konstruktor (till exempel, smtp_client o sms_client) ansvarig för att tala med den kungliga tjänsten.
Utöver allt detta, en Aviseringstjänst som tar emot en lista över anmälare (allt som implementerar kontraktet) och erbjuder en metod för att skicka ett meddelande via alla kanalerDen här tjänsten vet ingenting om SMTP, SMS-leverantörer eller HTTP API:er: den itererar och anropar bara send() om varje anmälare.
Denna design visar tydligt flera principer i praktiken:
- SRPVarje klass har ett mycket begränsat ansvar: a
EmailNotifierDen hanterar bara e-postmeddelanden,SMSNotifierEndast SMS, orkestertjänsten och externa kunder känner bara till transporten. - DIP:
NotificationServiceDet beror på abstraktionenNotifierinte av specifika klasser, och anmälarna är i sin tur beroende av injicerade klienter. - OCP: att lägga till en
PushNotifieroWebhookNotifierDet finns ingen anledning att röraNotificationServiceSkapa bara en ny implementering och registrera den. - LSP och ISPalla implementeringar av
NotifierDe respekterar samma avtalsend(), utan obligatoriska metoder som inte är användbara för en specifik kanal.
Ur ett testperspektiv räcker det att skapa en eller flera DummyNotifier som spelar in samtalen till send() utan att göra något annat. Genom att injicera dem i tjänsten kan vi verifiera att alla aviseringar har utlösts utan behov av nätverk, köer eller externa system.
Detta exempel kan också kompletteras med principer som DRY (undvik att duplicera vanlig formatlogik), KISS (introducera inte återförsök, köer eller prioritering förrän det verkligen behövs) och YAGNI (förutse inte funktioner som vi inte har i den omedelbara färdplanen).
SOLIDs förhållande till andra designprinciper
Även om SOLID fokuserar på objektorientering, kombineras det i praktiken med andra generella principer för mjukvarudesign som DRY, KISS och YAGNI , vilka också förekommer ofta i litteraturen om ren kod.
DRY (Don't Repeat Yourself) påminner oss om att viktig affärslogik bara bör ha en representation i systemet. Det passar perfekt med SRP och OCP: om vi upprepar beteende i olika klasser kräver utökning eller korrigering av ett användningsfall att man modifierar många ställen och ökar risken för inkonsekvenser.
KISS (Keep It Simple, Stupid) uppmuntrar oss att undvika onödigt komplexa arkitekturer och abstraktioner. Även om det kan verka motsägelsefullt, innebär tillämpning av SOLID inte att man fyller projektet med gränssnitt för själva gränssnittet, utan snarare att man introducerar tillräckligt för att hålla designen begriplig för teamet.
YAGNI (You Aren't Gonna Need It) är ett motgift mot överdriven design. I projekt med mycket volatila krav kan det vara kontraproduktivt att försöka förutse alla möjliga framtida förändringar genom att bygga lager på lager. Det ideala tillvägagångssättet är att hitta en balans: tillämpa SOLID-principer på de delar vi vet kommer att växa eller förändras, och hålla det vi vet är stabilt enklare.
Sammantaget ger alla dessa principer upphov till en utvecklingsstil där prioriteten är tydlig, modulär och samarbetsorienterad kod , men utan att falla in i arkitekturens fetischism för arkitekturens skull.
Fördelar, begränsningar och fall där det inte lönar sig att vara purist
SOLID-principerna anses nästan vara en tumregel i medelstora och stora projekt , men det betyder inte att de ska tillämpas till punkt och pricka i alla sammanhang. Det är också viktigt att vara medveten om deras begränsningar.
I mycket små applikationer, engångsskript eller lågbudgetprojekt som inte kommer att underhållas långsiktigt, kan det vara mer kostsamt än fördelaktigt att införa för många lager av abstraktion . Den mentala och kodande omkostnaden för att tillämpa DIP eller ISP på allt kanske inte är värd det.
Det finns också situationer med mycket snäva tidskrav eller vid utveckling av en MVP, där det är viktigt att prioritera leveranshastighet. Det är klokt att skjuta upp en del av SOLID-förfiningen till senare, när produkten har stabiliserats och vi vet vilka delar som kommer att överleva.
I äldre system med mycket kod är det oftast orealistiskt att försöka "solida" allt på en gång. I dessa scenarier är det bättre att tillämpa principerna stegvis på kodområden som används ofta eller som blockerar nya funktioner.
Slutligen finns det domäner som är mycket känsliga för extrem prestanda (till exempel vissa högfrekventa rutiner) där abstraktionslager kan introducera oönskad overhead. I dessa fall är det ibland motiverat att offra en del av SOLID-renheten i mycket specifika block, förutsatt att resten av systemet förblir väldesignat.
Sammantaget gör förståelse och tillämpning av SOLID-principer i Python att du kan gå från kod som "bara fungerar" till kod som håller över tid , anpassar sig bättre till affärsförändringar, underlättar teamsamarbete och minskar överraskningar i produktionen. Nyckeln är att använda dem klokt och veta när det är värt att introducera en annan abstraktion och när det är bättre att hålla saker och ting enkla och okomplicerade.
Passionerad författare om bytesvärlden och tekniken i allmänhet. Jag älskar att dela med mig av min kunskap genom att skriva, och det är vad jag kommer att göra i den här bloggen, visa dig alla de mest intressanta sakerna om prylar, mjukvara, hårdvara, tekniska trender och mer. Mitt mål är att hjälpa dig att navigera i den digitala världen på ett enkelt och underhållande sätt.