Я никогда не считал себя программистом и тем более ООП-шником, однако я программирую и иногда использую ООП в своей работе. Только для меня ООП не совсем то же самое, что понимают под этим многие разработчики. В сегодняшней статье я хочу поделиться своим личным видением ООП на простом примере. В этом примере я покажу как реализованы все три принципа ООП.
В качестве языка программирования мы будем использовать Python (да-да, понимаю, что не самый ООП-шный язык, но тем не менее)
Вводные данные
У нас есть некая программа, которая что-то делает, а потом сохраняет полученные данные. Что именно делает программа нас для этого примера не интересует, нас будут интересовать объекты, которые я назвал `принтерами`. После выполнения основного кода программа будет сохранять полученные данные в .csv, .json и .xml файлы. Хотя в этом конкретном примере непосредственно сохранять мы ничего не будем, так как это не есть цель данной статьи.
Пример
Импортируем библиотеки, которые нам потребуются
from abc import ABC, abstractmethod
from typing import Dict, List
from time import sleep
from datetime import datetime
Код главной программы
def main() -> None:
printers: Dict[str, IPrinter] = {
'csv': CSVPrinter(),
'json': JSONPrinter(),
'xml': XMLPrinter()
}
# Вот это результат выполнения программы, который мы будем "сохранять" :)
data = [
{'a': 1, 'b': 'Hello'},
{'a': 2, 'b': 'Bye'},
]
for printer in printers.values():
printer.process(data)
print(f"All printers succeed")
Программа инициализирует наши принтеры в словаре `printers`, далее получает результат и передает полученные данные в метод `process` каждого принтера.
Давайте реализуем абстрактный класс IPrinter
class IPrinter(ABC):
ext: str = None
def __init__(self) -> None:
if self.ext is None:
raise NotImplementedError('You must specify `ext` attribute in the subclass')
if not self.ext.startswith('.'):
raise ValueError('`ext` must start with `.`')
self._filename: str = None
@abstractmethod
def process(self, data: List[Dict]) -> None:
...
@property
def filename(self) -> str:
if self._filename is None:
self._filename = f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}{self.ext}"
return self._filename
Здесь мы указали абстрактный метод `process`, тот самый который будет использовать программа, поэтому мы делаем его имплементацию обязательной, чтобы не было ошибок. Также у нас тут есть метод-свойство `filename`, в котором мы генерируем имя файла для сохранения, и метод `__init__`, в котором мы проводим пару проверок для требований подклассов.
Теперь давайте посмотрим на реализацию трех конкретных "принтеров"
class CSVPrinter(IPrinter):
ext: str = '.csv'
def process(self, data: List[Dict]) -> None:
print(f"Save data to a {self.filename} file")
class JSONPrinter(IPrinter):
ext: str = '.json'
def process(self, data: List[Dict]) -> None:
print(f"Save data to a {self.filename} file")
class XMLPrinter(IPrinter):
ext: str = '.xml'
def process(self, data: List[Dict]) -> None:
pass
Каждый класс наследуется от абстрактного класса `IPrinter`. Здесь мы применяем, наверное, самый простой принцип ООП - Наследование. А зачем мы его применяем? А затем, чтобы указать тому, кто будет использовать эти классы, что там есть метод `process` (В случае с python - это скорее будет разработчик и, возможно, его IDE, которая будет давать ему правильные подсказки).
Далее идет атрибут `ext`, его мы и проверяем в родительском классе на соответствие требованиям, так как он используется в создании имени файла.
В методе `process`, как я писал выше, мы ничего не сохраняем, а просто выводим принт. В принте мы используем свойство `filename`. Это следующий принцип ООП - Инкапсуляция. Другими словами, мы инкапсулировали логику создания имени файла в родительском классе. Дочерний класс знает только, что есть такое свойство, а то что это метод-свойство ему знать не нужно, точно также ему не нужно знать откуда это свойство взялось.
Полиморфизм
Вроде бы все, за исключением того, что мы пропустили еще один принцип ООП - Полиморфизм. А он уже был! Да, был! В главной программе.
printers: Dict[str, IPrinter] = {
'csv': CSVPrinter(),
'json': JSONPrinter(),
'xml': XMLPrinter()
}
Вот в этом месте, у нас есть некий принтер, программа не знает, что он будет делать ее задача передать ему в метод `process` данные, которые она получила. Это и есть, в моем понимании, полиморфизм. Множественные формы, но один объект или интерфейс.
Но что, если у каждого могут разные свойства!? Например, `CSVPrinter` хочет знать с каким разделителем сохранить файл. Один из способов реализовать это - через конструктор этого класса.
class CSVPrinter(IPrinter):
ext: str = '.csv'
def __init__(self, sep: str=';') -> None:
super().__init__()
self.sep = sep
def process(self, data: List[Dict]) -> None:
print(f"Save data to a {self.filename} file with ({self.sep}) separator")
sleep(13)
print(f"{self.filename} saved")
Здесь мы переписали метод `__init__`, в котором вызвали этот же метод у родительского класса, а также установили атрибут `sep` со значением по умолчанию ";". Тогда, если на нужно создать `принтер` с разделителем - ",", то для этого мы передадим это как параметр при создании экземпляра.
csv = CSVPrinter(sep=',')
Заключение
В заключении хочу сказать, что использование ООП ради ООП плохая идея. Это ухудшает читаемость кода и как следствие его поддержку, однако если вы пишете сложную программу, которую нужно структурировать, то именно ООП будет как нельзя кстати.