- Объекты-сущности (Entity Objects)
- Объекты-значения (Value Objects)
- Встраиваемые объекты (Embedded Objects)
- Выводы
В объектно-ориентированном программировании (ООП) особое внимание уделяется представлению и манипуляции реальных объектов или концепций. Но не все объекты имеют одинаковые свойства и характеристики.
Важно понимать различия между объектами-сущностями, объектами-значениями и внедренными объектами. Благодаря этому можно эффективно структурировать код и обеспечивать его поддержку. В этом уроке мы изучим эти три концепции.
Объекты-сущности (Entity Objects)
Когда говорят про ООП, рассуждают про сущности предметной области, например: пользователи, заказы, товары. У такого использования объектов есть определенные условия, которые должны соблюдаться для обеспечения нормального функционирования.
Подобное использование ООП хоть и описывается во всех учебниках как пример нужности ООП, имеет слабое отношение к реальному коду. На практике большинство существующих классов и объектов в коде приложений, библиотек и фреймворков не имеют связи с предметной областью. Их появление и использование крутится вокруг такой темы как полиморфизм, которая изучается в соответствующем курсе.
Объекты-сущности (entities) создаются не ради одноразового использования. Они живут определенный период во время запуска программы или между запусками в неком хранилище.
Например, пользователи Хекслета представлены объектами класса User
. Они создаются во время регистрации и потом существуют в системе бесконечное время. Изредка они удаляются по инициативе самих пользователей.
Для отличия одного пользователя от другого мы не можем просто использовать имя и фамилию, так как они могут быть не уникальными или изменяться со временем. Поэтому для обеспечения уникальности каждого пользователя мы используем искусственные идентификаторы, которые обычно генерируются базой данных. Сравнение пользователей происходит по их идентификаторам.
В Python объекты-сущности можно представить с использованием классов:
class User:
def __init__(self, id, name):
self.id = id
self.name = name
def __eq__(self, other):
return self.id == other.id
# Создаем двух пользователей с одинаковым именем и идентификатором.
user1 = User(1, 'John')
user2 = User(1, 'John')
user3 = User(3, 'John')
print(user1 == user2) # Вывод: True
print(user1 == user3) # Вывод: False
В этом примере объекты User
создаются для представления пользователей в системе. Их идентификация осуществляется посредством уникального идентификатора, что делает их объектами-сущностями.
Объекты, у которых есть свой идентификатор и время жизни, называют объектами-сущностями. Но кроме них существует и другая разновидность объектов. Она тоже связана с предметной областью – это объекты-значения.
Объекты-значения (Value Objects)
Объекты-значения отличаются от объектов-сущностей тем, что их идентичность определяется их состоянием, а не уникальным идентификатором. Например, два объекта «деньги» равны, если они представляют одну и ту же сумму в одной и той же валюте.
Например, когда у нас в кошельке лежит 10$, то нам неважно, какая конкретно это купюра. Мы легко можем взять эту купюру, поменять ее на другую эквивалентную по номиналу. Для нас в этот момент ничего не меняется. Десять долларов остались десятью долларами.
То же можно сказать и про многое другое, например: адреса доставки, страна проживания, путь до файла, адрес страницы сайта, точки на плоскости. Во всех этих ситуациях нас волнует само значение — факт его существования.
Представим систему, в которой идет работа с деньгами в разных валютах. В такой ситуации удобно представить деньги в виде объекта, который помимо номинала хранит информацию о валюте. В таком случае сравнение будет работать следующим образом:
m1 = Money(150, 'USD')
m2 = Money(130, 'EUR')
# Предположим, что 150 долларов по текущему курсу равны 130 евро
# Функция конвертирует деньги для сравнения
print(m1.equals(m2)) # => True
В этом примере два объекта Money
равны, потому что они представляют одну сумму в одной валюте. Их идентичность определяется их состоянием, а не уникальным идентификатором.
Получается, что нам не важны объекты, нам важны значения. Объект здесь — способ организации кода, но он не идентифицирует хранящиеся внутри него данные. Такие объекты называют объектами-значениями.
При этом одни и те же вещи могут быть как объектами-значениями, так и объектами-сущностями. Все зависит от конкретной предметной области. Для большинства компаний деньги — это просто значения, но не для тех кто их печатает. Вторым важно различать купюры между собой. Поэтому на каждой из них есть уникальный номер, который и позволяет проводить идентификацию.
Рассмотрим третью разновидность объектов.
Встраиваемые объекты (Embedded Objects)
Данные, с которыми работают веб-приложения, обычно хранятся в реляционных базах данных. В этих базах каждая сущность представлена строкой в таблице, где каждое поле соответствует атрибуту объекта.
Иногда возникают ситуации, когда несколько атрибутов сущности объединяются в одну группу, описывающую какую-то особенность или характеристику. Например, почтовый адрес:
# Предположим, что у нас есть функция find(), которая ищет пользователя по идентификатору.
# Это гипотетический код
user = find(5)
print(user.street) # 'lenina'
print(user.zipcode) # 432111
print(user.house) # 10
Существуют два подхода для работы с такими группами атрибутов. По первому подходу любая логика работы с этими атрибутами описывается внутри самой сущности. Например, вывод адреса в виде текста:
class User:
def get_full_address(self):
return f"{self.street}, {self.house}, {self.zipcode}"
print(user.get_full_address())
Основная проблема этого подхода — возможное дублирование кода, если адрес используется где-то еще кроме пользователя. В этом случае придется реализовывать методы работы с этими данными в каждом месте, где они встречаются.
Второй подход предполагает создание отдельного класса и внедрение объекта этого класса в основной объект. Это может звучать сложно, но на практике это довольно просто:
class Address:
def __init__(self, street, house, zipcode):
self.street = street
self.house = house
self.zipcode = zipcode
def __str__(self):
return f"{self.street}, {self.house}, {self.zipcode}"
class User:
def __init__(self, address):
self.street = address['street']
self.house = address['house']
self.zipcode = address['zipcode']
def get_address(self):
# Поскольку у нас объект-значение,
# мы можем создавать его столько раз, сколько нам нужно,
# но при необходимости этот процесс можно оптимизировать
return Address(self.street, self.house, self.zipcode)
address = {'street': 'pushkina', 'house': 42, 'zipcode': 42000}
user = User(address)
print(user.get_address()) #=> pushkina, 42, 42000
В этом примере Python мы показываем, как можно инкапсулировать логику работы с адресом пользователя в отдельном классе Address
. Метод __str__
в классе Address
предназначен для преобразования объекта в строку.
Подход с использованием встроенного объекта позволяет упростить код и избежать дублирования, поскольку весь код, связанный с адресом, теперь находится в одном месте.
Выводы
В этом уроке мы рассмотрели три основных типа объектов в ООП: объекты-сущности, объекты-значения и встроенные объекты. Зная различия между ними, вы можете более эффективно проектировать и структурировать код, делать его более понятным, поддерживаемым и легко масштабируемым.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.