Как работать с паттернами | robot_dreams
Для отслеживания статуса заказа — авторизируйтесь
Введите код, который был выслан на почту Введите код с SMS, который был выслан на номер
 
Код действителен в течение 5 минут Код с sms действителен в течение 5 минут
Вы уверены, что хотите выйти?
Сеанс завершен
На главную
Как работать с паттернами: учимся обходить «грабли»

Как работать с паттернами: учимся обходить «грабли»

Топ ошибок, которые допускают новички, пытаясь писать чистый код

Хотя о чистом коде говорят часто, в разработке ПО паттерны проектирования не являются жестким стандартом. Более того, хотя паттерны решают массу проблем и задач — от дублирования кода до его нечитабельности, — рекомендации работают не всегда. Если слепо им доверять, можно получить результат, далекий от идеального.

В этой статье разбираем, какие ошибки при написании чистого кода и использовании паттернов новички совершают чаще всего.

Агрессивное использование паттернов

Шаблоны проектирования не всегда уместны, потому применять их стоит только тогда, когда они действительно необходимы. Принудительное (агрессивное) использование паттернов — это одна из самых частых ошибок в программировании. Чрезмерное задействование паттернов приводит к заметному усложнению кода — он становится избыточным.

Пример избыточности кода

В коде ниже используется паттерн проектирования «Фабричный метод», который позволяет создавать объекты без указания конкретного класса.

В этом примере класс AnimalFactory представляет фабрику для создания животных. Метод create_animal() принимает в качестве аргумента тип животного и возвращает объект этого типа

Использование паттерна «Фабричный метод» неуместно, потому что классы Dog и Cat не имеют сложной логики создания. В этом случае проще и понятнее создавать объекты напрямую, без использования фабрики, что и показано во второй части кода:

# Неуместное задействование фабричного метода для создания объекта
class AnimalFactory:
    def create_animal(self, animal_type):
        if animal_type == "Dog":
            return Dog()
        elif animal_type == "Cat":
            return Cat()
        # Также другие ветки для разных животных

# Правильный код: создание объектов напрямую без использования фабричного метода
dog = Dog()
cat = Cat()

Вот другой пример кода, больше приближенный к реальности. Здесь программист слишком часто использует паттерн Singleton.

Обычно он применяется, чтобы гарантировать, что в системе будет только один экземпляр определенного класса, и предоставляет глобальную точку доступа к этому экземпляру. Его основная цель — убедиться, что для определенного класса существует только один объект в системе, и обеспечить контроль доступа к этому объекту из разных частей программы.

class Database:
    """
    Класс для доступа к базе данных.
    """
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance
    def connect(self):
        """
        Подключение к базе данных.
        """
        ...
    def disconnect(self):
        """
        Отключение от базы данных.
        """
        ...
class UserRepository:
    """
    Класс для работы с пользователями.
    """
    def __init__(self):
        self.database = Database()
    def get_user(self, id):
        """
        Получение пользователя по ID.
        """
        ...
    def save_user(self, user):
        """
        Сохранение пользователя.
        """
        ...
class ProductRepository:
    """
    Класс для работы с продуктами.
    """

    def __init__(self):
        self.database = Database()

    def get_product(self, id):
        """
        Получение продукта по ID.
        """
        ...
    def save_product(self, product):
        """
        Сохранение продукта.
        """
        ...
class OrderRepository:
    """
    Класс для работы с заказами.
    """

    def __init__(self):
        self.database = Database()

    def get_order(self, id):
        """
        Получение заказа по ID.
        """
        ...
    def save_order(self, order):
        """
        Сохранение заказа.
        """
        ...

В этом коде паттерн Singleton переиспользуется в четырех классах Database, UserRepository, ProductRepository и OrderRepository. Это приводит к избыточности, так как код подключения к базе данных повторяется в каждом классе.

Чтобы избежать избыточности кода, можно переместить код подключения к базе данных в отдельный класс, например, DatabaseFactory. Затем можно использовать этот класс в качестве фабричного метода для создания экземпляров Database:

class DatabaseFactory:
    """
    Фабричный метод для создания экземпляров Database.
    """

    def create_database(self):
        """
        Создание экземпляра Database.
        """
        return Database()

class UserRepository:
    """
    Класс для работы с пользователями.
    """

    def __init__(self):
        self.database = DatabaseFactory.create_database()

    def get_user(self, id):
        """
        Получение пользователя по ID.
        """
        ...

    def save_user(self, user):
        """
        Сохранение пользователя.
        """
        ...

class ProductRepository:
    """
    Класс для работы с продуктами.
    """

    def __init__(self):
        self.database = DatabaseFactory.create_database()

    def get_product(self, id):
        """
        Получение продукта по ID.
        """
        ...

    def save_product(self, product):
        """
        Сохранение продукта.
        """
        ...


class OrderRepository:
    """
    Класс для работы с заказами.
    """

    def __init__(self):
        self.database = DatabaseFactory.create_database()

    def get_order(self, id):
        """
        Получение заказа по ID.
        """
        ...

    def save_order(self, order):
        """
        Сохранение заказа.
        """
        ...

В этом варианте кода подключение к базе данных находится в одном месте, что делает его более понятным и компактным.

Обратите внимание: второй вариант соответствует сразу двум принципам написания «чистого кода» — принцип инверсии зависимостей из SOLID и DRY (Don't Repeat Yourself).

Паттерны без учета контекста проекта

Прибегая к паттерну, нужна учитывать суть проекта и его требования. Помните, что паттерны не являются целью сами по себе — это всего лишь инструменты для улучшения кода. В противном случае применение паттернов будет лишено смысла.

Пример: паттерн «Стратегия»

Предположим, вы работаете над приложением для онлайн-магазина и решаете использовать паттерн «Стратегия» для реализации скидок на товары. Этот паттерн позволяет определить семейство алгоритмов, инкапсулировать каждый из них и делать их взаимозаменяемыми.

Исходно, у вас есть два типа скидок: фиксированная скидка в долларах и процентная. Вы создаете два класса, представляющих каждый из этих типов:

class FixedDiscount:
    def apply_discount(self, price, discount_amount):
        return price — discount_amount

class PercentageDiscount:
    def apply_discount(self, price, discount_percentage):
        return price * (1 — discount_percentage / 100)

Затем вы создаете класс, представляющий товар, и используете паттерн «Стратегия» для применения скидки:

class Product:
    def __init__(self, name, price, discount_strategy):
        self.name = name
        self.price = price
        self.discount_strategy = discount_strategy

    def apply_discount(self):
        return self.discount_strategy.apply_discount(self.price, self.discount_amount)

Теперь у вас есть гибкая система скидок и вы можете применять разные скидки к разным товарам:

fixed_discount = FixedDiscount()
percentage_discount = PercentageDiscount()

product1 = Product("Widget", 100, fixed_discount)
product2 = Product("Gadget", 200, percentage_discount)

discounted_price1 = product1.apply_discount()  # Используется фиксированная скидка
discounted_price2 = product2.apply_discount()  # Используется процентная скидка

Но применение паттерна «Стратегия» имеет смысл только в том случае, если у вас есть реальная потребность в динамическом выборе алгоритма в зависимости от контекста. Если ваш магазин всегда использует только один тип скидок и никогда не меняет его, применение паттерна «Стратегия» может быть избыточным.

Использование антипаттернов

Антипаттерн — это шаблон проектирования, программирования или архитектурного решения, которое, несмотря на свою видимую эффективность, приводит к нежелательным и негативным последствиям в разработке программного обеспечения. Антипаттерны не стоит использовать, так как они могут ухудшать качество кода, усложнять его поддержку и вносить путаницу в разработку проекта.

Примеры: «код-спагетти» и God Object

Ниже приведен один из антипаттернов — код-спагетти. В нем запутанная логика и нет четкой структуры. Как следствие — любое изменение в логике или добавление новой функциональности будет сложно ввести без риска сломать существующий код:

def calculate_total_price(cart):
    total_price = 0
    for item in cart:
        if item['product']['discount']:
            if item['product']['discount']['type'] == 'percentage':
                total_price += item['quantity'] * item['product']['price'] * (1 — item['product']['discount']['value'] / 100)
            elif item['product']['discount']['type'] == 'fixed':
                total_price += (item['quantity'] * item['product']['price']) — item['product']['discount']['value']
            else:
                total_price += item['quantity'] * item['product']['price']
        else:
            total_price += item['quantity'] * item['product']['price']
    return total_price

Другой антипаттерн, с которым можно часто столкнуться на практике, — God Object. Ключевая особенность этого антипаттерна заключается в том, что он нарушает принцип единственной ответственности (Single Responsibility Principle) в рекомендациях SOLID.

Поскольку все методы связаны в одном классе, это затрудняет изменение или добавление новой функциональности. Код получается менее поддерживаемым, трудно понимаемым и тяжело расширяемым:

class GodObject:
    def __init__(self):
        self.users = []
        self.products = []
        self.orders = []

    def get_user_by_id(self, id):
        for user in self.users:
            if user.id == id:
                return user
        return None

    def get_product_by_id(self, id):
        for product in self.products:
            if product.id == id:
                return product
        return None

    def get_order_by_id(self, id):
        for order in self.orders:
            if order.id == id:
                return order
        return None

    def add_user(self, user):
        self.users.append(user)

    def add_product(self, product):
        self.products.append(product)

    def add_order(self, order):
        self.orders.append(order)

god_object = GodObject()

user = god_object.get_user_by_id(1)
product = god_object.get_product_by_id(2)
order = god_object.get_order_by_id(3)

god_object.add_user(user)
god_object.add_product(product)
god_object.add_order(order)

Вместо использования God Object рекомендуется разделить ответственности на более мелкие и модульные компоненты. В этом конкретном случае целесообразнее создать отдельные классы для пользователей, продуктов и заказов, каждый из которых будет иметь собственную функциональность и методы. Это поможет сделать код более чистым и управляемым.

Пример исправленного кода:

class User:
    def __init__(self, id):
        self.id = id

class Product:
    def __init__(self, id):
        self.id = id

class Order:
    def __init__(self, id):
        self.id = id

class GodObject:
    def __init__(self):
        self.users = []
        self.products = []
        self.orders = []

    def add_user(self, user):
        self.users.append(user)

    def add_product(self, product):
        self.products.append(product)

    def add_order(self, order):
        self.orders.append(order)

    def get_user_by_id(self, id):
        for user in self.users:
            if user.id == id:
                return user
        return None

    def get_product_by_id(self, id):
        for product in self.products:
            if product.id == id:
                return product
        return None

    def get_order_by_id(self, id):
        for order in self.orders:
            if order.id == id:
                return order

# Использование:
god_object = GodObject()
user = User(1)
product = Product(2)
order = Order(3)

god_object.add_user(user)
god_object.add_product(product)
god_object.add_order(order)

retrieved_user = god_object.get_user_by_id(1)
retrieved_product = god_object.get_product_by_id(2)
retrieved_order = god_object.get_order_by_id(3)

Изменение кодовой базы под паттерн

Внедрение паттерна может потребовать больших изменений и даже потенциально нарушить работу программного обеспечения. Поэтому используйте паттерны с самого начала проекта или во время рефакторинга, чтобы избежать радикальных изменений уже имеющегося кода. Внедрение паттерна требует внимательного анализа и тестирования, чтобы убедиться, что новая архитектура не нарушает работу существующего программного обеспечения.

Пример: имплементация паттерна «Мост»

Вот пример такой ситуации. Предположим, есть код, который управляет разными устройствами и их функциональностью. Устройства могут быть телевизорами и радио, и у каждого из них есть свои уникальные методы и свойства:

class TVRemote:
    def power_on(self):
        # Включить телевизор
        pass

    def power_off(self):
        # Выключить телевизор
        pass

    def change_channel(self, channel):
        # Сменить канал
        pass

class RadioRemote:
    def turn_on(self):
        # Включить радио
        pass

    def turn_off(self):
        # Выключить радио
        pass

    def set_frequency(self, frequency):
        # Установить частоту
        pass

Цель: добавить новую функциональность, которая позволит управлять устройствами через пульт дистанционного управления с возможностью программирования. Для этого принимается решение внедрить паттерн «Мост», который разделяет абстракцию (в данном случае — пульт дистанционного управления) от реализации (устройства).

Создаем абстракцию RemoteControl и классы-реализации TVRemoteControl и RadioRemoteControl:

class RemoteControl:
    def __init__(self, device):
        self.device = device

    def power_on(self):
        self.device.power_on()

    def power_off(self):
        self.device.power_off()

    def set_channel(self, channel):
        self.device.change_channel(channel)

И классы-реализации:

class TVRemoteControl(RemoteControl):
    pass

class RadioRemoteControl(RemoteControl):
    def set_frequency(self, frequency):
        self.device.set_frequency(frequency)

Теперь вы можете использовать абстракцию RemoteControl, чтобы управлять устройствами через пульт дистанционного управления:

tv_remote = TVRemote()
radio_remote = RadioRemote()

tv_controller = TVRemoteControl(tv_remote)
radio_controller = RadioRemoteControl(radio_remote)

tv_controller.power_on()
tv_controller.set_channel(5)

radio_controller.power_on()
radio_controller.set_frequency(101.5)

Даже на таком небольшом участке кода процедура внедрения паттерна «Мост» потребовала больших изменений в существующем коде, так как нужно было разделить абстракцию и реализацию, а также создать новые классы-реализации. Все это могло бы привести к потенциальным нарушениям работы программы, если бы изменения не выполнялись аккуратно и не были учтены все зависимости от старого кода.

Неправильный выбор паттерна

Использование сложных конструкций может привести к плохому пониманию архитектуры приложения. Поэтому рекомендуется по возможности выбирать лаконичные и простые решения, которые понятны и легко реализуются. Если выбирать паттерн исходя из соображений «работает — вот и хорошо», можно получить на выходе продукт, который будет сложно масштабировать, а у команды разработчиков уйдет много времени на анализ его архитектуры.

Сложный и простой примеры реализации функциональности

Предположим, мы хотим реализовать систему управления заказами для интернет-магазина. Принято решение имплементировать поведенческий паттерн «Команда», который может разделить запросы от клиентов на конкретные команды для выполнения позднее.

from abc import ABC, abstractmethod

# Абстрактный класс команды
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

# Конкретная команда — создание заказа
class CreateOrderCommand(Command):
    def __init__(self, order):
        self.order = order

    def execute(self):
        self.order.create()

# Конкретная команда — отмена заказа
class CancelOrderCommand(Command):
    def __init__(self, order):
        self.order = order

    def execute(self):
        self.order.cancel()

# Класс заказа
class Order:
    def create(self):
        print("Заказ создан")

    def cancel(self):
        print("Заказ отменен")

# Клиентский код
order = Order()
create_command = CreateOrderCommand(order)
cancel_command = CancelOrderCommand(order)

create_command.execute()
cancel_command.execute()

Очевидно, что такое решение усложняет код — появляются дополнительные абстракции, излишние для такой простой задачи, как управление заказами.

Вместо использования паттерна можно было написать более лаконичный вариант:

# Простой класс заказа
class Order:
    def create(self):
        print("Заказ создан")

    def cancel(self):
        print("Заказ отменен")

# Клиентский код
order = Order()
order.create()
order.cancel()

Отсутствие документации

Хотя при правильном выборе и использовании паттерна он упрощает код, это не означает, что тот становится автоматически понятным всем остальным. Поэтому всегда ведите документацию.

Более того, процесс документирования заставляет и самих разработчиков задумываться над логикой и структурой кода, что может приводить к продуктивным идеям по улучшению его качества и читаемости.

Документацию можно написать в виде текстового документа, аннотаций к коду или другого формата. Важно, чтобы описание было понятным и доступным.

Пример с паттерном «Наблюдатель»

Важность документации можно проследить на примере паттерна «Наблюдатель». Этот шаблон используется для создания зависимости между объектами, когда при изменении состояния одного объекта все зависящие от него автоматически уведомляются и обновляются.

Предположим, у нас есть проект, в котором используется паттерн «Наблюдатель» для реализации функциональности уведомлений о событиях. Важно объяснить, как этот паттерн работает, чтобы другие разработчики могли применять его правильно.

Пример без документации:

class EventSubject:
    def __init__(self):
        self._observers = []

    def add_observer(self, observer):
        self._observers.append(observer)

    def remove_observer(self, observer):
        self._observers.remove(observer)

    def notify_observers(self, event):
        for observer in self._observers:
            observer.update(event)

class EventObserver:
    def update(self, event):
        print(f"Received event: {event}")

subject = EventSubject()
observer1 = EventObserver()
observer2 = EventObserver()

subject.add_observer(observer1)
subject.add_observer(observer2)

subject.notify_observers("Event 1")
subject.notify_observers("Event 2")

То, как работает этот код и как именно организована связь между EventSubject и EventObserver, не очевидно. Другие программисты могут не понимать, что EventSubject действительно является объектом, наблюдающим за событиями, и как именно происходит уведомление.

Заключение

Проверенные шаблоны проектирования экономят время. Они помогают создавать код, который будет понятным, поддерживаемым и масштабируемым. Но только при условии их правильного использования.

Подводя итог вышесказанного, можно вывести ряд советов по корректному применению паттернов:

  • Тщательно анализируйте задачу, выясняя, какой паттерн наилучшим образом соответствует ее решению.
  • Используйте паттерны только там, где они точно нужны.
  • Добавляйте документацию и комментарии к коду, чтобы объяснить, почему и какой паттерн используется. Это поможет другим разработчикам понять вашу логику.
  • Следите за тем, чтобы код соответствовал принципам SOLID.
  • Перед тем, как внедрять паттерн в проект, убедитесь, что он проходит тестирование и не вносит ошибок.
  • Не каждый паттерн легко применим во всех языках программирования. Убедитесь, что выбранный паттерн подходит для вашего языка. Если возможно, используйте стандартные библиотеки и реализации паттернов для вашего языка, чтобы уменьшить сложность кода и повысить его стабильность.
Ещё статьи
Экспертки о том, как оценивают кандидатов на нетехнических интервью
Часть 2. Работа с записями: вставка, чтение, изменение и удаление