Узнайте, как избавиться от запаха кода с геттерами и перенести бизнес-логику внутрь объектов. Практические примеры на Python и советы по рефакторингу.
Представьте: у вас есть объект Food с геттером use_by_date() для отображения срока годности на этикетке. Всё законно. Но затем вам нужно удалить просроченные продукты — и вы пишете функцию, которая дёргает этот геттер и сравнивает дату с сегодняшней. Потом появляется ещё одна функция с похожей логикой — и вот уже два места в коде решают, что значит «просрочено», а сам объект Food остаётся в стороне. Это классический запах кода, который ведёт к дублированию и хрупкому рефакторингу.
В этой статье разберём, почему переиспользование геттера для новой бизнес-логики — ловушка, и как правильно добавить метод в объект, следуя принципу «Tell, Don't Ask».
Когда вы используете геттер для вычислений вне объекта, вы нарушаете инкапсуляцию: объект перестаёт контролировать свои данные. Каждый вызывающий код интерпретирует значение по-своему, что ведёт к рассинхронизации правил. Рассмотрим пример на Python.
class Food:
def __init__(self, name, use_by_date):
self.name = name
self._use_by_date = use_by_date
def use_by_date(self):
return self._use_by_date
# Внешняя функция: удаляем просроченные
def remove_expired_food(shelf, today):
return [food for food in shelf if food.use_by_date() < today]
# Другая функция: почти та же логика
def flag_near_expiry(shelf, today, warning_days):
result = []
for food in shelf:
days_left = (food.use_by_date() - today).days
if 0 <= days_left <= warning_days:
result.append(food)
return resultЗдесь remove_expired_food и flag_near_expiry сами решают, что такое «просрочено» или «скоро истечёт». Если завтра правила изменятся (например, просрочка считается по-другому), придётся править обе функции. А объект Food остаётся просто хранилищем данных.
Правильный путь — добавить в объект методы, которые отвечают на вопросы о его состоянии. Тогда логика живёт там, где данные, и все вызывающие используют единую реализацию.
from datetime import date, timedelta
class Food:
def __init__(self, name, use_by_date):
self.name = name
self._use_by_date = use_by_date
def use_by_date(self):
# Геттер остаётся только для отображения
return self._use_by_date
def is_expired_on(self, today):
return self._use_by_date < today
def days_until_expiry(self, today):
return (self._use_by_date - today).days
# Теперь внешние функции просто используют методы
def remove_expired_food(shelf, today):
return [food for food in shelf if food.is_expired_on(today)]
def flag_near_expiry(shelf, today, warning_days):
return [food for food in shelf
if 0 <= food.days_until_expiry(today) <= warning_days]Теперь логика сравнения находится внутри Food. Если правила изменятся, вы правите только класс. Все вызывающие используют один и тот же метод, поэтому рассинхронизация исключена.
Это ручная проверка — линтер не поможет. Ищите геттеры, которые вызываются в нескольких местах, и каждый раз вокруг них строится своя логика сравнения (if, filter, сортировка). Если два вызова читают один и тот же геттер и по-разному интерпретируют результат — объекту не хватает метода.
DTO (Data Transfer Objects) иногда используют геттеры для передачи данных, но это не оправдание: даже в DTO можно добавить методы, если появляется бизнес-логика. Запах возникает, когда вы переиспользуете геттер как костыль, вместо того чтобы добавить метод.
Прямо сейчас: найдите в своём коде геттер, который используется для сравнения или фильтрации вне класса. Замените внешние сравнения методами объекта. Начните с одного класса — и вы увидите, как код станет чище и устойчивее к изменениям.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →