1.5. S.O.L.I.D. Principles

  • SRP - Single Responsibility Principle

  • OCP - Open/Closed Principle

  • LSP - Liskov Substitution Principle

  • ISP - Interface Segregation Principle

1.5.1. Single Responsibility Principle

There should never be more than one reason for a class to change. In other words, every class should have only one responsibility.

—SOLID, Single Responsibility Principle, Robert C. Martin

SetUp:

>>> from dataclasses import dataclass
>>> from random import randint
>>> from typing import ClassVar

Bad:

>>> @dataclass
... class Hero:
...     HEALTH_MIN: ClassVar[int] = 10
...     HEALTH_MAX: ClassVar[int] = 20
...     health: int = 0
...     position_x: int = 0
...     position_y: int = 0
...
...     def position_set(self, x: int, y: int) -> None:
...         self.position_x = x
...         self.position_y = y
...
...     def position_change(self, right=0, left=0, down=0, up=0):
...         x = self.position_x + right - left
...         y = self.position_y + down - up
...         self.position_set(x, y)
...
...     def position_get(self) -> tuple:
...         return self.position_x, self.position_y
...
...     def __post_init__(self) -> None:
...         self.health = randint(self.HEALTH_MIN, self.HEALTH_MAX)
...
...     def is_alive(self) -> bool:
...         return self.health > 0
...
...     def is_dead(self) -> bool:
...         return self.health <= 0

Good:

>>> @dataclass
... class HasHealth:
...     HEALTH_MIN: ClassVar[int]
...     HEALTH_MAX: ClassVar[int]
...     health: int = 0
...
...     def __post_init__(self) -> None:
...         self.health = randint(self.HEALTH_MIN, self.HEALTH_MAX)
...
...     def is_alive(self) -> bool:
...         return self.health > 0
...
...     def is_dead(self) -> bool:
...         return self.health <= 0
>>>
>>>
>>> @dataclass
... class HasPosition:
...     position_x: int = 0
...     position_y: int = 0
...
...     def position_set(self, x: int, y: int) -> None:
...         self.position_x = x
...         self.position_y = y
...
...     def position_change(self, right=0, left=0, down=0, up=0):
...         x = self.position_x + right - left
...         y = self.position_y + down - up
...         self.position_set(x, y)
...
...     def position_get(self) -> tuple:
...         return self.position_x, self.position_y
>>>
>>>
>>> class Hero(HasHealth, HasPosition):
...     HEALTH_MIN: int = 10
...     HEALTH_MAX: int = 20

1.5.2. Open/Closed Principle

Software entities ... should be open for extension, but closed for modification.

—SOLID, Open/Closed Principle, Robert C. Martin

SetUp:

>>> from abc import abstractproperty, abstractmethod, ABC

Good:

>>> class Document(ABC):
...     @abstractproperty
...     def EXTENSIONS(self) -> list[str]: ...
...
...     @abstractmethod
...     def display(self):
...         raise NotImplementedError('Display method must be implemented')
...
...     def __init__(self, filename):
...         self.filename = filename
...
...     def __new__(cls, filename):
...         basename, extension = filename.split('.')
...         plugins = cls.__subclasses__()
...         for plugin in plugins:
...             if extension in plugin.EXTENSIONS:
...                 instance = object.__new__(plugin)
...                 instance.__init__(filename)
...                 return instance
...         raise ValueError('Unknown file type')
>>>
>>>
>>> class PDFDocument(Document):
...     EXTENSIONS = ['pdf']
...
...     def display(self):
...         print('Displaying PDF document')
>>>
>>>
>>> class WordDocument(Document):
...     EXTENSIONS = ['doc', 'docx']
...
...     def display(self):
...         print('Displaying Word document')

1.5.3. Liskov Substitution Principle

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

—SOLID, Liskov Substitution Principle, Robert C. Martin

SetUp:

>>> class User:
...     def login(self): ...
...     def logout(self): ...
>>>
>>> class Admin(User):
...     def add_users(self): ...
...     def del_users(self): ...
>>> def auth(account):
...     account.login()
...     account.logout()

Good:

>>> mark = User()
>>> auth(mark)  # will work
>>> melissa = Admin()
>>> auth(melissa)  # has to work

1.5.4. Interface Segregation Principle

Clients should not be forced to depend upon interfaces that they do not use.

—SOLID, Interface Segregation Principle, Robert C. Martin

SetUp:

>>> from typing import Protocol

Bad:

>>> class Account(Protocol):
...     def login(self): ...
...     def logout(self): ...

Good:

>>> class CanLogin(Protocol):
...     def login(self): ...
>>>
>>> class CanLogout(Protocol):
...     def logout(self): ...

1.5.5. Dependency Inversion Principle

Depend upon abstractions, [not] concretes.

—SOLID, Dependency Inversion Principle, Robert C. Martin

SetUp:

>>> class Account:
...     pass
>>>
>>> class User(Account):
...     pass
>>>
>>> class Admin(Account):
...     pass

Bad:

>>> mark: User = User()
>>> melissa: Admin = Admin()

Good:

>>> mark: Account = User()
>>> melissa: Account = Admin()