Проектирование и архитектура ПО

Архитектор данных
Содержание

Что такое проектирование программного обеспечения?

Представьте, что вы хотите построить дом. Вы вряд ли сразу начнёте класть кирпичи, правда? Сперва нужно понять, каков будет фундамент, где пройдут стены и коммуникации. Так и в разработке программного обеспечения.

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

По сути, проектирование — это ваш «чертёж», который помогает избежать бардака в коде и сделать так, чтобы приложение было понятно не только вам (сейчас), но и вашей команде, будущим разработчикам и даже вам самим… через год.

Понятие и важность проектирования

Важно понимать, что проектирование — это не просто формальность. Это фундамент, обеспечивающий:

  • Простоту понимания. Чтобы можно было объяснить другому человеку, где какой модуль и зачем он нужен.
  • Гибкость. Код со временем развивается, появляются новые требования — и нужно уметь безболезненно что-то добавить или изменить.
  • Масштабируемость. Если ваша система становится популярной и растёт, она должна спокойно выдерживать большие нагрузки (и корректировки архитектуры).

Основные цели проектирования: простота, гибкость, масштабируемость

  • Простота: Упрощаем там, где можно. Порой хочется «закрутить гайки» потуже, но потом в этой сложной схеме сам застрянешь.
  • Гибкость: Должны предусмотреть, что завтра что-то изменится. К примеру, вдруг решите вместо локальной базы данных подключиться к облачной — а ваш код к этому не готов.
  • Масштабируемость: Вдруг на вас обрушилась волна популярности, и теперь вместо 100 пользователей у вас 10 000? Хорошо, если система это «переживёт» без тотальной перестройки.

Основные термины и определения

Интерфейс

  • Что это? Интерфейс — это «договор» или «способ взаимодействия» между компонентами (классами, модулями). Он описывает, какие методы (функции) и данные доступны «снаружи», не раскрывая деталей реализации.
  • Почему важно? Интерфейс позволяет скрыть внутреннюю кухню класса или модуля и объявляет, что именно мы можем вызвать или получить. Он даёт предсказуемый способ использования функционала.
  • Пример: У кофемашины есть кнопка «Сделать эспрессо» — это интерфейс. Как именно она там нагревает воду, смешивает кофе — нам не важно (это реализация).

Модули верхнего уровня и нижнего уровня (в контексте Dependency Inversion Principle)

  • Модули верхнего уровня — это части программы, которые решают «высокоуровневые» задачи или предоставляют бизнес-логику. Они определяют, что «делает» ваша система.
    • Пример: «Сервис управления пользователями» (UserService), который регистрирует, авторизует людей, следит за доступами.
  • Модули нижнего уровня — это детали реализации, которые решают «технические» задачи и обслуживают верхний уровень: чтение/запись данных в базу, отправка писем, подключение к сторонним сервисам и т.д.
    • Пример: «Репозиторий пользователей» (UserRepository), который обращается в базу данных.

Согласно принципу Dependency Inversion, модули верхнего уровня не должны зависеть от деталей нижнего уровня. Вместо этого оба уровня зависят от абстракций.

Абстракции (в контексте Dependency Inversion Principle)

  • Что это? Абстракция — это общее описание поведения или набора свойств, не привязанное к конкретной реализации. Чаще всего в коде абстракция представлена интерфейсом или абстрактным классом.
  • Почему важно? Абстракции позволяют «подменять» реализацию без изменения логики верхнего уровня. Если вам завтра потребуется заменить доступ к базе данных (допустим, с PostgreSQL на MongoDB), вы меняете только реализацию, а не весь верхнеуровневый модуль.
  • Пример: Интерфейс Database с методами connect() и execute_query(query). В одной реализации эти методы работают с PostgreSQL, в другой — с MySQL, а в третьей — с тестовой «фейковой» базой.

Паттерн (шаблон проектирования)

  • Что это? Паттерн или шаблон проектирования — это «готовый рецепт» решения типовой проблемы. Он описывает универсальный способ организации кода для решения определённой задачи.
  • Почему важно? Паттерны помогают писать код более структурированно, избегать повторения и учиться на опыте сообщества разработчиков. Вместо reinventing the wheel (изобретать велосипед заново) вы берёте проверенный подход и адаптируете его к своей ситуации.
  • Пример: Singleton (синглтон) — шаблон для создания единственного экземпляра класса (аккуратно с ним, иногда это приводит к трудностям в тестировании).

Компонент

  • Что это? Компонент — часть системы, которая выполняет конкретную роль. Может быть отдельным сервисом, модулем, классом или группой классов.
  • Зачем это нужно? Разделение приложения на компоненты помогает поддерживать порядок в проекте: вы знаете, кто за что «отвечает», и можете эти части независимо дорабатывать.

Класс и Объект (в контексте ООП)

  • Класс — это «чертёж» или «шаблон» для создания объектов. Определяет свойства (атрибуты) и поведение (методы).
  • Объект — это конкретный «экземпляр» класса.
    • Пример: Класс «Кошка» описывает, какие у кошки есть характеристики (цвет шерсти, возраст) и что она умеет делать (мяукать, мурлыкать). Объект «Мурзик» — это конкретная кошка со своими значениями этих характеристик.

Архитектура

  • Что это? Архитектура — это общий план того, как части (модули, компоненты, слои) приложения устроены и взаимодействуют друг с другом.
  • Почему важно? Хорошо продуманная архитектура делает систему понятной и расширяемой. Вы можете менять отдельные куски, не ломая остальные.
  • Пример: Микросервисная архитектура, где каждая часть (например, сервис авторизации, сервис каталога товаров, сервис корзины) живёт в своём «контейнере» и общается с другими через HTTP-запросы.

Рефакторинг

  • Что это? Рефакторинг — это процесс улучшения существующего кода без изменения его внешнего поведения. Переписываем «наследие» так, чтобы было красивее, понятнее и надёжнее.
  • Зачем он? Позволяет уберечь систему от нарастающего хаоса, когда маленькие изменения множатся и превращают код в кашу.

Библиотека и Фреймворк

  • Библиотека: Набор готовых модулей/классов/функций, которые вы вызываете при необходимости. Управление потоком выполнения (когда что вызывать) остаётся за вами.
  • Фреймворк: Определённая «рамка» (frame) для вашего приложения. Он задаёт архитектуру и часто сам вызывает ваш код в нужный момент. Здесь уже фреймворк диктует правила игры.

Эволюция методологий разработки ПО

Методологии можно представить как «стили» разработки и управления проектом:

  • Waterfall (водопадная модель): cтруктурированный подход. Сначала анализ, потом дизайн, потом реализация и так далее, поэтапно и последовательно. Проблема — слишком статичная, сложно вносить изменения на поздних этапах.
  • Agile (гибкие методологии): короткие итерации (спринты), быстрая обратная связь, адаптация к изменяющимся требованиям. Это как если бы вы строили дом не полностью разом, а постепенно: сначала комнаты, проверяете, устраивает ли заказчика, потом достраиваете ещё и т.д.
  • DevOps: сочетание разработки (Dev) и операций (Ops). Главная идея — процесс поставки кода на «прод» (продакшн) должен быть быстрым, автоматизированным и непрерывным.

Все эти методологии эволюционировали, реагируя на запросы рынка: «как быстрее и надёжнее создавать сложные системы?».

waterfall, agile и devops подходы к разработке ПО

Роль архитектуры в разработке сложных систем

Когда система вырастает из простого скрипта на 50 строк, может возникнуть хаос. Архитектура нужна, чтобы этот хаос упорядочить:

  • Определить, какие у нас есть подсистемы и как они взаимодействуют.
  • Продумать, где хранить данные (база данных? файлы? облако?).
  • Определить протоколы взаимодействия (REST, gRPC, SOAP… да хоть почтовыми голубями, но это, скорее всего, не вариант).
  • Учитывать скорость разработки, надёжность, безопасность и масштабируемость.

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

Виды архитектуры

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

  1. Монолитная архитектура: В такой системе весь код объединен в один большой блок. Это как одноэтажный дом, где все комнаты связаны между собой. Этот подход прост в реализации и используется для небольших проектов, но при росте системы монолит может стать слишком громоздким.Монолитная архитектура
  2. Микросервисная архитектура: Это как многоэтажное здание, где каждый этаж отвечает за свою функцию. В микросервисной архитектуре система разбита на небольшие, независимые сервисы, каждый из которых выполняет свою задачу. Например, в системе бронирования отелей может быть отдельный сервис для поиска отелей, другой — для обработки платежей и так далее. Преимущество этого подхода в том, что можно масштабировать и обновлять каждый сервис независимо от других.микросервисная архитектура
  3. Многослойная архитектура (n-tier): Эта архитектура напоминает многослойный торт. Каждый слой отвечает за свою часть функционала: интерфейс пользователя, бизнес-логика, данные и так далее. Это позволяет изолировать различные части системы друг от друга, что облегчает их модификацию и тестирование.Многослойная архитектура

Основные принципы объектно-ориентированного проектирования (ООП)

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

Инкапсуляция

Инкапсуляция — это принцип, согласно которому данные и методы, которые работают с этими данными, объединяются в одном объекте, скрывая внутреннюю реализацию от внешнего мира.

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

Пример на Python:

class BankAccount:
    def __init__(self, account_holder, balance=0):
        self.account_holder = account_holder
        self.__balance = balance  # Баланс скрыт от внешнего мира

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"Deposited {amount}. New balance is {self.__balance}")
        else:
            print("Deposit amount must be positive")

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"Withdrew {amount}. New balance is {self.__balance}")
        else:
            print("Invalid withdraw amount")

    def get_balance(self):
        return self.__balance

# Пример использования
account = BankAccount("Alice", 100)
account.deposit(50)  # Пополнение счета
account.withdraw(30)  # Снятие со счета
print(account.get_balance())  # Проверка баланса

# Попробуем получить доступ к скрытому атрибуту
print(account.__balance)  # Ошибка: AttributeError

В этом примере баланс счета (__balance) скрыт от внешнего доступа. Инкапсуляция помогает защитить данные и предоставляет методы для работы с ними.

Наследование

Наследование — это механизм, позволяющий одному классу (дочернему) унаследовать свойства и методы другого класса (родительского).

Позволяет «переиспользовать» код. Класс «Кошка» может унаследоваться от класса «Животное». Унаследованный класс (Кошка) автоматически получает всё от класса-родителя (Животное), а ещё может добавить своё (например, метод «Мурлыкать»).

Пример на Python:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass  # Абстрактный метод, который должен быть переопределен в дочерних классах

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Пример использования
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.speak())  # Buddy says Woof!
print(cat.speak())  # Whiskers says Meow!

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

Полиморфизм

Полиморфизм позволяет объектам разных классов обрабатывать вызовы одних и тех же методов по-разному.

Способность объектов с одинаковым интерфейсом вести себя по-разному. Пример: метод draw() у класса «Круг» рисует круг, а у класса «Прямоугольник» — прямоугольник. Но с точки зрения кода мы всего лишь вызываем .draw() у объекта — и получаем нужную фигуру.

Пример на Python:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Subclasses must implement this method")

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Пример использования полиморфизма
animals = [Dog("Buddy"), Cat("Whiskers")]

for animal in animals:
    print(animal.speak())

Здесь метод speak реализован в каждом из дочерних классов по-своему, но для внешнего мира все объекты ведут себя одинаково — они умеют «говорить». Благодаря полиморфизму мы можем обрабатывать объекты различных классов единообразно.

Абстракция

Абстракция — это процесс выделения важных характеристик объекта и игнорирования несущественных деталей.

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

Пример на Python:

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        return "Car engine started"

class Motorcycle(Vehicle):
    def start_engine(self):
        return "Motorcycle engine started"

# Пример использования абстракции
vehicles = [Car(), Motorcycle()]

for vehicle in vehicles:
    print(vehicle.start_engine())

В этом примере класс Vehicle является абстрактным, и он определяет метод start_engine, который должен быть реализован в дочерних классах. Абстракция позволяет описывать общие концепции и оставлять детали реализации на уровне конкретных классов.

Задание 1. Инкапсуляция: Создайте класс User с инкапсулированными полями __password и __email. Реализуйте методы для безопасного изменения и получения этих данных, соблюдая принципы инкапсуляции.

Задание 2. Наследование: Разработайте систему классов для управления парком транспортных средств (автомобили, мотоциклы, грузовики). Определите базовый класс Vehicle и создайте дочерние классы, наследующие общие свойства и методы.

Задание 3. Полиморфизм: Создайте несколько классов, реализующих общий интерфейс (например, разные виды платежных систем). Напишите программу, которая принимает список объектов этих классов и вызывает их методы одинаково, используя полиморфизм.

Задание 4. Абстракция: Реализуйте абстрактный класс Shape с методами area и perimeter, которые должны быть определены в дочерних классах Rectangle и Circle. Создайте несколько объектов этих классов и вычислите их площади и периметры.

Принципы SOLID

Принципы SOLID — это набор из пяти основных правил, разработанных для того, чтобы помочь программистам создавать более гибкий, понятный и поддерживаемый код.

Эти принципы особенно важны в объектно-ориентированном проектировании (ООП). Рассмотрим каждый из них по отдельности, с примерами их реализации и нарушений, а также с объяснением последствий нарушений.

Принципы единственной ответственности (Single Responsibility Principle, SRP) и открытости/закрытости (Open/Closed Principle, OCP)

Сюжет: Представим, что мы разрабатываем интернет-магазин. У нас есть класс OrderManager, который отвечает за:

  1. Создание заказа и работу с данными о заказе.
  2. Применение скидок к заказу (процентные, фиксированные и т.д.).
  3. Отправку уведомлений клиенту.

На первый взгляд кажется логичным «свалить» всё в один класс — ведь всё это «работа с заказом». Но со временем код начинает разрастаться, возникают проблемы при изменении, и команда разработчиков начинает путаться.

Как помогают SRP и OCP:

  • SRP (Single Responsibility Principle) говорит: у класса должна быть только одна зона ответственности. Если OrderManager одновременно занимается и скидками, и уведомлениями, и данными о заказе, — это уже несколько разных «обязанностей».
  • OCP (Open/Closed Principle) гласит, что класс (или модуль) должен быть открыт для расширения, но закрыт для модификации. Когда нам нужно добавить новый тип скидки (например, скидку по промокоду), желательно не трогать старый код, а расширять систему новым компонентом.

Давайте посмотрим на исходный код (сильно упрощённый, но достаточно «живой»), который пишет начинающий разработчик:

class OrderManager:
    def __init__(self, items):
        """
        items - список (product_id, quantity, price)
        """
        self.items = items

    def calculate_total(self):
        total = sum(qty * price for _, qty, price in self.items)
        return total

    def apply_discount(self, discount_type, amount):
        """
        discount_type может быть 'percent' или 'fixed'
        amount - числовое значение скидки
        """
        if discount_type == 'percent':
            total = self.calculate_total()
            discounted_total = total - total * (amount / 100.0)
            return discounted_total
        elif discount_type == 'fixed':
            total = self.calculate_total()
            return total - amount
        else:
            print("Unknown discount type!")
            return self.calculate_total()

    def send_notification(self, customer_email):
        """
        Отправка письма пользователю
        """
        total = self.calculate_total()
        print(f"Sending email to {customer_email} about order total {total}")

    def place_order(self, customer_email):
        """
        Финальный метод для оформления заказа
        """
        total_with_discount = self.apply_discount('percent', 10)  # Всегда 10% скидка!
        print(f"Order placed. Final total: {total_with_discount}")
        self.send_notification(customer_email)

Где видны нарушения:

  1. SRP: Класс OrderManager и рассчитывает итоговую стоимость, и управляет скидками (причём внутри метода), и отправляет уведомления. Это как минимум три разные обязанности.
  2. OCP: При добавлении новой скидки (например, «2+1 бесплатно»), придётся менять метод apply_discount. Наш класс не закрыт для модификации, и чем дальше, тем больше if/else будет расползаться.

Последствия:

  • Если бизнес захочет поменять логику отправки уведомлений (например, отправлять SMS вместо email) — придётся лезть в тот же класс OrderManager.
  • Если появится новая скидка (по промокоду, сезонная, VIP…) — опять правим apply_discount.
  • Тестировать всё становится сложнее, так как один класс содержит и логику оформления заказа, и скидки, и уведомления.

Шаг 1: Выделяем «Сервис скидок» (исправление SRP и подготовка к OCP)

Чтобы не нарушать SRP, уберём логику скидок из OrderManager и вынесем её в отдельный сервис, который будет расширяться (OCP).

class DiscountStrategy:
    """
    Абстракция для разных стратегий скидок.
    """
    def apply_discount(self, total: float) -> float:
        raise NotImplementedError


class PercentDiscount(DiscountStrategy):
    def __init__(self, percent):
        self.percent = percent

    def apply_discount(self, total: float) -> float:
        return total - total * (self.percent / 100.0)


class FixedDiscount(DiscountStrategy):
    def __init__(self, amount):
        self.amount = amount

    def apply_discount(self, total: float) -> float:
        return total - self.amount


class OrderManager:
    def __init__(self, items, discount_strategy: DiscountStrategy = None):
        self.items = items
        # Если стратегия не передана, используем "нулевую" скидку по умолчанию
        self.discount_strategy = discount_strategy or DiscountStrategy()

    def calculate_total(self):
        return sum(qty * price for _, qty, price in self.items)

    def place_order(self, customer_email):
        base_total = self.calculate_total()
        final_total = self.discount_strategy.apply_discount(base_total)
        print(f"Order placed. Final total: {final_total}")

        # Уведомления пока ещё здесь (мы поправим это на следующем шаге)
        print(f"Sending email to {customer_email} about order total {final_total}")

Что изменилось:

  1. apply_discount превратился в использование объекта DiscountStrategy.
  2. Класс OrderManager теперь занимается в основном оформлением заказа: считает базовую сумму, вызывает стратегию скидки.
  3. Мы можем добавлять новые классы со своими скидками (например, PromoCodeDiscount), не трогая код OrderManager — он «открыт для расширения, закрыт для модификации» (OCP).

Шаг 2: Выделяем «Сервис уведомлений» (окончательный переход к SRP)

Теперь уберём из OrderManager логику отправки уведомлений. Это отдельная ответственность, и, возможно, у нас будет несколько способов оповещения.

class NotificationService:
    def notify(self, customer_email, total):
        print(f"Sending email to {customer_email} about order total {total}")
        # в будущем можем добавить SMS / push и т.д.

class OrderManager:
    def __init__(self, items, discount_strategy: DiscountStrategy, notifier: NotificationService):
        self.items = items
        self.discount_strategy = discount_strategy
        self.notifier = notifier

    def calculate_total(self):
        return sum(qty * price for _, qty, price in self.items)

    def place_order(self, customer_email):
        base_total = self.calculate_total()
        final_total = self.discount_strategy.apply_discount(base_total)
        print(f"Order placed. Final total: {final_total}")

        # Передаём уведомление в отдельный сервис
        self.notifier.notify(customer_email, final_total)

Плюсы:

  1. SRP:
    • OrderManager только управляет заказом и считает базовую сумму;
    • DiscountStrategy и её наследники только рассчитывают скидку;
    • NotificationService только отправляет уведомления.
  2. OCP:
    • Хочешь новую скидку? Добавляешь новый класс, унаследованный от DiscountStrategy.
    • Хочешь другой вид уведомления? Расширяешь или заменяешь NotificationService.

Таким образом, любое изменение в одном блоке не ломает остальные и не требует лезть в «центральный» класс.

Пример UML (упрощённо):

     +-------------------+            +-------------------+
     |  OrderManager    |            | NotificationService|
     |------------------|            |--------------------|
     | - items          |            | + notify(email,t)  |
     | - discount_strat |            |                    |
     | - notifier       |            |                    |
     |------------------|            |--------------------|
     | + calculate_total()           |
     | + place_order(email)          |
     +---------------+---------------+
                     |
                     v
           +-------------------+
           |DiscountStrategy  |
           |(abstract class)  |
           |------------------|
           | + apply_discount() <- overriden
           +---------^---------+
                     |
    +----------------+---------------+
    |                                |
+----------+                    +-------------+
|PercentDisc|                    | FixedDisc  |
|---------- |                    |------------|
| - percent |                    | - amount   |
|---------- |                    |------------|
| override..|                    | override...|
+-----------+                    +-------------+

Почему важен SRP?

  • Если всё валить в один класс, то любое новое требование (например, слать уведомления клиенту ещё и по WhatsApp) приводит к постоянному расширению одного «монстр-класса».
  • При раздельной структуре нам не придётся «трогать» OrderManager, если меняется способ уведомлений.

Почему важен OCP?

  • Если бизнес придумывает новую скидку, мы просто дописываем ещё одну стратегию и встраиваем её — не ломаем имеющийся код.
  • Код становится менее хрупким и проще в поддержке.

Задание 5: «Найти нарушения»

  1. Получите «плохой» код (тот, что мы приводили в начале блока, где всё в одном классе OrderManager).
  2. Определите, какие принципы нарушены и где конкретно (укажите строки или логическую часть).
  3. Объясните, почему это создаёт сложности в реальном проекте.

Задание 6: «Исправить код»

  1. Разделите логику так, чтобы класс, отвечающий за заказ, не занимался скидками и уведомлениями.
  2. Добавьте как минимум два вида скидок (например, процентная и фиксированная) и одну реализацию уведомлений (email).
  3. Убедитесь, что основной класс заказа не меняется, когда вы добавляете новый тип скидки.

Задание 7: «Дефенс-код-ревью» (групповое обсуждение)

  1. Презентация: Каждый участник (или группа) показывает, как они решили задание 2.
  2. Обсуждение: В формате код-ревью остальные слушатели (или преподаватель) задают вопросы:
    • «Что будет, если мы хотим добавить ещё один тип уведомлений?»
    • «Вы точно не нарушили где-то OCP?»
    • «Что, если мы хотим применять сразу две скидки подряд?» (например, и фиксированную, и процентную).
  3. Защита решений: Автор кода аргументирует, почему его структура (или архитектура) удобна в поддержке, как она масштабируется.
  1. SRP упрощённо звучит так: «Одна ответственность — один класс». Это снижает связанность и упрощает изменение кода.
  2. OCP гласит: «Добавлять функциональность можно, не изменяя старый код». Это повышает расширяемость и снижает риск багов.
  3. В реальном интернет-магазине важно, чтобы модуль «заказа» не зависел напрямую от всей остальной логики — скидок, уведомлений и т.д. Таким образом мы сохраняем гибкость при доработках.
  4. Практические домашние задания с «плохим кодом» и рефакторингом — лучший способ понять, как эти принципы работают вживую.

 

Принцип LSP (Liskov Substitution Principle)

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

На человеческом языке: если у вас есть родительский класс Animal с методом move(), то все потомки Bird, Fish или Cat должны корректно реализовывать этот метод, не ломая общее поведение. Другими словами, поведение потомка не должно «сюрпризить» код, который ожидает родительский тип.

В контексте интернет-магазина это может проявиться, например, в классе Product. Представим, что у нас есть метод ship(), который отправляет физический товар. Но вдруг появляется «продукт» DigitalProduct, который не требует физической доставки. Если у DigitalProduct метод ship() «выбросит ошибку» или окажется бессмысленным, значит, в коде может быть нарушение LSP: не всякий «Product» умеет «ship()» корректно.

Пример «неправильного» кода (нарушение LSP)

Сюжет: у нас есть базовый класс Product c методом ship(), который создаёт заявку на доставку. Два потомка: PhysicalProduct (физический товар) и DigitalProduct (цифровая копия игры, музыки и т.д.).

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

    def ship(self, address):
        # Предположим, что это обязательный метод, отсылающий товар
        print(f"Shipping {self.name} to {address}")

class PhysicalProduct(Product):
    pass  # Нас всё устраивает

class DigitalProduct(Product):
    def ship(self, address):
        # Проблема: цифровой продукт не нужно физически доставлять!
        raise NotImplementedError("Digital products can't be shipped!")

Где нарушение:

  • Любой код, который ожидает объект типа Product (и вызывает ship()), сломается, если туда попадёт DigitalProduct.
  • Значит, потомок DigitalProduct не является корректным подтипом с точки зрения базового метода ship() — это и есть нарушение LSP.

Пошаговый рефакторинг (исправляем LSP)

Идея: не все продукты доставляются физически, поэтому нельзя требовать ship() от всех наследников Product. Логично ввести дополнительную абстракцию, например, ShippableProduct, и сделать DigitalProduct отдельным наследником, который не реализует ship().

class Product:
    """
    Общая логика (название, цена и т.д.), 
    но без методов, которые не подходят всем продуктам.
    """
    def __init__(self, name, price):
        self.name = name
        self.price = price

class ShippableProduct(Product):
    """
    Базовый класс для товаров, которые можно физически доставлять.
    """
    def ship(self, address):
        print(f"Shipping {self.name} to {address}")

class PhysicalProduct(ShippableProduct):
    """
    Реально доставляемый продукт
    """
    pass

class DigitalProduct(Product):
    """
    Цифровой продукт, не нуждается в методе ship().
    """
    def download_link(self):
        print(f"Link for digital item {self.name} is sent to the user")

Теперь у нас:

  • PhysicalProduct корректно наследует поведение ship(), никаких неожиданностей нет.
  • DigitalProduct вообще не имеет ship(), и код, который требует «доставку», не будет случайно вызываться на цифровом продукте.
  • Мы сохранили единую базу Product для общих полей (имя, цена), но разделили по смыслу (LSP соблюдён: объекты класса ShippableProduct взаимозаменяемы друг с другом, а DigitalProduct не попадает в эту иерархию).

Таким образом, если код где-то ожидает ShippableProduct, то ему гарантированно передадут класс, у которого метод ship() реализован. Нарушений LSP теперь нет.

Принцип ISP (Interface Segregation Principle)

«Клиенты не должны зависеть от интерфейсов, которые они не используют».

Другими словами, если вы создаёте интерфейс (или базовый класс) MultifunctionDevice c методами print(), scan(), fax(), то классы, которым нужен только метод print(), не должны быть вынуждены реализовывать scan() и fax().

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

Пример «неправильного» кода (нарушение ISP)

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

from abc import ABC, abstractmethod

class MultifunctionDevice(ABC):
    @abstractmethod
    def print_document(self, doc):
        pass

    @abstractmethod
    def scan_document(self):
        pass

class MFP(MultifunctionDevice):
    def print_document(self, doc):
        print(f"Printing: {doc}")

    def scan_document(self):
        print("Scanning document")

class SimplePrinter(MultifunctionDevice):
    def print_document(self, doc):
        print(f"Printing: {doc}")

    def scan_document(self):
        # Принтер НЕ умеет сканировать
        raise NotImplementedError("This device cannot scan")

Где нарушение:

  • SimplePrinter вынужден реализовывать (или хоть как-то заглушать) метод scan_document(), который ему не нужен. Это грубо нарушает ISP, ведь интерфейс слишком широкий.

Пошаговый рефакторинг (исправляем ISP)

Идея: разбить большой интерфейс на несколько узких. Например, PrinterInterface и ScannerInterface. Многофункциональное устройство будет реализовать оба интерфейса, а простой принтер — только один.

class PrinterInterface(ABC):
    @abstractmethod
    def print_document(self, doc):
        pass

class ScannerInterface(ABC):
    @abstractmethod
    def scan_document(self):
        pass

class MFP(PrinterInterface, ScannerInterface):
    def print_document(self, doc):
        print(f"Printing: {doc}")

    def scan_document(self):
        print("Scanning document")

class SimplePrinter(PrinterInterface):
    def print_document(self, doc):
        print(f"Printing: {doc}")

# Теперь у SimplePrinter нет ненужного метода scan_document()

Плюсы:

  • Каждый класс реализует только те интерфейсы, которые ему действительно нужны.
  • Если в будущем появится устройство с ещё какими-то функциями (например, факс), мы не будем заставлять всех её поддерживать — просто создадим отдельный интерфейс.
  • Этот подход упрощает жизнь новичкам и всем, кто будет поддерживать код.
  1. LSP:
    • Избегаем ситуации, когда подкласс «ломает» ожидания, заданные родительским классом.
    • Улучшаем предсказуемость кода: «Если у нас класс A — это подтип B, значит, он ведёт себя как B».
    • Реальный плюс: меньше скрытых ошибок, особенно когда код масштабируется и где-то в глубине системы вызывают метод, который оказался недоступен у подкласса.
  2. ISP:
    • Облегчает разделение обязанностей по интерфейсам.
    • Уменьшает «шум» в коде, когда класс не обязан реализовывать методы, которые ему не нужны.
    • Реальный плюс: легче тестировать (тестируем только нужный функционал), код становится гибче и понятнее.
Код для заданий 8-10

Представим, что мы хотим отгружать физические товары, и для этого завели базовый класс ShippableItem с методом ship(). Но затем решили добавить класс VirtualGiftCard (виртуальная подарочная карта), которая не нуждается в физической доставке, однако всё равно наследуется от ShippableItem и ломает логику метода ship().

class ShippableItem:
    def __init__(self, name):
        self.name = name

    def ship(self, address):
        print(f"Shipping {self.name} to {address}")

class VirtualGiftCard(ShippableItem):
    def __init__(self, name, amount):
        super().__init__(name)
        self.amount = amount

    def ship(self, address):
        # Цифровая карта не отправляется физически — логика не подходит
        raise NotImplementedError("Digital items can't be shipped!")

Где нарушение:

  • Код, который ожидает объект типа ShippableItem, предполагает, что метод ship() всегда будет работать, а тут он неожиданно выбрасывает NotImplementedError.
  • Таким образом, VirtualGiftCard не является корректным подтипом ShippableItem с точки зрения LSP.

Интерфейс (абстрактный класс) EcommerceOperations объединил в себе три метода: add_to_cart(), ship_product(), start_subscription(). Но далеко не все классы нуждаются во всех трёх методах. Тем не менее, они вынуждены их реализовывать — даже если функциональность не актуальна.

from abc import ABC, abstractmethod

class EcommerceOperations(ABC):
    @abstractmethod
    def add_to_cart(self):
        pass

    @abstractmethod
    def ship_product(self):
        pass

    @abstractmethod
    def start_subscription(self):
        pass


class SimpleProduct(EcommerceOperations):
    def add_to_cart(self):
        print("Added to cart")

    def ship_product(self):
        print("Shipped product")

    def start_subscription(self):
        # Продукты без подписки не должны управлять подписками!
        raise NotImplementedError("Simple products do not require subscription")


class SubscriptionProduct(EcommerceOperations):
    def add_to_cart(self):
        # Подписки могут не продаваться через классическую «корзину»
        raise NotImplementedError("Subscription items are purchased differently")

    def ship_product(self):
        # Подписки не отгружаются физически
        raise NotImplementedError("Subscription items are not shipped")

    def start_subscription(self):
        print("Subscription started")

Где нарушение:

  • SimpleProduct вынужден реализовывать start_subscription(), хотя ему это не нужно.
  • SubscriptionProduct «заглушает» сразу два метода (add_to_cart и ship_product), которые не имеют смысла для подписок.
  • Это явное нарушение принципа ISP: интерфейс слишком «толстый», и классы зависят от методов, которые им не нужны.

Идеи для «исправления»

  1. LSP: Разделить «доставляемые» и «недоставляемые» сущности на разные классы/иерархии (например, сделать отдельный базовый класс DigitalItem, где нет метода ship(), или ввести абстракцию ShippableItem только для физически отгружаемых товаров).
  2. ISP: Разбить интерфейс EcommerceOperations на более узкие интерфейсы (например, CartOperable, Shippable, Subscribable) так, чтобы каждая реализация брала только те методы, которые ей нужны.

Задание 8: «Найти нарушения»

  1. Дан код, где объединены два кейса:
    • Кейс 1 (LSP): есть базовый класс ShippableItem c методом ship(). Есть класс VirtualGiftCard (цифровой подарок), который не может быть отгружен физически, но «пытается» наследоваться от ShippableItem и при вызове ship() выбрасывает NotImplementedError.
    • Кейс 2 (ISP): есть интерфейс EcommerceOperations c методами add_to_cart(), ship_product(), start_subscription(), и несколько классов, которые вынуждены реализовывать методы, им не нужные.
  2. Что именно нарушено в каждом случае (LSP или ISP), и , почему это плохо в реальном проекте.

Задание 9: «Исправить код»

  1. Исправить LSP-часть:
    • Ввести или убрать классы так, чтобы объекты, которые не могут быть отгружены, не наследовались от ShippableItem. Возможно, нужна отдельная абстракция для цифровых товаров.
  2. Исправить ISP-часть:
    • Разделить интерфейс EcommerceOperations на более маленькие интерфейсы (например, Orderable и Subscribable) или что-то в этом духе.
  3. Убедиться, что после рефакторинга классы не реализуют методы, которые им не нужны, и что каждый «подтип» действительно подходит к родительскому интерфейсу.

Задание 10: «Дефенс-код-ревью»

  1. Презентация: каждый (или каждая группа) показывает свой новый код, рассказывая, какие классы/интерфейсы появились, какие убрали.
  2. Вопросы: другие участники или преподаватель спрашивают:
    • «Почему именно так разделили интерфейсы?»
    • «Не остались ли скрытые нарушения LSP? Например, если класс наследует метод, которым не может пользоваться?»
    • «Если завтра добавим новый класс TrialSubscriptionItem, где есть “пробный период”, всё ещё будет работать без ломки кода?»
  3. Обсуждение альтернатив: возможно, кто-то решит задачу иначе, это тоже повод для полезной дискуссии.

Вместе с SRP и OCP, принципы LSP и ISP делают код более:

  • Предсказуемым (нет «подкласса, который неожиданно ломает родительское поведение»).
  • Адаптивным (можно выделять отдельные интерфейсы под отдельные роли, а не таскать лишние методы).
  • Поддерживаемым (не надо городить заглушки и raise NotImplementedError в наследниках).

В реальных командах LSP и ISP особенно важны, когда у нас большая иерархия классов или множество интерфейсов. Как только видите, что какой-то класс вынужден переопределять метод пустой реализацией («ну, мне это не нужно»), задумайтесь о нарушении LSP/ISP и о возможном рефакторинге.

Принцип инверсии зависимостей (DIP) и внедрение зависимостей (DI)

Ранее мы разобрали:

  1. SRP: класс должен иметь одну ответственность.
  2. OCP: код должен легко расширяться без изменения старых модулей.
  3. LSP: подклассы не должны ломать поведение родителя.
  4. ISP: интерфейсы не должны навязывать методы, которые класс не использует.

Теперь переходим к принципу DIP (Dependency Inversion Principle), который говорит:

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Вместе с этим на практике часто используют технику DI (Dependency Injection), чтобы передавать зависимости (например, сервис для отправки уведомлений) «снаружи», а не создавать их внутри класса.

Кратко: Если у вас есть класс «высокого уровня» (например, OrderService), который сам создаёт объекты «нижнего уровня» (например, EmailSender), вы жёстко их связали. DIP и DI помогут это исправить, сделав систему более гибкой, тестируемой и расширяемой.

Пример «неправильного» кода (нарушение DIP)

Сюжет: у нас снова есть интернет-магазин, где класс NotificationManager занимается уведомлениями пользователей по e-mail. Проблема в том, что он напрямую создаёт (new) конкретный класс EmailSender вместо того, чтобы зависеть от абстракции. Если вдруг понадобится отправлять SMS или Push-уведомления, придётся менять NotificationManager.

class EmailSender:
    def send_email(self, to, subject, body):
        print(f"Sending EMAIL to {to}, subject: {subject}, body: {body}")


class NotificationManager:
    def __init__(self):
        # Принцип DIP нарушен: 
        # Модуль "высокого уровня" напрямую зависит от "EmailSender"
        self.email_sender = EmailSender()

    def notify_user(self, user_email, message):
        subject = "Order Update"
        self.email_sender.send_email(user_email, subject, message)

Где нарушение:

  1. NotificationManager — это «высокоуровневый» модуль (мы можем считать его частью бизнес-логики), который ничего не должен знать о конкретном способе отправки.
  2. Вместо этого он прямо создаёт EmailSender(). Если завтра нам понадобится SmsSender, придётся менять NotificationManager. Это жёсткая зависимость от детали.

Последствия:

  • Сложнее тестировать: нельзя легко подменить EmailSender на фейковый (мок) без переписывания кода.
  • Сложнее расширять: чтобы добавить новый тип уведомлений, придётся менять код NotificationManager (а это нарушение OCP, да и DIP тоже).

Пошаговый рефакторинг (соблюдаем DIP)

Шаг 1: Вводим абстракцию (интерфейс/базовый класс)

Вместо того чтобы напрямую использовать EmailSender, создадим абстрактный класс (или интерфейс) MessageSender. Это позволяет «высокоуровневому» коду зависеть от абстракции, а не от конкретной реализации.

from abc import ABC, abstractmethod

class MessageSender(ABC):
    @abstractmethod
    def send(self, to, subject, body):
        pass


class EmailSender(MessageSender):
    def send(self, to, subject, body):
        print(f"Sending EMAIL to {to}, subject: {subject}, body: {body}")

class SmsSender(MessageSender):
    def send(self, to, subject, body):
        print(f"Sending SMS to {to}, message: {body}")

Шаг 2: Пусть NotificationManager зависит от абстракции, а не от конкретного класса

class NotificationManager:
    def __init__(self, sender: MessageSender):
        """
        Принимаем в конструктор любой объект, реализующий MessageSender.
        Таким образом, NotificationManager не зависит от "EmailSender" напрямую.
        """
        self.sender = sender

    def notify_user(self, user_email, message):
        subject = "Order Update"
        self.sender.send(user_email, subject, message)

Теперь NotificationManager не создаёт EmailSender внутри. Он просто получает его «снаружи» через параметры конструктора.

Плюсы:

  • Мы можем легко внедрить SmsSender или PushSender без переписывания кода NotificationManager.
  • Для тестов можно создать MockSender и передавать его при тестировании.

Шаг 3: Используем DI (Dependency Injection) на практике

В реальном проекте обычно есть точка создания «высокоуровневых» объектов (например, в main.py, или в фреймворке типа Django/Flask). Там мы решаем, какой конкретно MessageSender внедрить:

if __name__ == "__main__":
    email_sender = EmailSender()
    notify_manager = NotificationManager(email_sender)  # Внедрение зависимости
    notify_manager.notify_user("test@example.com", "Your order was shipped!")

    # Или легко поменять реализацию:
    sms_sender = SmsSender()
    notify_manager_sms = NotificationManager(sms_sender)
    notify_manager_sms.notify_user("+123456789", "Your order is on the way!")

Таким образом, класс NotificationManager не изменяется при переключении способа уведомлений. Мы лишь меняем код инициализации (внедрения). Это и есть DIP + DI в действии.

Почему это полезно (DIP):

  1. Снижает связанность: высокоуровневый код не жёстко завязан на детали.
  2. Проще тестировать: мы можем подменить реальную реализацию (E-mail или SMS) на мок-объект.
  3. Проще развивать: хотим добавить новый способ уведомлений? Просто пишем новый класс NewSender и передаём его в конструктор.

Почему это полезно (DI):

  1. Устраняет «new» зависимости внутри: класс не создаёт объекты самостоятельно.
  2. Гибкость: можно легко менять объект, который мы «подкладываем» внутрь класса, и ничего не трогать в самом классе.
  3. Упрощённая структура проекта: в крупных системах часто применяют DI-контейнеры или фреймворки, где конфигурацию (какой класс подставить в какую зависимость) можно управлять централизованно.
Показать скрытое содержание

Ниже приведён пример «плохого» кода, который нарушает принцип инверсии зависимостей (DIP). В нём классы «высокого уровня» (условно — бизнес-логика) напрямую создают конкретные объекты «нижнего уровня» (в данном случае, платёжную систему PayPal и MySQL-подключение к базе данных).

class MySQLDatabaseConnection:
    def connect(self):
        print("Connecting to MySQL database...")

    def execute(self, query: str):
        print(f"Executing query: {query}")


class PayPalPaymentGateway:
    def pay(self, amount: float):
        print(f"Paying {amount} using PayPal...")


class AccountManager:
    def create_account(self, username: str):
        # Логика создания аккаунта
        db = MySQLDatabaseConnection()  # Прямое создание конкретной реализации
        db.connect()
        db.execute(f"INSERT INTO users (username) VALUES ('{username}')")
        print(f"Account '{username}' created.")


class OrderService:
    def process_order(self, order_id: int, amount: float):
        # Логика обработки заказа
        payment_gateway = PayPalPaymentGateway()  # Жёсткая зависимость от PayPal
        payment_gateway.pay(amount)
        print(f"Order {order_id} processed with amount {amount}.")

Почему это нарушение DIP?

  1. Прямое создание:
    • AccountManager сам создаёт экземпляр MySQLDatabaseConnection.
    • OrderService сам создаёт экземпляр PayPalPaymentGateway.
      Это значит, что классы «верхнего уровня» привязаны к деталям «нижнего уровня».
  2. Сложность замены:
    • Если завтра решим использовать PostgreSQLDatabaseConnection вместо MySQL или перейти с PayPal на StripePaymentGateway, придётся менять код внутри AccountManager и OrderService.
    • Это нарушает принцип открытости/закрытости (OCP), а также сам DIP, по которому классы верхнего уровня должны зависеть от абстракций (например, DatabaseConnection, PaymentGateway), а не от конкретных реализаций.
  3. Тестирование:
    • Невозможно легко подменить MySQLDatabaseConnection на тестовую заглушку, ведь она «жёстко» зашита внутри AccountManager.
    • То же самое с PayPalPaymentGateway в OrderService. Труднее писать модульные тесты, так как для тестирования придётся либо изменять код, либо действительно поднимать PayPal sandbox/базу MySQL.

Что, если захотим заменить PaymentGateway?

В «правильном» варианте мы бы:

  1. Создали абстракцию (интерфейс или базовый класс) IPaymentGateway c методом pay(amount).
  2. Сделали конкретные реализации (например, PayPalGateway, StripeGateway), которые наследуют или реализуют IPaymentGateway.
  3. Передавали реализацию IPaymentGateway в OrderService снаружи (через конструктор или сеттер).

Таким образом, «высокоуровневый» класс OrderService не зависит от деталей конкретной платёжной системы. Менять PayPal на что-то другое можно без правки самого OrderService, что и есть сущность DIP.

Задание 11: «Найти нарушения DIP»

Дан фрагмент кода, в котором класс «высокого уровня» (например, OrderService или AccountManager) сам создаёт конкретные объекты (например, DatabaseConnection или PaymentGateway). Почему это нарушение DIP и чем оно грозит в реальном проекте (что если мы захотим заменить PaymentGateway?).

Задание 12: «Исправить код» (с помощью абстракций и DI)

  1. Создать интерфейс или абстрактный класс (например, IPaymentGateway) и сделать конкретные реализации (PayPalGateway, StripeGateway).
  2. Изменить «высокоуровневый» класс, чтобы он принимал IPaymentGateway извне (через конструктор или сеттер).

Задание 13: «Дефенс-код-ревью»

  1. Презентация: студент(ы) показывают, как вынесли зависимости в абстракции и применили DI.
  2. Обсуждение:
    • «Что, если нам понадобится второй способ оплаты? Придётся ли менять ваш OrderService
    • «Как бы вы тестировали класс OrderService, если внутри создаётся конкретная реализация PayPalGateway
    • «Может ли в вашей структуре быть ситуация, когда абстракция всё-таки зависит от конкретного класса?»
  3. Возможные альтернативы: некоторые могут сделать внедрение через конструктор, другие — через сеттер. Пусть обсудят, что лучше и в каких случаях.
  1. DIP говорит: завязка должна идти на абстракции, а не на конкретные детали. Это снижает связанность, повышает гибкость.
  2. DI — это практический метод реализации DIP. Передавая зависимости снаружи, мы разрываем «жёсткие» связи, упрощаем тестирование и расширение функционала.
  3. В реальном мире DIP+DI очень часто встречаются в фреймворках (Spring в Java, NestJS в TypeScript, различные DI-библиотеки в Python). Даже простой «ручной» подход, когда мы передаём объекты в конструктор, уже даёт огромную выгоду.
  4. Практические задания с «плохим» кодом, где класс сам создаёт конкретные реализации, а потом пошаговый рефакторинг в сторону абстракций и DI, — лучший способ понять, как это работает.

Принципы DRY, KISS и YAGNI

При разработке программного обеспечения, помимо сложных архитектурных решений и принципов объектно-ориентированного проектирования, существуют простые, но очень важные правила, которые помогают создавать качественный и поддерживаемый код. Три таких принципа — DRY, KISS и YAGNI. Они направлены на упрощение разработки и улучшение качества кода.

DRY: Don’t Repeat Yourself (Не повторяйся)

Описание:
«Не повторяйся!». Если обнаружили, что копируете один и тот же код в нескольких местах — пора выносить его в отдельную функцию/метод/класс. Иначе любая правка «там» потребует искать и править «везде».

Пример реализации:

def calculate_discount(price, discount):
    return price - (price * discount)

def apply_discount_to_cart(cart, discount):
    return [calculate_discount(item['price'], discount) for item in cart]

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

Пример нарушения:

def apply_discount_to_cart(cart, discount):
    discounted_cart = []
    for item in cart:
        discounted_price = item['price'] - (item['price'] * discount)
        discounted_cart.append({'name': item['name'], 'price': discounted_price})
    return discounted_cart

def apply_discount_to_item(item, discount):
    discounted_price = item['price'] - (item['price'] * discount)
    return {'name': item['name'], 'price': discounted_price}

Последствия нарушения:
Когда код дублируется, это увеличивает вероятность ошибок, так как любое изменение в одном месте требует поиска и изменения всех дублирующихся фрагментов. Это усложняет сопровождение и тестирование кода. Если в будущем изменится формула расчета скидки, придется корректировать каждое место, где она дублируется, что создает риск пропустить одно из них.

KISS: Keep It Simple, Stupid (Делай проще, глупец)

Описание:
«Сохраняй простоту!». Или как говорят некоторые: «Дураку понятно». Сложные решения не всегда лучше. Чем проще — тем легче масштабировать и поддерживать.

Пример реализации:

def get_user_age(user):
    if user and user.birthdate:
        return calculate_age(user.birthdate)
    return None

Этот код прост и решает конкретную задачу — получить возраст пользователя, если у него указана дата рождения.

Пример нарушения:

def get_user_age(user):
    if isinstance(user, dict):
        if 'birthdate' in user.keys():
            if user['birthdate'] is not None:
                birthdate = user['birthdate']
                if isinstance(birthdate, str):
                    birthdate = parse_date(birthdate)
                if isinstance(birthdate, datetime):
                    return calculate_age(birthdate)
    return None

Последствия нарушения:
Этот код делает то же самое, что и предыдущий, но намного сложнее. Сложность не оправдана, и это затрудняет понимание кода. Если нужно будет изменить логику, придется разбираться в большом количестве условий, что увеличивает риск ошибок.

YAGNI: You Aren’t Gonna Need It (Вам это не понадобится)

Описание:
«Вам это, скорее всего, никогда не понадобится». Не нужно писать гипермного кода на будущее, который «возможно когда-то пригодится», ведь часто он просто висит балластом.

Пример реализации:

def calculate_total(cart):
    return sum(item['price'] for item in cart)

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

Пример нарушения:

def calculate_total(cart, include_tax=False, currency_conversion=None):
    total = sum(item['price'] for item in cart)
    if include_tax:
        total *= 1.2  # 20% налог
    if currency_conversion:
        total = convert_currency(total, currency_conversion)
    return total

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

Практическое задание:

  • Создайте модуль для работы с пользователями (UserService), который умеет:
    • Регистрировать пользователя
    • Авторизовывать пользователя
  • Следуйте принципам:
    • DRY: если встретите повторяющийся код, вынесите его в функцию.
    • KISS: не усложняйте: пока можно хранить пользователей просто в списке (база данных придёт позже).
    • SOLID: создайте чёткий класс. Пусть он решает только задачу «логики пользователей». Не смешивайте её с «логикой отправки email-уведомлений» (иначе нарушение Single Responsibility).
    • DI: например, для хранения пользователей может быть класс-хранилище (UserRepository), который вы «вкалываете» (inject) в сервис.
Скелет модуля
class UserRepository:
    def __init__(self):
        self.users = []

    def save_user(self, user_data):
        # Сохраняем пользователя
        self.users.append(user_data)
    
    def find_user(self, username):
        # Ищем пользователя
        for u in self.users:
            if u['username'] == username:
                return u
        return None

class UserService:
    def __init__(self, user_repo: UserRepository):
        self.user_repo = user_repo

    def register(self, username, password):
        # Проверим, есть ли такой пользователь
        if self.user_repo.find_user(username) is not None:
            raise ValueError("Такой пользователь уже есть!")
        # Сохраним
        self.user_repo.save_user({"username": username, "password": password})
        print(f"Пользователь {username} успешно зарегистрирован!")

    def login(self, username, password):
        user = self.user_repo.find_user(username)
        if user is None:
            print("Неправильное имя пользователя!")
        elif user["password"] != password:
            print("Неправильный пароль!")
        else:
            print(f"Добро пожаловать, {username}!")

if __name__ == "__main__":
    repo = UserRepository()
    service = UserService(repo)

    service.register("alice", "1234")
    service.login("alice", "1234")
    service.login("bob", "qwerty")  # Ошибка

Жизненный цикл разработки ПО и архитектура

Роли и этапы в жизненном цикле разработки

  • Аналитики: собирают требования, выясняют, что же клиент на самом деле хочет.
  • Дизайнеры/архитекторы: определяют архитектуру, выбирают подходящие паттерны и технологии.
  • Разработчики: пишут код, следят за качеством, рефакторят.
  • Тестировщики: проверяют, что всё работает как задумано.
  • DevOps-инженеры: автоматизируют доставку (CI/CD), следят за инфраструктурой.
  • Продакт-менеджеры / Проджект-менеджеры: координируют процесс, ставят задачи, контролируют сроки и ресурсы.

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

Как архитектура влияет на весь цикл

Хорошая архитектура облегчает жизнь на всех этапах. Когда всё продумано, тестировщикам проще автоматизировать тесты, разработчикам — вводить новые фичи, DevOps-специалистам — настраивать CI/CD. Если архитектура «сыровата», то каждый новый шаг превращается в бой с кодом и инфраструктурой.

 

Жизненны цикл разработки ПО

Роли и этапы в жизненном цикле разработки ПО

Жизненный цикл разработки ПО обычно делится на несколько этапов, каждый из которых включает определенные задачи и роли. Рассмотрим основные этапы:

  1. Сбор и анализ требований:
    • Роли: Бизнес-аналитики, продакт-менеджеры, пользователи.
    • Описание: На этом этапе определяются цели проекта, изучаются потребности пользователей и формируются требования к системе. Это очень важный этап, так как ошибки здесь могут привести к созданию продукта, не соответствующего ожиданиям заказчика.
  2. Проектирование архитектуры:
    • Роли: Архитекторы ПО, системные аналитики.
    • Описание: Здесь разрабатывается архитектура системы, то есть определяется, как будут организованы компоненты ПО, какие технологии и паттерны будут использоваться. Архитектура задает основу для всей системы и определяет, как она будет развиваться в будущем.
  3. Дизайн и моделирование:
    • Роли: Дизайнеры, UX/UI специалисты.
    • Описание: На этом этапе создаются интерфейсы пользователя, схемы данных, а также прототипы системы. Здесь важно продумать, как пользователи будут взаимодействовать с системой, чтобы сделать ее удобной и интуитивно понятной.
  4. Реализация (кодирование):
    • Роли: Разработчики (программисты).
    • Описание: На этом этапе разрабатывается исходный код программы в соответствии с проектной документацией и архитектурными решениями. Важно, чтобы разработчики следовали архитектурным принципам, чтобы система оставалась масштабируемой и легко поддерживаемой.
  5. Тестирование:
    • Роли: Тестировщики, QA-инженеры.
    • Описание: Здесь проверяется, соответствует ли программа заданным требованиям и нет ли в ней ошибок. Тестирование включает различные виды проверок, такие как модульное тестирование, интеграционное тестирование, системное тестирование и приемочное тестирование.
  6. Внедрение:
    • Роли: DevOps-инженеры, системные администраторы.
    • Описание: На этом этапе ПО разворачивается в рабочей среде и становится доступным пользователям. Внедрение может включать установку ПО на серверы, настройку баз данных, деплой приложения и обеспечение его работоспособности.
  7. Поддержка и сопровождение:
    • Роли: Служба поддержки, разработчики, DevOps-инженеры.
    • Описание: После внедрения ПО поддерживается и обновляется в течение всего времени эксплуатации. Включает исправление ошибок, улучшение производительности и добавление новых функций.
  8. Вывод из эксплуатации:
    • Роли: Руководители проекта, системные администраторы.
    • Описание: Когда система устаревает или заменяется новой, проводится плановое завершение ее использования. Это может включать миграцию данных на новые системы и отключение старого ПО.

Практика и сквозной проект

Начало работы над сквозным проектом: настройка репозитория, первичная структура кода

  1. Создаём Git-репозиторий (GitHub, GitLab — на ваш выбор).
  2. Структурируем проект:
    my_project/
    ├── src/
    │   ├── main.py
    │   ├── services/
    │   └── models/
    ├── tests/
    ├── requirements.txt
    └── README.md
    
  3. requirements.txt — для зависимостей Python, например, pytest для тестов.

Для курса важно выбрать такой сквозной проект, который:

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

Идеи для проектов


1. Платформа для управления задачами (аналог Trello, Todoist)

📌 Ключевые особенности проекта:

  • Пользователь может создавать, редактировать и удалять задачи.
  • Задачи можно организовывать по категориям (например, «Работа», «Учёба», «Домашние дела»).
  • Поддержка статусов (например, «В процессе», «Выполнено»).
  • Возможность добавления комментариев к задачам.
  • Реализация авторизации и ролей пользователей (обычные пользователи, администраторы).
  • Функция напоминаний о дедлайнах.

📌 Что можно проработать в проекте?

  • ООП: класс Task, User, Category, Board и их связи.
  • SOLID: принцип единственной ответственности (TaskManager не должен заниматься хранением данных).
  • Dependency Injection: разделение логики управления задачами и хранилища данных.
  • Архитектура: монолит, REST API, MVC.
  • Практика работы с базами данных: SQLite → PostgreSQL.
  • Интерактивность: CLI, Web-интерфейс (FastAPI + React/Vue).

Почему это круто?
Практически у всех есть список задач, и этот проект можно реально использовать в жизни.


2. Система рекомендаций книг/фильмов/игр

📌 Ключевые особенности проекта:

  • Каталог с книгами/фильмами/играми.
  • Пользователь может ставить оценки и писать рецензии.
  • Генерация персонализированных рекомендаций (например, на основе предпочтений пользователя).
  • Система тегов и фильтров (по жанру, автору, году выпуска).
  • История просмотренных/прочитанных материалов.

📌 Что можно проработать в проекте?

  • ООП: классы User, Book, Movie, Review, RecommendationEngine.
  • Dependency Injection: разные хранилища данных (файлы, SQL, NoSQL).
  • Архитектура: микросервисная (отдельный сервис рекомендаций).
  • Асинхронное программирование: фоновая обработка рейтингов и рекомендаций.
  • Интеграция с API: например, OpenLibrary, TMDb.
  • Визуализация: Telegram-бот, Web-приложение.

Почему это круто?
Каждый любит хорошие рекомендации, а свой алгоритм для Netflix или Spotify — звучит заманчиво!


3. Система управления бронированием (отели, спортзалы, коворкинги)

📌 Ключевые особенности проекта:

  • Каталог объектов (гостиницы, залы, столики).
  • Возможность бронировать и отменять бронирование.
  • Уведомления (email, Telegram).
  • Отзывы и рейтинг.
  • Интеграция с картами (например, Google Maps, Яндекс.Карты).
  • Административный интерфейс для владельцев.

📌 Что можно проработать в проекте?

  • ООП: классы Booking, User, Hotel, Coworking, Gym.
  • Dependency Injection: подмена провайдеров хранения данных.
  • SOLID: разделение сервисов бронирования, оплаты, уведомлений.
  • Практика работы с API: отправка email, работа с платежными системами.
  • Асинхронность: автоматическая отмена бронирований.
  • REST API: backend для веб-интерфейса или мобильного приложения.

Почему это круто?
Можно развивать в любую сторону — от AirBnB до аренды велосипедов!


4. Генератор отчётов по данным

📌 Ключевые особенности проекта:

  • Загружаем данные (CSV, Excel, JSON).
  • Анализируем и обрабатываем их.
  • Генерируем отчёты в PDF, HTML, Excel.
  • Визуализируем данные с помощью графиков.
  • Автоматическая отправка отчётов на email.

📌 Что можно проработать в проекте?

  • ООП: классы Report, DataProcessor, ChartGenerator.
  • Dependency Injection: смена хранилищ (LocalStorage, CloudStorage).
  • Архитектура: разделение обработчиков данных, рендереров отчётов.
  • Библиотеки Python: pandas, matplotlib, reportlab.
  • Интеграция с API: загрузка данных из Google Sheets, REST-сервисов.
  • Много потоков (multithreading): обработка больших файлов.

Почему это круто?
Отчёты нужны везде — в бизнесе, в аналитике, в науке. Отличный шанс попрактиковаться в реальных задачах!


5. Анализатор новостей и трендов (парсер, NLP, AI)

📌 Ключевые особенности проекта:

  • Система собирает новости (из RSS, Twitter, Google News).
  • Анализирует их тональность (позитивная, негативная).
  • Классифицирует по категориям (спорт, политика, технологии).
  • Создаёт дайджесты и отправляет пользователю.
  • Можно подписаться на ключевые слова.

📌 Что можно проработать в проекте?

  • ООП: NewsSource, Article, SentimentAnalyzer.
  • Архитектура: сервис обработки текста + база данных.
  • AI/ML: работа с моделями NLP (spaCy, transformers).
  • Асинхронность: фоновый сбор новостей.
  • REST API: возможность подключить frontend.
  • Интерактивность: Telegram-бот для получения дайджестов.

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


🛠️ Как выбрать проект?

Все проекты масштабируемы. Можно начать с простого варианта (чистый Python + консоль), затем усложнять: ✅ Добавлять базу данных
✅ Разделять на слои (MVC, микросервисы)
✅ Переписывать под асинхронный код
✅ Добавлять API, UI

  • Task Manager — простая бизнес-логика, без сложных интеграций.
  • Генератор отчётов — просто обрабатывать данные, а дальше усложнять.

Если хочется «мощную архитектуру»:

  • Бронирование или Рекомендации — сложные связи, роли пользователей.
  • Парсер новостей — интеграция API, асинхронность, AI.

Если хочется «гибкость в развитии»:

  • Любой проект! Все идеи можно усложнять и улучшать.

Финальное слово

Пусть этот урок станет вашей «точкой входа» в проектирование ПО. Помните о принципах KISS и YAGNI — не усложняйте, когда можно сделать проще, и не пишите то, что «вдруг пригодится», если конкретной задачи нет. Но всё же не забывайте, что тщательное проектирование в начале — это ваша страховка от боли и слёз в будущем.

Старайтесь экспериментировать: пробуйте писать код, ломать его, переделывать, снова ломать… Именно так вы прочувствуете, что действительно значит хорошая архитектура и почему её так все хвалят.

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

Удачи в ваших первых шагах на пути проектировщика! Будьте любопытными и не бойтесь задавать вопросы, даже если вам кажется, что они «очевидные». Как говорится, «глупых вопросов не бывает» — особенно в ИТ, где каждый день появляется что-то новое.

Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.