Привет, Хабр!

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

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

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

Принципы хорошего программного дизайна

  1. Модульность

    • Модульность означает разделение большого проекта на более мелкие, независимо функционирующие части, или модули. Это облегчает тестирование, понимание и обслуживание кода. Каждый модуль фокусируется на выполнении конкретной задачи и имеет чётко определённый интерфейс взаимодействия с другими частями системы.

  2. Низкая связанность

    • Связанность относится к степени, в которой модули зависят друг от друга. Низкая связанность означает, что изменения в одном модуле минимально влияют на другие модули. Это увеличивает гибкость системы и упрощает её обслуживание, так как изменения или исправления могут быть выполнены с меньшими затратами времени и ресурсов.

    • Например, в объектно-ориентированном программировании, классы должны быть разработаны таким образом, чтобы они были как можно более автономными и взаимодействовали с другими классами через хорошо определенные интерфейсы.

  3. Абстракция

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

    • Абстракция достигается через создание классов и интерфейсов, которые представляют обобщенные концепции и поведение, а не конкретные реализации.

  4. Простота

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

  5. Предвидение изменений

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

Подходы к разделению функциональности

Принципы SOLID - это набор пяти основных принципов, которые помогают создавать гибкий, поддерживаемый и расширяемый код:

  1. Принцип единственной ответственности (Single responsibility principle, SRP): Этот принцип гласит, что класс должен иметь только одну причину для изменения. То есть, он должен выполнять только одну функцию или обязанность. Это позволяет разделить функциональность на более мелкие, управляемые части. Если класс имеет множество ответственностей, он становится сложным в поддержке и изменении.

  2. Принцип открытости/закрытости (Open/Closed principle, OCP): Согласно этому принципу, программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации. Это означает, что вы можете добавить новую функциональность, не изменяя существующий код. Это помогает уменьшить риск внесения ошибок при изменении существующего кода.

  3. Принцип подстановки Барбары Лисков (Liskov substitution principle, LSP): Этот принцип утверждает, что объекты подклассов должны быть способны заменить объекты базовых классов без изменения желаемых свойств программы. Это гарантирует, что подклассы будут совместимы с кодом, написанным для базовых классов.

  4. Принцип разделения интерфейса (Interface segregation principle, ISP): Согласно этому принципу, клиенты не должны зависеть от интерфейсов, которые они не используют. Интерфейсы должны быть специфичными для потребностей клиентов. Это позволяет избежать зависимости от неиспользуемых методов и уменьшает сложность интерфейсов.

  5. Принцип инверсии зависимостей (Dependency inversion principle, DIP): Этот принцип утверждает, что модули верхнего уровня не должны зависеть от модулей нижнего уровня, а оба должны зависеть от абстракций. Это обеспечивает более гибкую архитектуру, позволяя легко заменять конкретные реализации и снижая связанность между компонентами.

Рассмотрим подробнее принцип единственной ответственности (SRP)

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

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

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

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

Пару примеров рефакторинга:

Класс, выполняющий несколько функций

Исходный код:

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

    def calculate_total(self):
        total = 0
        for product in self.products:
            total += product.price
        return total

    def send_notification(self):
        # Отправка уведомления о заказе
        pass

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

Рефакторинг:

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

    def calculate_total(self):
        total = 0
        for product in self.products:
            total += product.price
        return total

class OrderNotifier:
    def send_notification(self, order):
        # Отправка уведомления о заказе
        pass

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

Класс, который работает с данными и выводит результат

Исходный код:

class ReportGenerator:
    def __init__(self, data):
        self.data = data

    def generate_report(self):
        # Генерация отчета на основе данных
        pass

    def display_report(self):
        # Вывод отчета на экран
        pass

Этот класс имеет две ответственности: генерация отчета и вывод отчета на экран. Давайте разделим эти ответственности на два отдельных класса.

Рефакторинг:

class ReportGenerator:
    def __init__(self, data):
        self.data = data

    def generate_report(self):
        # Генерация отчета на основе данных
        pass

class ReportPrinter:
    def __init__(self, report):
        self.report = report

    def display_report(self):
        # Вывод отчета на экран
        pass

Теперь у нас есть два отдельных класса: ReportGenerator, который отвечает за генерацию отчета, и ReportPrinter, который отвечает за вывод отчета на экран.

Применение паттерна "Снимок" (Memento) для реализации сохранения и восстановления состояния - это проектировочный паттерн, который позволяет сохранять текущее состояние объекта и восстанавливать его в будущем, не раскрывая внутренней структуры объекта. Этот паттерн особенно полезен, когда требуется сохранить и восстановить состояние объекта без нарушения инкапсуляции.

Как работает паттерн "Снимок"?

  1. Создание Снимка (Memento): Снимок - это объект, который хранит текущее состояние другого объекта, но не предоставляет доступ к этому состоянию напрямую. Обычно для создания снимка используется вложенный класс или отдельный объект.

  2. Создание Снимателя (Caretaker): Сниматель - это объект, который отвечает за сохранение и хранение снимков объекта. Он может создавать снимки, сохранять их в специальном хранилище (например, стеке) и восстанавливать состояние объекта из снимка.

  3. Создание Оригинального объекта (Originator): Оригинальный объект - это объект, состояние которого нужно сохранять и восстанавливать. Он может запросить сниматель сохранить его состояние или использовать сохраненное состояние для восстановления.

Пример 1: Применение паттерна "Снимок" для сохранения состояния текстового редактора на Python

# Снимок (Memento)
class EditorMemento:
    def __init__(self, content):
        self.content = content

# Оригинальный объект (Originator)
class TextEditor:
    def __init__(self):
        self.content = ""

    def create_memento(self):
        return EditorMemento(self.content)

    def restore_from_memento(self, memento):
        self.content = memento.content

# Сниматель (Caretaker)
class History:
    def __init__(self):
        self.snapshots = []

    def save(self, editor):
        self.snapshots.append(editor.create_memento())

    def undo(self, editor):
        if self.snapshots:
            memento = self.snapshots.pop()
            editor.restore_from_memento(memento)

# Пример использования
editor = TextEditor()
history = History()

editor.content = "Первый текст"
history.save(editor)

editor.content = "Второй текст"
history.save(editor)

print("Текущий текст:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после undo:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после второго undo:", editor.content)

Пример 2: Применение паттерна "Снимок" для сохранения состояния игры на Python

# Снимок (Memento)
class GameStateMemento:
    def __init__(self, level, score):
        self.level = level
        self.score = score

# Оригинальный объект (Originator)
class Game:
    def __init__(self):
        self.level = 1
        self.score = 0

    def create_memento(self):
        return GameStateMemento(self.level, self.score)

    def restore_from_memento(self, memento):
        self.level = memento.level
        self.score = memento.score

    def play(self):
        self.level += 1
        self.score += 10

# Сниматель (Caretaker)
class GameHistory:
    def __init__(self):
        self.states = []

    def save(self, game):
        self.states.append(game.create_memento())

    def undo(self, game):
        if self.states:
            memento = self.states.pop()
            game.restore_from_memento(memento)

# Пример использования
game = Game()
history = GameHistory()

game.play()
history.save(game)

game.play()
history.save(game)

print("Текущий уровень:", game.level)
print("Текущий счет:", game.score)

history.undo(game)
print("Отмена: Текущий уровень после undo:", game.level)
print("Отмена: Текущий счет после undo:", game.score)

history.undo(game)
print("Отмена: Текущий уровень после второго undo:", game.level)
print("Отмена: Текущий счет после второго undo:", game.score)

В обоих примерах паттерн "Снимок" позволяет сохранять и восстанавливать состояние объекта, не раскрывая его внутренней структуры. Это помогает управлять состоянием объектов в приложении и реализовывать функции отмены и восстановления.

Сохранение и восстановление состояния объекта

Сохранение состояния объекта означает сохранение его текущих данных и настроек так, чтобы в будущем можно было восстановить объект в точно таком же состоянии. Это часто достигается путем сериализации (преобразования данных объекта в последовательность байтов) и сохранения этой последовательности в файле, базе данных или другом месте хранения.

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

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

Процесс сохранения и восстановления состояния обычно выглядит так:

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

Python предоставляет модуль pickle для сериализации и десериализации объектов. Создадим пример, в котором будет создан простой класс, его экземпляр будет сериализован в файл, а затем восстановлен обратно:

import pickle

class Example:
    def __init__(self, data):
        self.data = data

# Создание объекта
example = Example("Сохраненные данные")

# Сериализация объекта в файл
with open('saved_state.pkl', 'wb') as file:
    pickle.dump(example, file)

# Десериализация объекта из файла
with open('saved_state.pkl', 'rb') as file:
    loaded_example = pickle.load(file)
    print(loaded_example.data)

В C# для сериализации и десериализации можно использовать System.Runtime.Serialization. Cоздадим класс, который сериализуется в XML-формат и сохраняется в файл, а затем восстанавливается из этого файла:

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Xml;

[DataContract]
public class Example
{
    [DataMember]
    public string Data { get; set; }

    public Example(string data)
    {
        Data = data;
    }
}

public class Program
{
    public static void Main()
    {
        var example = new Example("Сохраненные данные");

        // Сериализация в XML
        var serializer = new DataContractSerializer(typeof(Example));
        using (var stream = File.Create("saved_state.xml"))
        {
            serializer.WriteObject(stream, example);
        }

        // Десериализация из XML
        Example loadedExample;
        using (var stream = File.OpenRead("saved_state.xml"))
        {
            loadedExample = (Example)serializer.ReadObject(stream);
        }

        Console.WriteLine(loadedExample.Data);
    }
}

Паттерн memento (снимок) также полезен, когда требуется сохранить и восстановить состояние объекта без нарушения инкапсуляции.

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

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

# Снимок (Memento)
class EditorMemento:
    def __init__(self, content):
        self.content = content

# Оригинальный объект (Originator)
class TextEditor:
    def __init__(self):
        self.content = ""

    def create_memento(self):
        return EditorMemento(self.content)

    def restore_from_memento(self, memento):
        self.content = memento.content

# Сниматель (Caretaker)
class History:
    def __init__(self):
        self.snapshots = []

    def save(self, editor):
        self.snapshots.append(editor.create_memento())

    def undo(self, editor):
        if self.snapshots:
            memento = self.snapshots.pop()
            editor.restore_from_memento(memento)

# Пример использования
editor = TextEditor()
history = History()

editor.content = "Первый текст"
history.save(editor)

editor.content = "Второй текст"
history.save(editor)

print("Текущий текст:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после undo:", editor.content)

history.undo(editor)
print("Отмена: Текущий текст после второго undo:", editor.content)

Избыточная функциональность - это неотъемлемая часть процесса, но она может быть управляемой и контролируемой применяя

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

Комментарии (1)


  1. dyadyaSerezha
    19.12.2023 02:47

    1) В первом примере кода надо указать, что это Python (кстати, нет типов).

    2) "когерентные классы" - зачем писать ничего не объясняющие определения?

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

    4) Зачем-то, без объяснений, вдруг подробно рассмотрен паттерн сохранения и восстановления состояния. Зачем? Он тут явно лишний.