Если вы занимаетесь программированием на Flutter, то наверняка сталкивались с задачами, которые можно было бы решить более эффективно и легко, используя уже проверенные практиками решения. В этом и помогают паттерны проектирования на Dart — шаблоны, которые разработчики применяют для решения часто встречающихся проблем. В двух статьях команда Mad Brains рассмотрит 16 паттернов проектирования на Dart, как они могут быть использованы для улучшения качества кода и повышения эффективности разработки.

Singleton

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

Реализация: сокрытие конструктора класса и создание статического метода / поля / геттера, предоставляющего доступ к экземпляру класса.

Пример кода

class Logger {
  Logger._internal();

  static final Logger _instance = Logger._internal();

  static Logger get instance => _instance;
}

void main() {
  final Logger logger = Logger.instance;
  final Logger anotherOneLogger = Logger.instance;

  print(identical(logger, anotherOneLogger)); // Output: true
}

Плюсы

  • Дает гарантию, что в программе может быть только один экземпляр класса;

  • Предоставляет глобальную точку доступа к экземпляру.

Минусы

  • Имеет проблемы с потоками в многопоточных языках;

  • Требует особой тактики тестирования при юнит-тестировании.

Abstract factory

Паттерн Abstract factory (Абстрактная фабрика) является порождающим паттерном проектирования.Целью паттерна является возможность создания семейства связанных объектов, не привязываясь к их конкретным классам.

Реализация:

  1. Выделение общих интерфейсов для семейств объектов.

  2. Определение интерфейса абстрактной фабрики, имеющего методы для создания каждого из типов семейств объектов.

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

Пример кода

/// Общий интерфейс для кнопок
abstract class Button {
  const Button();

  void paint();
}

/// Конкретная кнопка - Android
class AndroidButton implements Button {
  const AndroidButton();

  @override
  void paint() => print('AndroidButton is painted');
}

/// Конкретная кнопка - IOS
class IOSButton implements Button {
  const IOSButton();

  @override
  void paint() => print('IOSButton is painted');
}

/// Общий интерфейс для чекбоксов
abstract class CheckBox {
  const CheckBox();

  void paint();
}

/// Конкретный чекбокс - Android
class AndroidCheckBox implements CheckBox {
  const AndroidCheckBox();

  @override
  void paint() => print('AndroidCheckBox is painted');
}

/// Конкретный чекбокс - IOS
class IOSCheckBox implements CheckBox {
  const IOSCheckBox();

  @override
  void paint() => print('IOSCheckBox is painted');
}

/// Интерфейс абстрактной фабрики
abstract class GUIFactory {
  const GUIFactory();

  Button createButton();
  CheckBox createCheckBox();
}

/// Конкретная фабрика - Android
class AndroidFactory implements GUIFactory {
  const AndroidFactory();

  @override
  Button createButton() => AndroidButton();

  @override
  CheckBox createCheckBox() => AndroidCheckBox();
}

/// Конкретная фабрика - IOS
class IOSFactory implements GUIFactory {
  const IOSFactory();

  @override
  Button createButton() => IOSButton();

  @override
  CheckBox createCheckBox() => IOSCheckBox();
}

// Клиент, использующий абстрактную фабрику. Клиент не привязывается к
// конкретным классам объектов и может работать с любыми вариациями семейства
// объектов благодаря их абстрактным интерфейсам.
class Application {
  Application(this._factory) {
    _button = _factory.createButton();
    _checkBox = _factory.createCheckBox();
  }

  final GUIFactory _factory;
  late final Button _button;
  late final CheckBox _checkBox;

  void paint() {
    _button.paint();
    _checkBox.paint();
  }
}

void main() {
  late Application app;

  app = Application(IOSFactory());

  app.paint(); // Output: 'IOSButton is painted\nIOSCheckBox is painted'

  app = Application(AndroidFactory());

  app.paint(); // Output: 'AndroidButton is painted\nAndroidCheckBox is painted'
}

Плюсы

  • Реализует Принцип открытости/закрытости;

  • Упрощает замену и добавление новых семейств продуктов;

  • Гарантирует сочетаемость продуктов;

  • Избавляет клиентский код от привязки к конкретным классам продуктов.

Минусы

  • Усложняет код программы из-за введения множества дополнительных классов.

Adapter

Паттерн Adapter (Wrapper, Адаптер) является структурным паттерном проектирования.Цель паттерна заключается в обеспечении возможности совместной работы объектов с несовместимыми интерфейсами.

Реализация:

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

  2. Поместить в адаптер существующий класс с нужным функционалом, но не совместимым с интерфейсом, ожидаемом в клиенте.

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

Пример кода

/// Интерфейс, который ожидает клиент
abstract class Logger {
  void log(String message);
}

/// Существующий класс, имеющий желаемую функциональность, но несовместимый интерфейс
class ConsoleLogger {
  void consoleLog(String message) => print(message);
}

/// Класс - адаптер, адаптирующий [ConsoleLogger] к интерфейсу [Logger]
class ConsoleLoggerAdapter implements Logger {
  final ConsoleLogger _consoleLogger;

  ConsoleLoggerAdapter(this._consoleLogger);

  @override
  void log(String message) => _consoleLogger.consoleLog(message);
}

/// Клиент использует [ConsoleLoggerAdapter] для взаимодействия с [ConsoleLogger]
void main() {
  final Logger logger = ConsoleLoggerAdapter(ConsoleLogger());

  logger.log('Hello, World!'); // Output: 'Hello, World!'
}

Плюсы

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

Минусы

  • Усложняет код программы из-за введения дополнительных классов.

Decorator

Паттерн Decorator (Декоратор, Обертка) является структурным паттерном проектирования.Цель паттерна заключается в предоставлении возможности динамического добавления объектам новой функциональности, оборачивая их в классы-обёртки.

Реализация:

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

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

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

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

Пример кода

/// Интерфейс компонента
abstract class TextEditor {
  const TextEditor();

  abstract final String text;
}

/// Конкретный компонент
class SimpleTextEditor implements TextEditor {
  const SimpleTextEditor(this.text);

  @override
  final String text;
}

/// Базовый класс для декораторов
abstract class TextEditorDecorator implements TextEditor {
  const TextEditorDecorator(this.textEditor);

  final TextEditor textEditor;
}

/// Конкретный декоратор
class BoldTextDecorator extends TextEditorDecorator {
  const BoldTextDecorator(super.textEditor);

  @override
  String get text => '<b>${textEditor.text}</b>';
}

/// Конкретный декоратор
class ItalicTextDecorator extends TextEditorDecorator {
  const ItalicTextDecorator(super.textEditor);

  @override
  String get text => '<i>${textEditor.text}</i>';
}

void main() {
  TextEditor editor = SimpleTextEditor('Hello world!');

  print(editor.text); // Output: 'Hello, World!'

  editor = BoldTextDecorator(editor);
  print(editor.text); // Output: '<b>Hello, World!</b>'

  editor = ItalicTextDecorator(editor);
  print(editor.text); // Output: '<i><b>Hello, World!</b></i>'
}

Плюсы

  • Позволяет динамически добавлять одну или несколько новых обязанностей;

  • Имеет большую гибкость, чем у наследования.

Минусы

  • Множество мелких классов.

Если эта статья кажется вам простой, то огонь — приходите к нам работать! У нас открыта вакансия Flutter-разработчика, подробности можно узнать на сайте Mad Brains в разделе «Карьера».

Command

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

С паттерном Команда всегда связаны четыре термина:

  1. Команды (Command) - классы, представляющие действие как объект.

  2. Получатель (Приемник, Receiver) - класс, содержащий реализацию действий, команды делегируют ему свои действия, вызывая его методы.

  3. Отправитель (Invoker) - класс, вызывающий команды. Работает с командами только через их общий интерфейс, не зная ничего о конкретных командах. Отправитель может вести учёт и запись выполненных команд.

  4. Клиент (Client) - создает объекты конкретных команд и связывает их с отправителем для их выполнения.

Реализация:

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

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

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

  4. Создание в клиенте объекта получателя, объектов команд и их связь с отправителем.

Пример кода

/// Получатель
class TVControllerReceiver {
  TVControllerReceiver();

  int _currentChannel = 0;
  int _currentVolume = 0;

  int get currentChannel => _currentChannel;
  int get currentVolume => _currentVolume;

  void channelNext() {
    _currentChannel++;
    print('changed channel to next, now current is $_currentChannel');
  }

  void channelPrevious() {
    _currentChannel--;
    print('changed channel to previous, now current is $_currentChannel');
  }

  void volumeUp() {
    _currentVolume++;
    print('volume up, now current is $_currentVolume');
  }

  void volumeDown() {
    _currentVolume--;
    print('volume down, now current is $_currentVolume');
  }
}

/// Интерфейс команды
abstract class Command {
  abstract final TVControllerReceiver receiver;

  void execute();
}

/// Конкретная команда
class ChannelNextCommand implements Command {
  ChannelNextCommand(this.receiver);

  @override
  final TVControllerReceiver receiver;

  @override
  void execute() => receiver.channelNext();
}

/// Конкретная команда
class ChannelPreviousCommand implements Command {
  ChannelPreviousCommand(this.receiver);

  @override
  final TVControllerReceiver receiver;

  @override
  void execute() => receiver.channelPrevious();
}

/// Конкретная команда
class VolumeUpCommand implements Command {
  VolumeUpCommand(this.receiver);

  @override
  final TVControllerReceiver receiver;

  @override
  void execute() => receiver.volumeUp();
}

/// Конкретная команда
class VolumeDownCommand implements Command {
  VolumeDownCommand(this.receiver);

  @override
  final TVControllerReceiver receiver;

  @override
  void execute() => receiver.volumeDown();
}

/// Отправитель
class TVControllerInvoker {
  TVControllerInvoker();

  Command? _lastCommand;
  final List<String> _logs = [];

  void executeCommand(Command command) {
    command.execute();

    _lastCommand = command;
    _logs.add('${DateTime.now()} ${command.runtimeType}');
  }

  void repeatLastCommand() {
    final Command? command = _lastCommand;

    if (command != null) executeCommand(command);
  }

  void logHistory() {
    for (final String log in _logs) {
      print(log);
    }
  }
}

void main() {
  final TVControllerReceiver receiver = TVControllerReceiver();
  final TVControllerInvoker invoker = TVControllerInvoker();

  invoker.executeCommand(ChannelNextCommand(receiver));
  invoker.executeCommand(VolumeUpCommand(receiver));
  invoker.repeatLastCommand();

  invoker.logHistory();
}

Плюсы

  • Реализует Принцип открытости/закрытости;

  • Позволяет реализовать отмену и повтор операций, хранить историю их выполнения;

  • Позволяет собирать сложные команды из простых;

  • Позволяет реализовать отложенный запуск операций (ставить их в очередь).

Минусы

  • Усложняет код из-за необходимсоти создания множества дополнительных классов.

Visitor

Паттерн Visitor (Посетитель) является поведенческим паттерном проектирования.Цель паттерна заключается в предоставлении возможности добавлять в программу новые операции над объектами других классов без необходимости изменения этих классов.

Реализация:

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

  2. Реализация метода принятия accept(Visitor visitor)  посетителя в интерфейсе или базовом классе иерархии элементов, над которыми будет производиться операция "посещения".Иерархия элементов должна знать только о базовом интерфейсе посетителей, в то время как посетители будут знать о всех подклассах иерархии элементов.

  3. Реализация метода принятия accept(Visitor visitor)  посетителя во всех конкретных элементах иерархии. Каждый конкретный элемент должен делегировать выполнение метода accept(Visitor visitor)  тому методу посетителя, в котором тип параметра совпадает с текущим классом элемента.

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

  5. Клиенты создают объекты посетителей и передают их каждому элементу, использую метод принятия accept(Visitor visitor) .

Пример кода

/// Базовый класс иерархии элементов пользователей соц. сетей
abstract class SocialNetworkUser {
  const SocialNetworkUser();

  abstract final String name;
  abstract final String link;

  void accept(Visitor visitor);
}

/// Конкретный класс иерархии элементов пользователей соц. сетей
class VKUser extends SocialNetworkUser {
  const VKUser({required this.name, required this.link});

  @override
  final String name;

  @override
  final String link;

  @override
  void accept(Visitor visitor) => visitor.visitVKUser(this);
}

/// Конкретный класс иерархии элементов пользователей соц. сетей
class TelegramUser extends SocialNetworkUser {
  const TelegramUser({
    required this.name,
    required this.link,
    this.phoneNumber,
  });

  @override
  final String name;

  @override
  final String link;

  final String? phoneNumber;

  @override
  void accept(Visitor visitor) => visitor.visitTelegramUser(this);
}

/// Интерфейс посетителя с объявленными методами посещения каждого класса иерархии
abstract class Visitor {
  void visitVKUser(VKUser user);
  void visitTelegramUser(TelegramUser user);
}

/// Конкретный интерфейс, выполняющий действие
class LogInfoVisitor implements Visitor {
  @override
  void visitVKUser(VKUser user) {
    print('${user.runtimeType} - ${user.name} - ${user.link}');
  }

  @override
  void visitTelegramUser(TelegramUser user) {
    final String phoneNumber = user.phoneNumber ?? 'number hidden';

    print('${user.runtimeType} - ${user.name} - ${user.link} - $phoneNumber');
  }
}

void main() {
  const List<SocialNetworkUser> users = [
    VKUser(name: 'Павел Дуров', link: 'vk.com/id1'),
    VKUser(name: 'Дмитрий Медведев', link: 'vk.com/dm'),
    TelegramUser(name: 'Ivan', link: 't.me/ivan', phoneNumber: '+78005553535'),
    TelegramUser(name: 'Anonym', link: 't.me/anon'),
  ];

  final LogInfoVisitor logInfoVisitor = LogInfoVisitor();

  for (final SocialNetworkUser user in users) {
    user.accept(logInfoVisitor);
  }
}

Плюсы

  • Упрощает добавление новых операций над элементами;

  • Объединяет родственные операции в классе Visitor ;

  • Дает возможность запоминать состояние по мере обхода элементов.

Минусы

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

Если вам интересно развиваться во Flutter, хорошая новость: у нас открыта вакансия Flutter-разработчика, подробности можно узнать на сайте Mad Brains в разделе «Карьера».

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

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


  1. Mitai
    20.05.2023 09:26

    InheritedWidget как с помощью него сделать такое
    ```MultiProvider(
    providers: [
    ChangeNotifierProvider(create: (context) => TabManager()),
    ChangeNotifierProvider(create: (context) => GroceryManager()),
    ]```
    Еще было бы интересно почитать вот эту или похожую статью про Navigator 2.0 адаптированную под Dart 3 and Flutter 3.10 +
    https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade


  1. alexshipin
    20.05.2023 09:26
    +2

    Рекомендую к ознакомлению, с примерами, интерактивными решениями.


    1. Mitai
      20.05.2023 09:26

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