Jak krok po kroku stosować zasady SOLID w Pythonie

  • Zasady SOLID stanowią jasną podstawę do projektowania bardziej czytelnego, łatwiejszego w utrzymaniu i skalowalnego obiektowo zorientowanego kodu Python.
  • Każda zasada (SRP, OCP, LSP, ISP i DIP) rozwiązuje określony typ problemu projektowego — od słabo rozdzielonych obowiązków po sztywne zależności.
  • Zastosowanie SOLID z klasami, abstrakcjami i wstrzykiwaniem zależności w Pythonie redukuje sprzężenia, poprawia testowalność i ułatwia ewolucję systemu.

Solid w Pythonie

Gdy zaczynasz pracować nad dużymi projektami w Pythonie, jedną z pierwszych rzeczy, które zauważasz, jest to, że Kod staje się trudny do zrozumienia, testowania i rozszerzania. Jeśli nie przestrzegasz kilku podstawowych zasad projektowania, to właśnie tu pojawiają się słynne zasady SOLID: zbiór najlepszych praktyk, które mają znacznie ułatwić życie zespołowi.

Zasady te powstały w dziedzinie klasyczne programowanie obiektowe (Java, C++, C#, itp.)Ale idealnie pasują do Pythona, o ile używasz klas i obiektów w sposób mniej lub bardziej poważny. Przyjrzyjmy się szczegółowo, czym są, skąd się wzięły, dlaczego są ważne i przede wszystkim, jak… Zastosuj SOLID z przejrzystymi przykładami w Pythonie aby uczynić Twój kod bardziej łatwym w utrzymaniu, skalowalnym i przyjemnym w pracy.

Czym jest SOLID i skąd się to wszystko wzięło?

Termin SOLID to akronim spopularyzowany przez Michaela Feathersa Aby połączyć pięć zasad projektowania, pierwotnie zaproponowanych przez Roberta C. Martina, lepiej znanego jako Wujek Bob. Ten amerykański inżynier oprogramowania, jeden z sygnatariuszy Manifestu Agile, opublikował w połowie lat 90. artykuł „Zasady OOD”, a później „Zasady projektowania i wzorce projektowe”, w którym położył podwaliny pod współczesne projektowanie obiektowe.

Z czasem inni autorzy, tacy jak Barbara Liskov i Bertrand Meyer Wnieśli również pomysły, które zostały zintegrowane z tym zestawem zasad. Michael Feathers wpadł po prostu na (bardzo sprytny) pomysł, aby je przekształcić tak, aby inicjały utworzyły słowo SOLID, co pomogło im rozprzestrzenić się lotem błyskawicy w społeczności programistów.

Pięć liter SOLID odpowiada następującym zasadom projektowania obiektowego, które można zastosować także w Pythonie:

  • S – Zasada pojedynczej odpowiedzialności (Zasada pojedynczej odpowiedzialności)
  • O – zasada otwarta/zamknięta (Zasada otwartego/zamkniętego)
  • L – Zasada podstawienia Liskova (Zasada podstawienia Liskova)
  • I – Zasada segregacji interfejsów (Zasada segregacji interfejsów)
  • D – Zasada inwersji zależności (Zasada odwrócenia zależności)

Ogólna idea jest taka, że ​​te pięć zasad, stosowanych łącznie, Pomagają pisać elastyczne, łatwe do testowania i utrzymywania oprogramowaniePrzekłada się to na szybsze wdrożenia, mniej tajemniczych błędów, lepsze ponowne wykorzystanie kodu i mniej problemów, gdy projekt jest w fazie produkcyjnej już od kilku lat.

Do czego służą zasady SOLID w Pythonie?

Stosowanie zasad SOLID w Pythonie to nie tylko ćwiczenie akademickie; ma to bezpośredni wpływ na codzienną pracę zespołu. Przestrzeganie tych zasad Redukują ilość zbędnego kodu, zmniejszają nieprzyjemny zapach kodu i zapobiegają „zapachowi zgnilizny” w kodzie.używając znanej analogii: „jeśli coś źle pachnie, to znaczy, że coś jest źle zaprojektowane”. W systemie Windows wielu programistów decyduje się na Zainstaluj i skonfiguruj WSL2 mieć środowisko Linux bliższe środowisku produkcyjnemu.

W środowiskach współpracy (zespoły zajmujące się rozwojem zaplecza, inżynieria danych, produkty o długich cyklach itd.) zasady te są kluczowe wiele osób może pracować nad tą samą bazą kodu, nie przekraczając swoich uprawnień ani nie psując niczego przy najmniejszej zmianie.Co więcej, Python, mimo że jest elastyczny i dynamiczny, pozwala na bezproblemowe stosowanie typowych abstrakcji OOP: klas abstrakcyjnych, hierarchii dziedziczenia, kompozycji i interfejsów za pośrednictwem abc, itp.

Podsumowując, SOLID pomaga Ci osiągnąć:

  • Czystszy, bardziej czytelny kodnawet lata po jej napisaniu.
  • Lepsza testowalnośćponieważ obowiązki są wyraźnie rozdzielone.
  • Wysoka możliwość ponownego wykorzystania i skalowalność dzięki mniejszej liczbie sztywnych zależności między modułami.
  • Mniej błędów ubocznychGdy zmieniasz coś w jednym module, nie psujesz przez przypadek pięciu innych rzeczy.

S – Zasada pojedynczej odpowiedzialności

Pierwsza zasada głosi, że Klasa powinna mieć tylko jeden powód do zmiany.Innymi słowy, musi przyjąć jedną, jasno zdefiniowaną odpowiedzialność. Nie oznacza to posiadania tylko jednej metody, ale raczej, że cała jej logika powinna wskazywać na jeden, spójny cel.

Wyobraź sobie klasę Pythona, która reprezentuje użytkownika i oprócz przechowywania jego danych, zajmuje się również dostępem do bazy danych i generowaniem raportów:

class User:
    def __init__(self, name: str):
        self.name = name

    def get_user_from_database(self, user_id: int) -> dict:
        # Recupera datos desde la base de datos
        # ...
        pass

    def save_user_to_database(self) -> None:
        # Persiste el usuario en la base de datos
        # ...
        pass

    def generate_user_report(self) -> str:
        # Genera un informe del usuario
        # ...
        pass

Oto klasa łączy trzy różne obowiązkiReprezentowanie użytkownika, zarządzanie trwałością i tworzenie raportów. Zmiany w bazie danych, formacie raportu lub atrybutach użytkownika wymagają modyfikacji tej samej klasy, co zwiększa ryzyko wprowadzenia błędów interdyscyplinarnych.

Jeśli oddzielimy te kwestie, projekt ulegnie znacznej poprawie:

class User:
    def __init__(self, name: str):
        self.name = name


class UserDB:
    @staticmethod
    def get_user(user_id: int) -> User:
        # Lógica para obtener usuarios de la base de datos
        # ...
        return User("John Doe")

    @staticmethod
    def save_user(user: User) -> None:
        # Lógica para guardar el usuario
        # ...
        pass


class UserReportGenerator:
    @staticmethod
    def generate_report(user: User) -> str:
        # Lógica para generar informes de usuario
        # ...
        return f"Report for user: {user.name}"

Teraz klasa Użytkownik reprezentuje użytkownika wyłącznie jako podmiotJeśli zmieni się sposób generowania raportów, wystarczy dotknąć UserReportGeneratorJeśli zmienisz bazę danych, wystarczy dotknąć UserDBKażda klasa ma pojedynczy powód wprowadzenia zmian, co upraszcza debugowanie i ewolucję systemu.

SRP zastosowane do bardziej realistycznego przykładu: kaczki i komunikacja

Przyjrzyjmy się klasycznemu scenariuszowi: klasie Kaczka Do którego początkowo stopniowo dodawane są obowiązki, aż w końcu staje się to potworem trudnym do utrzymania. Wyobraź sobie naiwną implementację:

class Duck:
    def __init__(self, name: str):
        self.name = name

    def fly(self) -> None:
        print(f"{self.name} is flying not very high")

    def swim(self) -> None:
        print(f"{self.name} swims in the lake and quacks")

    def do_sound(self) -> str:
        return "Quack"

    def greet(self, other_duck: "Duck") -> None:
        print(f"{self.name}: {self.do_sound()}, hello {other_duck.name}")

Klasa Należy je po prostu zdefiniować jako „kaczkę”Ale zarządza również sposobem, w jaki komunikują się ze sobą. Jeśli jutro zmienisz logikę konwersacji (więcej fraz, inne języki, inne kanały), będziesz musiał zmodyfikować klasę duck, która już dobrze funkcjonuje jako encja.

Rozwiązaniem zgodnym z SRP jest wydzielenie tej drugiej odpowiedzialności z innej klasy specjalizującej się w komunikacji:

class Duck:
    def __init__(self, name: str):
        self.name = name

    def fly(self) -> None:
        print(f"{self.name} is flying not very high")

    def swim(self) -> None:
        print(f"{self.name} swims in the lake and quacks")

    def do_sound(self) -> str:
        return "Quack"


class Communicator:
    def __init__(self, channel: str):
        self.channel = channel

    def communicate(self, duck1: Duck, duck2: Duck) -> None:
        sentence1 = f"{duck1.name}: {duck1.do_sound()}, hello {duck2.name}"
        sentence2 = f"{duck2.name}: {duck2.do_sound()}, hello {duck1.name}"
        conversation = 
        print(*conversation, f"(via {self.channel})", sep="\n")

Dzięki temu rozdzieleniu, Możesz rozwijać logikę komunikacji, nie ruszając definicji kaczkiPonadto kod jest łatwiejszy do przetestowania: testujesz zachowanie Duck a z drugiej strony ten z Communicatorbez mieszania obowiązków.

O – zasada otwarta/zamknięta

Zasada OCP stanowi, że Podmioty zajmujące się oprogramowaniem powinny być otwarte na rozszerzanie swojego działania, ale zamknięte na bezpośrednie modyfikacje.Innymi słowy, gdy chcesz dodać nową funkcjonalność, w idealnym przypadku nie musisz przepisywać klas, które już działają i są używane przez inne moduły.

Klasycznym przykładem jest obliczanie pól figur geometrycznych. Przyjrzyjmy się najpierw wersji, która… nie szanuje OCP:

class Rectangle:
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height


class Circle:
    def __init__(self, radius: float):
        self.radius = radius


class AreaCalculator:
    def calculate_area(self, shape) -> float:
        if isinstance(shape, Rectangle):
            return shape.width * shape.height
        elif isinstance(shape, Circle):
            return 3.14159 * shape.radius * shape.radius
        else:
            raise ValueError("Forma no soportada")

Jeśli jutro będziesz chciał dodać trójkąt, będziesz zmuszony zmodyfikuj kod AreaCalculatordodanie kolejnego elifNarusza to zasadę OCP, ponieważ klasa nie jest już „zamknięta” na zmiany.

Poprawna wersja zakłada wprowadzenie abstrakcji Shape z metodą area() które każda postać realizuje na swój własny sposób:

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        pass


class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height


class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius * self.radius


class AreaCalculator:
    def calculate_area(self, shape: Shape) -> float:
        return shape.area()

Dzięki temu projektowi, dodaj trójkąt, którego nie dotykasz AreaCalculatorWystarczy utworzyć nową podklasę:

class Triangle(Shape):
    def __init__(self, base: float, height: float):
        self.base = base
        self.height = height

    def area(self) -> float:
        return 0.5 * self.base * self.height

Zasada Otwarte/Zamknięte bardzo dobrze pasuje do idei zdefiniuj jasne punkty rozszerzenia poprzez abstrakcje:interfejsy, klasy abstrakcyjne, haki itp. W Pythonie moduł abc Pozwala wyrazić to wprost, nawet jeśli język jest dynamiczny.

OCP zastosowane w przykładzie komunikatora

Jeśli wrócimy do przykładu KomunikatorMożemy pójść o krok dalej i przygotować projekt tak, aby obsługiwał różne typy konwersacji, bez konieczności każdorazowego przepisywania komunikatora. W tym celu definiujemy abstrakcję konwersacji i pozwalamy komunikatorowi tylko z niej korzystać:

from typing import final
from abc import ABC, abstractmethod


class AbstractConversation(ABC):
    @abstractmethod
    def do_conversation(self) -> list:
        pass


class SimpleConversation(AbstractConversation):
    def __init__(self, duck1: Duck, duck2: Duck):
        self.duck1 = duck1
        self.duck2 = duck2

    def do_conversation(self) -> list:
        sentence1 = f"{self.duck1.name}: {self.duck1.do_sound()}, hello {self.duck2.name}"
        sentence2 = f"{self.duck2.name}: {self.duck2.do_sound()}, hello {self.duck1.name}"
        return 


class Communicator:
    def __init__(self, channel: str):
        self.channel = channel

    @final
    def communicate(self, conversation: AbstractConversation) -> None:
        print(*conversation.do_conversation(), f"(via {self.channel})", sep="\n")

W tej wersji Jeśli chcesz dodać nowy sposób mówienia (na przykład agresywna rozmowa, rozmowa, w której uczestnicy zmieniają się, itd.), po prostu tworzysz kolejną podklasę AbstractConversation. Metoda communicate() de Communicator Nie ulega zmianie i jest zgodny z OCP co do joty.

L – Zasada podstawienia Liskova

Zasada substytucji Liskova, sformułowana przez Barbarę Liskov, głosi, że Podklasy powinny móc zastępować swoje klasy bazowe bez zmiany oczekiwanego zachowania programu.W praktyce oznacza to, że jeśli kod działa z jednym wystąpieniem klasy bazowej, powinien działać równie dobrze z dowolnym wystąpieniem podklasy.

Typowym przykładem naruszenia LSP jest modelowanie wszystkich ptaków jedną metodą fly()w tym strusie:

class Bird:
    def fly(self) -> None:
        pass


class Duck(Bird):
    def fly(self) -> None:
        print("¡El pato está volando!")


class Ostrich(Bird):
    def fly(self) -> None:
        # Las avestruces no vuelan
        raise NotImplementedError("Las avestruces no pueden volar")

Każdy kod, który zakłada, że Każdy ptak, który potrafi latać, poniesie porażkę, gdy dostanie strusia. To znaczy Ostrich Nie jest to odpowiedni substytut Birdłamiąc tym samym zasady LSP.

Rozwiązaniem jest dostosowanie hierarchii tak, aby lepiej odzwierciedlała rzeczywistość: nie wszystkie ptaki latają, więc Tylko część ptaków powinna mieć tę metodę fly():

class Bird:
    pass


class FlyingBird(Bird):
    def fly(self) -> None:
        pass


class Duck(FlyingBird):
    def fly(self) -> None:
        print("¡El pato está volando!")


class Ostrich(Bird):
    # No vuela, así que no implementa fly()
    pass

Dzięki temu projektowi, Każda funkcja wymagająca latającego ptaka będzie deklarować, że go wymaga. FlyingBirdi nigdy nie otrzyma strusia. W ten sposób LSP jest respektowane, a nieoczekiwane wyjątki w czasie wykonywania są unikane.

LSP i rozmowy z ptakami

Wracając do przykładu konwersacji, często zaczynamy kodować, myśląc tylko o kaczkach, a potem chcemy dodać wrony lub inne ptaki. Jeśli klasa konwersacji zależy od… Duck, Nie będziesz mógł go ponownie wykorzystać z innymi gatunkami ptaków bez dotykania kodu:

class Crow:
    # Implementación específica del cuervo
    ...

Si SimpleConversation Jest on przeznaczony tylko dla kaczek; nie da się go zastosować do wrony bez modyfikacji. Prawidłowym podejściem jest utworzenie wspólnej abstrakcji. Bird i sprawić, by rozmowa opierała się na tej abstrakcji:

from abc import ABC, abstractmethod


class Bird(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def do_sound(self) -> str:
        pass


class Crow(Bird):
    def do_sound(self) -> str:
        return "Caw"


class Duck(Bird):
    def do_sound(self) -> str:
        return "Quack"


class SimpleConversation(AbstractConversation):
    def __init__(self, bird1: Bird, bird2: Bird):
        self.bird1 = bird1
        self.bird2 = bird2

    def do_conversation(self) -> list:
        sentence1 = f"{self.bird1.name}: {self.bird1.do_sound()}, hello {self.bird2.name}"
        sentence2 = f"{self.bird2.name}: {self.bird2.do_sound()}, hello {self.bird1.name}"
        return 

W ten sposób każda podklasa Bird który szanuje umowę (do_sound()(imię itp.) jest ważny substytut i nie zakłóci oczekiwanego zachowania SimpleConversation.

I – Zasada segregacji interfejsów

Zasada dostawcy usług internetowych zakłada, że Żaden klient nie powinien być zmuszany do korzystania z metod, których nie stosuje.Przekładając to na klasy abstrakcyjne lub interfejsy, oznacza to, że lepiej mieć kilka małych, szczegółowych interfejsów niż jeden duży, ogólny.

Przyjrzyj się temu projektowi, w którym interfejs Worker Wymaga od wszystkich, którzy ją wdrażają, stosowania konkretnych metod pracy i odżywiania:

from abc import ABC, abstractmethod


class Worker(ABC):
    @abstractmethod
    def work(self) -> None:
        pass

    @abstractmethod
    def eat(self) -> None:
        pass


class Human(Worker):
    def work(self) -> None:
        print("El humano está trabajando")

    def eat(self) -> None:
        print("El humano está comiendo")


class Robot(Worker):
    def work(self) -> None:
        print("El robot está trabajando")

    def eat(self) -> None:
        # El robot no come, pero está obligado a declarar este método
        pass

Klasa Robot opiera się na metodzie eat() to nie jest potrzebneWszelkie zmiany związane z pożywieniem będą miały wpływ na robota, nawet jeśli nie mają nic wspólnego z tym zachowaniem.

Stosując ISP podzieliliśmy interfejs na dwa mniejsze i bardziej szczegółowe:

class Workable(ABC):
    @abstractmethod
    def work(self) -> None:
        pass


class Eatable(ABC):
    @abstractmethod
    def eat(self) -> None:
        pass


class Human(Workable, Eatable):
    def work(self) -> None:
        print("El humano está trabajando")

    def eat(self) -> None:
        print("El humano está comiendo")


class Robot(Workable):
    def work(self) -> None:
        print("El robot está trabajando")

Teraz Każda klasa implementuje tylko te metody, których faktycznie potrzebuje.Zmniejsza to powiązania, ułatwia ewolucję projektu i sprawia, że ​​kod staje się bardziej ekspresyjny: staje się bardzo jasne, kto co może zrobić.

ISP w modelowaniu ptaków: latanie i pływanie

Coś podobnego dzieje się w przypadku modelowania ptaków, które latają i pływają. Jeśli podstawowa abstrakcja Bird Wymaga wdrożenia obu fly() jako swim()Skończysz z klasami takimi jak Crow którzy muszą udawać, że potrafią pływać:

class Bird(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def fly(self) -> None:
        pass

    @abstractmethod
    def swim(self) -> None:
        pass

    @abstractmethod
    def do_sound(self) -> str:
        pass

Rozwiązanie według dostawcy usług internetowych to posegregować interfejs na bardziej szczegółowe możliwości:

class Bird(ABC):
    def __init__(self, name: str):
        self.name = name

    @abstractmethod
    def do_sound(self) -> str:
        pass


class FlyingBird(Bird):
    @abstractmethod
    def fly(self) -> None:
        pass


class SwimmingBird(Bird):
    @abstractmethod
    def swim(self) -> None:
        pass


class Crow(FlyingBird):
    def fly(self) -> None:
        print(f"{self.name} is flying high and fast!")

    def do_sound(self) -> str:
        return "Caw"


class Duck(SwimmingBird, FlyingBird):
    def fly(self) -> None:
        print(f"{self.name} is flying not very high")

    def swim(self) -> None:
        print(f"{self.name} swims in the lake and quacks")

    def do_sound(self) -> str:
        return "Quack"

Jeśli kiedykolwiek zdecydujesz się na modelowanie pingwina, po prostu każesz mu dziedziczyć po SwimmingBird ale nie z FlyingBirdI nie będziesz musiał implementować pustych metod ani rzucać sztucznych wyjątków.

D – Zasada inwersji zależności

Ostatnią zasadę, DIP, można podsumować w dwóch kluczowych ideach: Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu; oba powinny zależeć od abstrakcji.Abstrakcje nie powinny zależeć od szczegółów, to raczej szczegóły powinny zależeć od abstrakcji.

W praktyce oznacza to, że logika biznesowa nie powinna być powiązana ze szczegółowymi informacjami, takimi jak „Używam MySQL”, „Zapisuję do pliku lokalnego” lub „Wysyłam wiadomości SMS za pomocą tego dostawcy”. Zamiast tego definiujesz abstrakcyjne interfejsy (na przykład Database, Channel, NotificationService) i sprawiasz, że Twój kod wysokiego poziomu komunikuje się tylko z nimi.

Projekt, który przerwać DIP Byłoby to repozytorium użytkowników, które bezpośrednio tworzy instancję bazy danych MySQL:

class MySQLDatabase:
    def connect(self) -> None:
        # Conectar a MySQL
        pass

    def query(self, sql: str) -> list:
        # Ejecutar consulta
        return []


class UserRepository:
    def __init__(self) -> None:
        self.database = MySQLDatabase()  # Dependencia directa

    def get_users(self) -> list:
        return self.database.query("SELECT * FROM users")

Jeśli zdecydujesz się używać PostgreSQL jutro, musisz zmodyfikuj klasę wysokiego poziomu UserRepositoryJesteś przywiązany do konkretnego szczegółu implementacji.

Stosując DIP, najpierw definiujemy abstrakcję bazy danych, a następnie konkretne implementacje dziedziczą po niej:

from abc import ABC, abstractmethod


class Database(ABC):
    @abstractmethod
    def connect(self) -> None:
        pass

    @abstractmethod
    def query(self, sql: str) -> list:
        pass


class MySQLDatabase(Database):
    def connect(self) -> None:
        # Conexión a MySQL
        pass

    def query(self, sql: str) -> list:
        # Consulta en MySQL
        return []


class PostgreSQLDatabase(Database):
    def connect(self) -> None:
        # Conexión a PostgreSQL
        pass

    def query(self, sql: str) -> list:
        # Consulta en PostgreSQL
        return []


class UserRepository:
    def __init__(self, database: Database) -> None:
        self.database = database  # Depende de una abstracción

    def get_users(self) -> list:
        return self.database.query("SELECT * FROM users")

W ten sposób Można wstrzyknąć dowolną implementację Database podczas tworzenia repozytorium, bez naruszania jego wewnętrznego kodu:

mysql_db = MySQLDatabase()
user_repo = UserRepository(mysql_db)

postgres_db = PostgreSQLDatabase()
user_repo = UserRepository(postgres_db)

Ten wzór jest znany jako Wstrzykiwanie zależności I jest to najczęstszy sposób stosowania DIP: klasy nie tworzą własnych zależności, ale otrzymują je z zewnątrz (poprzez konstruktor lub określone metody), zawsze używając abstrakcji jako typu.

DIP stosowany do kanałów i komunikatorów

W przykładzie rozmów ptaków możemy również usprawnić zarządzanie kanałami, stosując DIP. Załóżmy, że zdefiniujemy jedną abstrakcję dla kanału i inną dla komunikatora:

class AbstractChannel(ABC):
    @abstractmethod
    def get_channel_message(self) -> str:
        pass


class AbstractCommunicator(ABC):
    @abstractmethod
    def get_channel(self) -> AbstractChannel:
        pass

    @final
    def communicate(self, conversation: AbstractConversation) -> None:
        print(*conversation.do_conversation(),
              self.get_channel().get_channel_message(),
              sep="\n")

Pierwszą, naiwną implementacją mogłoby być:

class SMSChannel(AbstractChannel):
    def get_channel_message(self) -> str:
        return "(via SMS)"


class SMSCommunicator(AbstractCommunicator):
    def __init__(self) -> None:
        self._channel = SMSChannel()  # Depende de detalle concreto

    def get_channel(self) -> AbstractChannel:
        return self._channel

Choć wydaje się to słuszne, Ten komunikator jest nadal bezpośrednio połączony z SMSChannelUlepszyliśmy projekt, umożliwiając komunikatorowi otrzymywanie kanału z zewnątrz (wstrzykiwanie zależności) i w ten sposób poleganie wyłącznie na abstrakcji:

class SimpleCommunicator(AbstractCommunicator):
    def __init__(self, channel: AbstractChannel) -> None:
        self._channel = channel

    def get_channel(self) -> AbstractChannel:
        return self._channel

Dzięki temu podejściu każdy nowy kanał (poczta e-mail, powiadomienia push itp.) wdraża AbstractChannel y Można z niego korzystać bez zmiany kodu komunikatora.Ponownie, klasy wysokiego poziomu opierają się na abstrakcjach, a nie szczegółach.

Co się stanie jeśli zignorujesz SOLID?

Jeżeli nie weźmiemy pod uwagę tych zasad, kod będzie miał tendencję do występowania problemów takich jak: zapach kodu, gnicie kodu i niemożność rozwikłania powiązańOznacza to ogromne klasy z tysiącem obowiązków, podklasy, które łamią kontrakty, cykliczne zależności i metody, które zmieniają się co drugi dzień, ponieważ wykonują zbyt wiele czynności.

Konsekwencje są oczywiste i bardzo bolesne dla każdego zespołu: Więcej luk w zabezpieczeniach, więcej błędów, ciągłe refaktoryzowanie i w najgorszym przypadku kod, który okazuje się praktycznie bezużyteczny.To coś, co potocznie nazywa się „kodem spaghetti”: trudnym do zrozumienia, pełnym poprawek i praktycznie niemożliwym do rozszerzenia bez zepsucia czegoś ważnego.

Zasady SOLID nie są niezmienne i nie zawsze warto stosować je wszystkie sztywno, zwłaszcza w przypadku szybkiego prototypowania lub bardzo małych projektów. Mimo to, Należy o nich pamiętać i stosować je w większości projektów obiektowych w Pythonie. To stanowi różnicę między projektem, który rozwija się z czasem, a takim, który rozpada się, gdy tylko trochę się rozrośnie.

Najlepsze środowiska IDE do programowania systemu Windows 11
Podobne artykuł:
Najlepsze środowiska IDE do programowania w systemie Windows 11

Dodaj jako preferowane źródło