редакции Выбор
Паттерны проектирования на Dart с примерами кода. Часть первая
Паттерны проектирования на 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 (Абстрактная фабрика) является порождающим паттерном проектирования.Целью паттерна является возможность создания семейства связанных объектов, не привязываясь к их конкретным классам.
Реализация:
- Выделение общих интерфейсов для семейств объектов.
- Определение интерфейса абстрактной фабрики, имеющего методы для создания каждого из типов семейств объектов.
- Создание для каждого семейства объектов конкретного класса фабрики, реализующего интерфейс абстрактной фабрики.
Пример кода
/// Общий интерфейс для кнопок
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 IOSCheckBox is painted’
app = Application(AndroidFactory());
app.paint(); // Output: ’AndroidButton is painted AndroidCheckBox is painted’
}
Плюсы
- Реализует Принцип открытости/закрытости;
- Упрощает замену и добавление новых семейств продуктов;
- Гарантирует сочетаемость продуктов;
- Избавляет клиентский код от привязки к конкретным классам продуктов.
Минусы
- Усложняет код программы из-за введения множества дополнительных классов.
Adapter
Паттерн Adapter (Wrapper, Адаптер) является структурным паттерном проектирования.Цель паттерна заключается в обеспечении возможности совместной работы объектов с несовместимыми интерфейсами.
Реализация:
- Создание класса адаптера, реализующего интерфейс, который ожидает клиент.
- Поместить в адаптер существующий класс с нужным функционалом, но не совместимым с интерфейсом, ожидаемом в клиенте.
- Реализовать в адаптере все методы интерфейса, который ожидает клиент, делегируя всю работу помещенному в адаптер классу.
Пример кода
/// Интерфейс, который ожидает клиент
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 (Декоратор, Обертка) является структурным паттерном проектирования.Цель паттерна заключается в предоставлении возможности динамического добавления объектам новой функциональности, оборачивая их в классы-обёртки.
Реализация:
- Создание интерфейса компонента, описывающего общие методы как для конкретного компонента, так и для его декораторов.
- Создание класса конкретного компонента, содержащего основную бизнес-логику. Конкретный компонент должен следовать интерфейсу компонента.
- Создание базового класса для декораторов. Он должен хранить в себе ссылку на вложенный компонент. Базовый декоратор должен следовать тому же интерфейсу компонента, что и конкретный компонент.
- Создание классов конкретных декораторов, наследующих базовый. Конкретный декоратор должен выполнять свою добавочную функцию до или после вызова этой же функции обёрнутого объекта.
Пример кода
/// Интерфейс компонента
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 => ’${textEditor.text}’;
}
/// Конкретный декоратор
class ItalicTextDecorator extends TextEditorDecorator {
const ItalicTextDecorator(super.textEditor);
@override
String get text => ’${textEditor.text}’;
}
void main() {
TextEditor editor = SimpleTextEditor(’Hello world!’);
print(editor.text); // Output: ’Hello, World!’
editor = BoldTextDecorator(editor);
print(editor.text); // Output: ’Hello, World!’
editor = ItalicTextDecorator(editor);
print(editor.text); // Output: ’Hello, World!’
}
Плюсы
- Позволяет динамически добавлять одну или несколько новых обязанностей;
- Имеет большую гибкость, чем у наследования.
Минусы
- Множество мелких классов.
Если эта статья кажется вам простой, то огонь — приходите к нам работать! У нас открыта вакансия Flutter-разработчика, подробности можно узнать на сайте Mad Brains в разделе «Карьера».
Command
Паттерн Command (Команда) является поведенческим паттерном проектирования.Цель паттерна заключается в представлении действий как объектов, заключающих в себе само действие и его параметры, что позволяет сохранять историю действий, ставить их в очередь, поддерживать их отмену и повтор.
С паттерном Команда всегда связаны четыре термина:
- Команды (Command) — классы, представляющие действие как объект.
- Получатель (Приемник, Receiver) — класс, содержащий реализацию действий, команды делегируют ему свои действия, вызывая его методы.
- Отправитель (Invoker) — класс, вызывающий команды. Работает с командами только через их общий интерфейс, не зная ничего о конкретных командах. Отправитель может вести учёт и запись выполненных команд.
- Клиент (Client) — создает объекты конкретных команд и связывает их с отправителем для их выполнения.
Реализация:
- Создание общего интерфейса для команд, определение в нем метода вызова команды.
- Создание классов конкретных команд. В каждом классе должно быть поле, хранящее объект-получатель, которому команда будет делегировать свою работу. При необходимости команда должна получать через конструктор и хранить в себе поля параметров, необходимых для вызова методов получателя.
- Добавление в класс отправитель метода для вызова команды. Отправитель может как хранить в себе команды для их вызова, так и принимать команду в методе для ее вызова, либо иметь сеттер для поля команды.
- Создание в клиенте объекта получателя, объектов команд и их связь с отправителем.
Пример кода
/// Получатель
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 _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 (Посетитель) является поведенческим паттерном проектирования.Цель паттерна заключается в предоставлении возможности добавлять в программу новые операции над объектами других классов без необходимости изменения этих классов.
Реализация:
- Создание интерфейса посетителя и объявление в нем методов посещения visit() для каждого класса, над которым будет выполняться операция «посещения».
- Реализация метода принятия accept(Visitor visitor) посетителя в интерфейсе или базовом классе иерархии элементов, над которыми будет производиться операция "посещения".Иерархия элементов должна знать только о базовом интерфейсе посетителей, в то время как посетители будут знать о всех подклассах иерархии элементов.
- Реализация метода принятия accept(Visitor visitor) посетителя во всех конкретных элементах иерархии. Каждый конкретный элемент должен делегировать выполнение метода accept(Visitor visitor) тому методу посетителя, в котором тип параметра совпадает с текущим классом элемента.
- Создание новых конкретных посетителей для каждого нового действия над элементами иерархии. Конкретный посетитель должен реализовывать все методы интерфейса посетителей, выполняя требуемое действие.
- Клиенты создают объекты посетителей и передают их каждому элементу, использую метод принятия 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 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 в разделе «Карьера».
Вторая часть статьи выйдет уже на следующей неделе. В комментариях пишите, что было полезно, и что хотели бы узнать о технологии подробнее.