Команда Flutter-разработчиков Mad Brains продолжает рассказывать про паттерны проектирования на Dart. Использование паттернов проектирования помогает разработчикам создавать более масштабируемое и сопровождаемое ПО, а также улучшать читаемость и содержательность кода. Первую часть статьи можно прочитать тут.

Observer

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

Реализация: 

  1. Создать интерфейс или абстрактный класс для субъекта, содержащий методы для добавления, удаления и уведомления наблюдателей.

  2. Создать интерфейс или абстрактный класс для наблюдателя, содержащий метод обработки уведомления.

  3. Реализовать интерфейсы или абстрактные классы в конкретных классах субъекта и наблюдателя.

  4. В классе субъекта хранить список наблюдателей и вызывать метод уведомления у каждого из них при изменении состояния.

Пример кода

abstract class Observable {
  void addObserver(Observer observer);
  void removeObserver(Observer observer);
  void notifyObservers();
}

class ConcreteObservable implements Observable {
  final List<Observer> _observers = <Observer>[];

  int? _veryUsefulNumber;

  set veryUsefulNumber(int number) {
    _veryUsefulNumber = number;
    notifyObservers();
  }

  @override
  void addObserver(Observer observer) {
    _observers.add(observer);
  }

  @override
  void removeObserver(Observer observer) {
    _observers.remove(observer);
  }

  @override
  void notifyObservers() {
    for (final observer in _observers) {
      observer.update(_veryUsefulNumber);
    }
  }
}

abstract class Observer {
  void update(int? data);
}

class ConcreteObserver implements Observer {
  @override
  void update(int? data) {
    print('now data is $data');
  }
}

void main() {
  final ConcreteObservable observable = ConcreteObservable();
  final Observer observer = ConcreteObserver();

  observable.addObserver(observer);

  for (int i = 0; i < 5; i++) {
    observable.veryUsefulNumber = i;
  }

  observable.removeObserver(observer);
}

В данном примере кода, интерфейс Observable определяет методы для управления наблюдателями и оповещения их об изменениях. ConcreteObservable реализует этот интерфейс, храня список наблюдателей и оповещая их об изменении нашего очень полезного числа. Интерфейс Observer определяет метод update для получения уведомлений об изменениях состояния и ConcreteObserver реализует этот метод, печатая данные.

В методе main создаются объекты ConcreteObservable и ConcreteObserver, конкретный наблюдатель добавляется к конкретному наблюдаемому и обновляется значение поля, после он будет оповещен об этом изменении. После нескольких изменений наблюдатель удаляется из списка наблюдаемых.

Плюсы:

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

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

  3. Позволяет динамически добавлять и удалять наблюдателей без изменения кода наблюдаемого объекта.

Минусы:

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

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

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

Паттерн «Наблюдатель» используется во Flutter: например, StreamBuilder и ChangeNotifier.

Memento

Паттерн «Хранитель» (Memento) - это паттерн проектирования, который позволяет сохранять и восстанавливать состояние объекта без нарушения инкапсуляции.

Реализация:

  1. Создать класс Originator, который хранит текущее состояние и создает объект Memento с текущим состоянием.

  2. Создать класс Memento, который содержит состояние Originator и недоступен для изменения из вне.

  3. Создать класс Caretaker, который запрашивает и восстанавливает состояние Originator с помощью объекта Memento.

  4. В Originator определить методы сохранения и восстановления состояния с использованием Memento

  5. В Caretaker определить методы сохранения Memento и доступа к нему.

Пример кода

class FormState {
  const FormState(this._email, this._password);

  final String _email;
  String get email => _email;

  final String _password;
  String get password => _password;
}

class FormStateStore {
  FormState? state;
}

class FormController {
  String email = '';
  String password = '';

  FormState saveState() {
    return FormState(email, password);
  }

  void restoreState(FormState state) {
    email = state.email;
    password = state.password;
  }

  void showState() {
    print('Email: $email');
    print('Password: $password');
  }
}

void main() {
  final FormController formController = FormController();
  final FormStateStore formStateStore = FormStateStore();

  formController.email = 'foo@example.com';
  formController.password = '12345678';

  print('______ INITIAL STATE ______');
  formController.showState();
  print('___________________________\n');


  formStateStore.state = formController.saveState();

  formController.email = 'bar@example.com';
  formController.password = 'qwerty123';

  print('______ CHANGED STATE ______');
  formController.showState();
  print('___________________________\n');

  final FormState? savedState = formStateStore.state;

  if (savedState != null) {
    formController.restoreState(savedState);
  }

  print('______ RESTORED STATE ______');
  formController.showState();
  print('____________________________');
}

В приведенном коде определены класс FormState, который хранит состояние формы, класс FormStateStore - хранит состояние формы, и класс FormController, который управляет состоянием формы. Метод saveState сохраняет состояние формы в объекте FormState и метод restoreState восстанавливает состояние формы из объекта FormState.

В методе main сначала создается экземпляр FormController, затем меняется состояние формы, далее сохраняется состояние формы в FormStateStore и изменяется состояние формы снова. Затем используется сохраненное состояние для восстановления состояния формы. Это демонстрирует, как паттерн Memento используется для сохранения и восстановления состояния объекта без нарушения его инкапсуляции.

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

Плюсы:

  1. Позволяет сохранять и восстанавливать состояние объекта без нарушения его инкапсуляции.

  2. Обеспечивает возможность отменять действия и создание механизма отмены/возврата действий.

  3. Позволяет сохранять несколько разных состояний одного и того же объекта.

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

Минусы:

  1. Необходимость создания дополнительных классов для хранения состояния, которые могут усложнять структуру кода.

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

  3. Необходимость реализовать механизм отмены/возврата действий может быть сложной и затратной в реализации

Во Flutter паттерн Memento может быть использован для сохранения и восстановления состояния форм или состояния после навигации в приложении.

Responsibility chain

Паттерн Responsibility chain («Цепочка ответственности») - это структурный паттерн проектирования, который используется для организации в системе иерархии объектов, каждый из которых может обрабатывать запрос. Каждый объект в цепочке решает, может ли он обработать запрос или же передать его следующему объекту в цепочке.

Реализация:

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

  2. Реализовать конкретные классы обработчиков, которые будут реализовывать интерфейс или абстрактный класс и обрабатывать запросы.

  3. Установить цепочку ответственности связывая обработчики в определенном порядке.

  4. Создать класс запроса и передавать его в начало цепочки ответственности

Пример кода

enum MessageSender {
  colleague(1),
  friends(2),
  family(3),
  other(0);

  const MessageSender(this.level);

  final int level;
}

abstract class MessageHandler {
  MessageHandler(this._handleSender);

  final MessageSender _handleSender;
  MessageHandler? _nextHandler;

  set nextHandler(MessageHandler handler) => _nextHandler = handler;

  void handle(String message, MessageSender sender) {
    if (_handleSender.level <= sender.level) {
      _handleMessage(message);
    }

    _nextHandler?.handle(message, sender);
  }

  void _handleMessage(String message);
}

class EmailMessageHandler extends MessageHandler {
  EmailMessageHandler(super.handleSender);

  @override
  void _handleMessage(String message) {
    print('Message "$message" send to e-mail');
  }
}

class SmsMessageHandler extends MessageHandler {
  SmsMessageHandler(super.handleSender);

  @override
  void _handleMessage(String message) {
    print('Message "$message" send to sms');
  }
}

class TrashBoxMessageHandler extends MessageHandler {
  TrashBoxMessageHandler(super.handleSender);

  @override
  void _handleMessage(String message) {
    print('Message "$message" deleted');
  }
}

void main() {
  final MessageHandler emailHandler = EmailMessageHandler(MessageSender.friends);
  final MessageHandler smsHandler = SmsMessageHandler(MessageSender.family);
  final MessageHandler trashBoxHandler = TrashBoxMessageHandler(MessageSender.other);

  emailHandler.nextHandler = smsHandler;
  smsHandler.nextHandler = trashBoxHandler;

  emailHandler.handle('Hello, I\'m your very far sibling from Nigeria...', MessageSender.other);
}

В приведенном примере кода есть 3 типа обработчика сообщений: EmailMessageHandler, SmsMessageHandler и TrashBoxMessageHandler. Каждый обработчик имеет уровень ответственности (MessageSender), по которому он может обрабатывать сообщения. Если обработчик не может обработать сообщение, то он передает его следующему обработчику в цепочке.

Плюсы:

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

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

  3. Независимость обработчиков: каждый обработчик не зависит от конкретного типа запроса

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

Минусы:

  1. Сложность отладки: если что-то идет не так в процессе обработки запроса, может быть трудно определить, где именно произошла ошибка.

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

Во Flutter можно использовать этот паттерн для обработки событий ввода (например, нажатий на кнопки) или валидации ввода в формах. Например, можно создать набор обработчиков, каждый из которых отвечает за определенный тип валидации (например, проверка длины пароля или корректности email-адреса) и настроить их в нужной последовательности, чтобы запустить валидацию в нужном порядке.

Strategy

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

Реализация:

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

  2. Реализовать конкретные классы стратегий, которые будут реализовывать интерфейс или абстрактный класс и реализовывать различные алгоритмы.

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

  4. Использовать класс контекста для вызова соответствующей стратегии

Пример кода

abstract class DeliveryMethod {
  void calculateDeliveryTime();

  void calculateDeliveryPrice();
}

class CourierDeliveryMethod implements DeliveryMethod {
  const CourierDeliveryMethod(this._distanceToUserInMeters);

  final int _distanceToUserInMeters;

  static const int _minimumCost = 100;

  @override
  void calculateDeliveryPrice() {
    print('The cost of courier delivery is ${_minimumCost + (_distanceToUserInMeters / 10).ceil()} RUB');
  }

  @override
  void calculateDeliveryTime() {
    final DateTime estimatedDeliveryTime = DateTime.now().add(const Duration(days: 2));

    print(
      'The courier will arrive on ${estimatedDeliveryTime.day}.${estimatedDeliveryTime.month}.${estimatedDeliveryTime.year}',
    );
  }
}

class PickupDeliveryMethod implements DeliveryMethod {
  const PickupDeliveryMethod(this._packageWeightInKilograms);

  final double _packageWeightInKilograms;

  static const int _minimumCost = 29;

  @override
  void calculateDeliveryPrice() {
    print('The cost of package pickup is ${_minimumCost + (_packageWeightInKilograms * 100).ceil()} RUB');
  }

  @override
  void calculateDeliveryTime() {
    print('Your package will arrive at pickup point in 5 days');
  }
}

class DeliverySelector {
  DeliverySelector(this._packageWeightInKilograms, this._distanceToUserInMeters);

  DeliveryMethod? _method;

  final double _packageWeightInKilograms;
  final int _distanceToUserInMeters;

  void selectPickup() {
    _method = PickupDeliveryMethod(_packageWeightInKilograms);
  }

  void selectCourier() {
    _method = CourierDeliveryMethod(_distanceToUserInMeters);
  }

  void showDeliveryInfo() {
    print('______ DELIVERY INFO ______');
    _method?.calculateDeliveryTime();
    _method?.calculateDeliveryPrice();
    print('___________________________\n');
  }
}

void main() {
  final DeliverySelector selector = DeliverySelector(0.5, 1203);

  selector.selectPickup();
  selector.showDeliveryInfo();

  selector.selectCourier();
  selector.showDeliveryInfo();
}

В этом примере DeliveryMethod - это абстрактный класс, представляющий метод доставки. Существуют две различные конкретные реализации этого класса: CourierDeliveryMethod и PickupDeliveryMethod. Оба этих класса предоставляют способ расчета времени доставки и цены.

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

Плюсы:

  1. Возможность переключения между алгоритмами во время выполнения.

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

  3. Возможность использовать алгоритм независимо от клиента.

  4. Повышение гибкости кода.

Минусы:

  1. Увеличение количества классов и общей сложности системы

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

Template method

Паттерн Template method («Шаблонный метод») - это поведенческий шаблон проектирования, который определяет каркас алгоритма в базовом классе, позволяя подклассам предоставлять конкретные реализации для определенных шагов алгоритма.

Реализация:

  1. Создать абстрактный класс с общим алгоритмом, состоящим из набора шагов.

  2. Определить абстрактные методы для каждого шага алгоритма, которые будут переопределяться в конкретных классах.

  3. Реализовать конкретные классы, которые будут наследовать абстрактный класс и переопределять абстрактные методы для реализации своей версии алгоритма.

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

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

Пример кода

import 'package:meta/meta.dart';

abstract class FormValidator {
  @nonVirtual
  void validate() {
    checkEmail();
    checkPassword();
    _checkAgreement();
  }

  void checkEmail();

  void checkPassword();

  void _checkAgreement() {
    print("Agreement is checked");
  }
}

class SignInFormValidator extends FormValidator {
  @override
  void checkEmail() {
    print("Email is valid");
  }

  @override
  void checkPassword() {
    print("Password is valid");
  }
}

class SignUpFormValidator extends FormValidator {
  @override
  void checkEmail() {
    print("Email is valid and confirmed");
  }

  @override
  void checkPassword() {
    print("Password is valid and the second password is equal to the first one");
  }
}

void main() {
  SignInFormValidator().validate();
  print('\n');
  SignUpFormValidator().validate();
}

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

В предоставленном коде перед использованием шаблона определены два класса SignInFormValidator и SignUpFormValidator. Оба класса имеют аналогичный метод validate(), который вызывает одни и те же три метода (_checkEmail(), _checkPassword(), _checkAgreement()) в том же порядке. Однако реализация этих методов различна для каждого класса, что может затруднить поддержку и понимание кода.

В коде после использования шаблона определяется абстрактный класс FormValidator. Этот класс содержит метод validate(), который определяет каркас алгоритма путем вызова методов checkEmail() и CheckPassword(), а также имеет метод _checkAgreement(), который определен в базовом классе.

Два конкретных класса SignInFormValidator и SignUpFormValidator определены как подклассы FormValidator, которые предоставляют конкретные реализации для методов checkEmail() и CheckPassword(), которые остаются абстрактными в базовом классе.

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

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

Плюсы:

  1. DRY подход, общие шаги реализуются один раз в базовом классе, а не дублируются в нескольких подклассах.

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

Минусы:

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

  2. Шаблонный метод может быть сложным для понимания и обслуживания, если структура и алгоритм не разработаны должным образом.

Bridge

Паттерн Bridge («Мост») - это структурный шаблон проектирования, используемый для отделения абстракции от ее реализации, чтобы они могли изменяться независимо. Шаблон включает в себя интерфейс (абстракцию), который определяет методы для выполняемых операций, и класс реализации, который обеспечивает фактическую реализацию методов, определенных в интерфейсе.

Реализация:

  1. Создать интерфейс или абстрактный класс для абстракции, который будет содержать методы для работы с реализацией.

  2. Создать интерфейс или абстрактный класс для реализации, который будет содержать методы, которые должна реализовать конкретная реализация.

  3. Реализовать конкретные классы для абстракции и реализации, которые будут реализовывать соответствующие интерфейсы или абстрактные классы.

  4. Установить связь между классом абстракции и реализации через конструктор или сеттер.

  5. Использовать класс абстракции для работы с реализацией через интерфейс или абстрактный класс, изолируя клиентский код от конкретных реализаций.

Пример кода

abstract class SalaryCalculator {
  const SalaryCalculator(this._source);

  final SalarySource _source;

  void calculate();
}

class TotalSalaryCalculator extends SalaryCalculator {
  const TotalSalaryCalculator(super.source);

  @override
  void calculate() {
    print('Calculating total salary...');
    _source.getSalary();
  }
}

class MonthSalaryCalculator extends SalaryCalculator {
  const MonthSalaryCalculator(super.source);

  @override
  void calculate() {
    print('Calculating month salary...');
    _source.getSalary();
  }
}

abstract class SalarySource {
  void getSalary();
}

class WhiteSalarySource extends SalarySource {
  @override
  void getSalary() {
    print('Working with white salary...');
  }
}

class BlackSalarySource extends SalarySource {
  @override
  void getSalary() {
    print('Working with black salary...');
  }
}

void main() {
  final SalaryCalculator calc = MonthSalaryCalculator(WhiteSalarySource());
  calc.calculate();
}

В нем есть абстракции SalaryCalculator и SalarySource и классы их реализации TotalSalaryCalculator, MonthSalaryCalculator и WhiteSalarySource, BlackSalarySource.

SalaryCalculator имеет экземпляр SalarySource, а SalarySource имеет определенные методы, которые реализуются WhiteSalarySource, BlackSalarySource.

Класс SalaryCalculator имеет метод calculate(), который использует _source.getSalary(), который реализуется фактическим источником заработной платы.

Плюсы:

  1. Отделение абстракции от ее реализации может сделать код более гибким и простым в понимании и обслуживании.

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

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

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

Builder

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

Реализация:

  1. Создать интерфейс или абстрактный класс для Строителя, который будет содержать методы для создания различных частей объекта.

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

  3. Создать интерфейс или абстрактный класс для Продукта, который будет содержать методы для получения различных частей объекта.

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

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

  6. Использовать класс Директор для создания экземпляра Продукта с нужными параметрами.

Пример кода

abstract class Builder {
  void buildPartA();
  void buildPartB();
  void buildPartC();
}

class ProductBuilder extends Builder {
  final Product product = Product();

  @override
  void buildPartA() {
    product.addPart('partA');
  }

  @override
  void buildPartB() {
    product.addPart('partB');
  }

  @override
  void buildPartC() {
    product.addPart('partC');
  }

  Product get result => product;
}

class Product {
  final List<String> _parts = <String>[];

  void addPart(String part) {
    _parts.add(part);
  }

  void showParts() {
    print(_parts);
  }
}

class Director {
  Builder? _builder;

  void setBuilder(Builder builder) {
    _builder = builder;
  }

  void buildMinimalViableProduct() {
    _builder?.buildPartA();
  }

  void buildFullFeaturedProduct() {
    _builder?.buildPartA();
    _builder?.buildPartB();
    _builder?.buildPartC();
  }
}

void main() {
  final Director director = Director();
  final ProductBuilder builder = ProductBuilder();

  director.setBuilder(builder);

  print('Building MVP');
  director.buildMinimalViableProduct();
  builder.result.showParts();

  print('Building FFP');
  director.buildFullFeaturedProduct();
  builder.result.showParts();
}

Плюсы:

  1. Он позволяет просто строить сложные объекты шаг за шагом.

  2. Он отделяет построение объекта от его представления, делая процесс более ясным и легким для понимания.

  3. Он позволяет создавать различные представления объектов одного и того же типа, используя один и тот же код построения.

  4. Он позволяет легко добавлять новые функции к объекту без изменения кода построения.

Минусы:

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

  2. Избыточен для простых объектов

Flyweight

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

Реализация:

  1. Идентифицировать общие свойства и методы объектов, которые можно сделать общими для всех экземпляров.

  2. Создать класс Flyweight, который будет содержать эти общие свойства и методы.

  3. Создать класс FlyweightFactory, который будет отвечать за создание и хранение экземпляров Flyweight.

  4. При запросе нового объекта, использовать FlyweightFactory для получения существующего экземпляра Flyweight или создания нового, если такого еще не существует.

  5. Использовать полученный экземпляр Flyweight для установки специфических свойств и вызова специфических методов для каждого объекта.

Пример кода

class FlyweightFactory {
  static final Map<String, Flyweight> flyweights = <String, Flyweight>{};

  static Flyweight? getFlyweight(String key) {
    if (flyweights.containsKey(key)) {
      return flyweights[key];
    } else {
      final flyweight = Flyweight(key);
      flyweights[key] = flyweight;
      return flyweight;
    }
  }
}

class Flyweight {
  const Flyweight(this.intrinsicState);

  final String intrinsicState;

  void operation(String extrinsicState) {
    print('Внутреннее состояние: $intrinsicState, Внешнее состояние: $extrinsicState');
  }
}

void main() {
  final Flyweight? flyweight1 = FlyweightFactory.getFlyweight('flyweight1');
  final Flyweight? flyweight2 = FlyweightFactory.getFlyweight('flyweight1');
  final Flyweight? flyweight3 = FlyweightFactory.getFlyweight('flyweight2');

  // В данном случае, flyweight1 и flyweight2 ссылаются на один и тот же объект
  // в кэше flyweights, что делает их легковесными

  flyweight1?.operation('state1');
  flyweight2?.operation('state2');
  flyweight3?.operation('state3');
}

Плюсы:

  1. Уменьшено использование памяти за счет совместного использования экземпляров объектов

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

  3. Улучшенная масштабируемость для большого количества однотипных объектов

Минусы:

  1. Повышенная сложность кода и обслуживания.

  2. Потенциальная проблема безопасности, поскольку объекты Flyweight совместно используются в разных контекстах, поэтому один клиент потенциально может изменить объект и вызвать неожиданное поведение у других клиентов.

  3. Шаблон Flyweight требует создания фабричного метода, что усложняет код.

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

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

Prototype

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

Реализация:

  1. Создать класс или интерфейс, который будет описывать методы для клонирования объекта.

  2. Создать классы, которые реализуют этот интерфейс и реализуют методы клонирования.

Пример кода

import 'dart:io';

/// Список с разными тайлами
abstract class Cell {
  Cell(this.content);

  Cell.fromSource(Cell cell) :
        content = cell.content;

  late final String content;

  Cell clone();

  String render();
}

class StringCell extends Cell {
  StringCell(super.content);

  StringCell.fromSource(StringCell cell) : super.fromSource(cell);

  @override
  StringCell clone() => StringCell.fromSource(this);

  @override
  String render() => '$content\t\t\t';
}

class DateCell extends Cell {
  DateCell(this.date) : super(date.toString());

  DateCell.fromSource(DateCell cell) : super.fromSource(cell) {
    date = cell.date;
  }

  late final DateTime date;

  @override
  DateCell clone() => DateCell.fromSource(this);

  @override
  String render() {
    final String formattedDate = '${date.day}.${date.month}.${date.year}';

    return '$formattedDate\t';
  }
}

class Table {
  Table.fromSize(int size):
      _cells = List<List<Cell?>>.generate(size, (_) => List<Cell?>.generate(size, (_) => null));

  final List<List<Cell?>> _cells;

  void draw() {
    for (final List<Cell?> cellColumn in _cells) {
      for (final Cell? cell in cellColumn) {
        if (cell == null) {
          stdout.write('0\t\t\t');

          continue;
        }

        stdout.write(cell.render());
      }

      stdout.writeln();
    }
  }

  void addCell({required Cell cell, required int x, required int y}) {
    _cells[y][x] = cell;
  }

  void copyCell({required int x1, required int y1, required int x2, required int y2}) {
    _cells[y2][x2] = _cells[y1][x1]?.clone();
  }
}

void main() {
  final Table table = Table.fromSize(5);

  table.addCell(cell: StringCell('foo'), x: 2, y: 4);
  table.addCell(cell: DateCell(DateTime.now()), x: 1, y: 2);

  table.copyCell(x1: 1, y1: 2, x2: 2, y2: 3);

  table.draw();
}

В примере кода класс Cell служит прототипом, а классы StringCell и DateCell являются его конкретными реализациями. Метод clone отвечает за создание копии объекта, которая затем добавляется в таблицу методами addCell и copyCell.

Плюсы:

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

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

  3. Он может упростить создание новых экземпляров класса без необходимости знать подробности о том, как реализован класс.

Минусы:

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

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

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

Шаблон используется, когда создание объекта является дорогостоящим и возникает необходимость воссоздавать один и тот же объект несколько раз.

Facade

Паттерн Facade («Фасад») - это структурный шаблон проектирования, который обеспечивает упрощенный интерфейс для сложной системы. Это позволяет клиентам взаимодействовать с системой через единый упрощенный API, а не напрямую взаимодействовать с отдельными компонентами системы.

Реализация:

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

  2. Создать класс фасада, который будет содержать ссылки на классы, интерфейсы и библиотеки из первого шага.

  3. Реализовать методы фасада, которые будут делегировать вызовы к классам, интерфейсам и библиотекам из первого шага.

  4. Предоставить клиентскому коду только доступ к фасаду, чтобы скрыть сложность работы с группой классов, интерфейсов и библиотек.

Пример кода

/// Нужно подключиться к секретной машине, затем зашифровать и закодировать сообщение, затем отправить
class MachineDriver {
  void connect() {
    print("Connecting to our very secure machine...");
    print('Connected');
  }

  void disconnect() {
    print("Disconnected");
  }

  void sendEncodedMessage(List<int> message) {
    print('Send message $message');
  }
}

class MessageEncryptor {
  String encrypt(String message) {
    print('Message encrypted');
    return message.toUpperCase();
  }
}

class MessageEncoder {
  List<int> encode(String message) {
    print('Message encoded');
    return message.codeUnits;
  }
}

class MessageSender {
  final MessageEncoder encoder = MessageEncoder();
  final MessageEncryptor encryptor = MessageEncryptor();
  final MachineDriver driver = MachineDriver();

  void sendMessage(String message) {
    final String encryptedMessage = encryptor.encrypt(message);
    final List<int> encodedMessage = encoder.encode(encryptedMessage);
    driver.connect();
    driver.sendEncodedMessage(encodedMessage);
    driver.disconnect();
  }
}

class MainService {
  void run() {
    // ...some stuff...

    final MessageSender messageSender = MessageSender();
    messageSender.sendMessage('foo');

    // ...other stuff...
  }
}

void main() {
  MainService().run();
}

В приведенном примере кода класс MessageSender действует как фасад для базовой функциональности, предоставляемой классами MessageEncoder, MessageEncryptor и MachineDriver. Вместо того, чтобы клиентам приходилось взаимодействовать с этими классами напрямую, они могут использовать метод SendMessage класса MessageSender, который инкапсулирует функциональность базовых классов, делая его более удобным в использовании.

Плюсы:

  1. Помогает снизить сложность взаимодействия с большой и сложной системой.

  2. Может уменьшить зависимости между клиентским кодом и деталями реализации системы

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

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