
Изучив классы фреймворка, пакеты сторонних разработчиков или документацию Dart / Flutter, вы, натыкались на ключевое слово factory и задавались вопросом, что это значит.
В этой статье мы собираемся прояснить:
Значение ключевого слова
Когда вам следует его использовать
Разница между
factoryи порождающим конструкторомРазличия между
factoryиstatic
Фабричный метод
Прежде чем мы углубимся в синтаксис и семантику ключевого слова factory, давайте рассмотрим его происхождение.
Ключевое слово factory в Dart на самом деле является синтаксическим сахаром для выражения чего-то. Это базовый паттерн, который называется фабричным методом или шаблоном фабричного метода.
По сути, конструктор по умолчанию (тот, который вы вызываете с помощью Cat()) — это не что иное, как static метод, определенный в классе ( Cat ), возвращаемый тип которого должен быть того же типа ( Cat ). Основным отличием по сравнению с «нормальной» static функцией в классе является невозможность изменения его возвращаемого типа.
Основные преимущества использования паттерна проектирования "фабрика" следующие:
Ответственность за создание объектов лежит не внутри самого класса, а в отдельном классе (фабричный классе), который реализует интерфейс.
Создание объекта лишено гибкости, поскольку вы не можете изменить конкретный экземпляр объекта.
Другими словами:
Breederне нужно знать, как создать экземплярCat, потому чтоCatпроизводится на фабриках ????????.Breederтолько говоритmakeACat(), и фабрика возвращает новогоCat.
В этом и преимущество, что Breeder не меняет своего поведения, если меняется способ производства Cat.
Вывод: Его можно использовать для создания объектов, не раскрывая вызывающей стороне детали базовой реализации.
Типы конструкторов
В Dart есть порождающие и фабричные конструкторы, которые могут быть именованные или неименованные.
Примером порождающего конструктора является конструктор по умолчанию, который создается автоматически для каждого класса.
Давайте рассмотрим класс, чтобы лучше понять различные типы конструкторов:
class Cat {
final Color color;
}
В нашем примере существует класс Cat только с одним свойством: color типа Color.
Типы конструкторов будут выглядеть следующим образом:
Генеративный |
Фабричный |
|
неименованный |
|
|
именованный |
|
|
Имейте в виду, что вам не разрешается создавать конструктор
factoryс именем, подобным уже существующему конструктору - либо порождающему, либо фабричному, и не имеет значения, именованный или неименованный.Единственным исключением для этого является случай, когда вы определяете неименованный
factoryконструктор, явно не определив неименованный порождающий конструктор. Когда вы не определяете неименованный конструктор, он будет сгенерирован для вас автоматически, а когда вы определитеfactoryконструктор, он будет переопределен.
Ключевое слово в Dart
Ключевое слово factory не является точной реализацией 1:1 того, как в классических языков ООП, таких как C++ или Java.
Идея заключается в том, чтобы иметь отдельный класс, который обрабатывает создание объекта (например, CatFactory ????).
Однако, используя factory конструктор, вы по-прежнему сохраняете логику создания объекта внутри того же класса. За исключением создания экземпляров подклассов, что также возможно с помощью factory конструкторов.
Когда вам следует его использовать
"Используйте ключевое слово
factoryпри реализации конструктора, который не всегда создает новый экземпляр своего класса. Например, конструкторfactoryможет возвращать экземпляр из кэша или экземпляр подтипа. Другим вариантом использования фабричных конструкторов является инициализация конечной переменной с использованием логики, которая не может быть обработана в списке инициализаторов."
В документации в основном упоминаются три варианта использования:
Возврат экземпляра из кэша
Возвращает экземпляр подтипа
Инициализация конечной переменной
Давайте объясним пример документов один за другим.
Экземпляр из кэша
Давайте представим, что у нас есть ColorComputer, которому требуется очень много времени, чтобы вычислить цвет кошки. У нас также есть CatCache, в котором хранится последний созданный цвет кошки, чтобы избежать необходимости выполнять heavyColorComputation() каждый раз, когда создается экземпляр Cat.colored.
class Cat {
Cat(this.color);
factory Cat.colored(CatCache catCache) {
Cat? cachedCat = catCache.getCachedColorCat();
if (cachedCat != null) {
return cachedCat;
}
Color color = ColorComputer().heavyColorComputation();
return Cat(color);
}
final Color color;
}
В этом случае можно использовать конструктор factory, т.к. мы вернем существующий экземпляр Cat (если кэш попадет).
Сложная инициализация final переменной
Если у вас есть более сложная инициализация final переменной, которая не может быть обработана внутри списка инициализаторов. Для решения можно использовать factory конструктор.
class Cat {
Cat._({
required this.id,
required this.name,
required this.age,
required this.color
});
final int id;
final String name;
final int age;
final Color color;
factory Cat.fromJson(Map<String, dynamic> json) {
DateTime now = DateTime.now();
late Color color;
if (now.hour < 12) {
color = const Color(0xFF000000);
}
else {
color = const Color(0xFFFFFFFF);
}
return Cat._(
id: json['id'],
name: json['name'],
age: json['age'],
color: color,
);
}
void meow() {
print('Meow!');
}
void whoAmI() {
print('I am $name ($id) and I am $age years old. My color is $color.');
}
}
Здесь инициализация переменной color требует некоторой логики. Поскольку необходимо выполнить несколько инструкций, это лучше сделать внутри конструктора factory.
Давайте вызовем конструктор fromJson и проверим его выходные данные:
const String myJson = '{"id": 5, "name": "Herbert", "age": 7}';
final Cat decodedCat = Cat.fromJson(jsonDecode(myJson));
decodedCat.meow();
decodedCat.whoAmI();
Это приводит к следующему результату:
"Meow!
I am Herbert (5) and I am 7 years old. My color is Color(0xffffffff)."— decodedCat
Экземпляр подтипа
Другим вариантом использования конструктора factory является возврат экземпляра производного класса. Это невозможно с помощью порождающего конструктора.
Это может быть полезно, если логика принятия решения о том, какой подкласс возвращать, всегда одинакова во всем вашем приложении. Вместо того чтобы дублировать его, вы могли бы реализовать его в централизованном месте.
abstract class Cat {
Cat({required this.age});
int age;
factory Cat.makeCat(bool aggressive, int age) {
if (aggressive || age < 3) {
return AggressiveCat(age: age);
}
return DefensiveCat(age: age);
}
void fight();
}
class AggressiveCat extends Cat {
AggressiveCat({required super.age});
@override
void fight() {
print('Where dem enemies at?!');
}
}
class DefensiveCat extends Cat {
DefensiveCat({required super.age});
@override
void fight() {
print('Nah, I\'m staying!');
}
}
Разница между порождающим конструктором и factory конструктором
Что мы узнали: порождающий конструктор всегда возвращает новый экземпляр класса, именно поэтому ему не нужно ключевое слово return.
С другой стороны, factory конструктор связан с гораздо более слабыми ограничениями. Для конструктора factory достаточно, чтобы возвращаемый класс, имел тот же тип, что и сам класс, или он удовлетворяет его интерфейсу (например, подклассу). Это может быть новый экземпляр класса, но также может быть существующий экземпляр класса (как показано в примере кэша выше).
Фабрика может использовать поток управления, чтобы определить, какой объект возвращать, и поэтому должна использовать ключевое слово return. Для того чтобы фабрика вернула новый экземпляр класса, она должна сначала вызвать порождающий конструктор.
Все незначительные и основные различия:
Фабричные конструкторы могут вызывать другой конструктор (и должны вызывать, если он не возвращает существующий экземпляр)
Фабричные конструкторы не могут использовать список инициализаторов (поскольку они напрямую не создают новый экземпляр)
Фабричным конструкторам в отличие от порождающего конструктора разрешено возвращать существующий объект
Фабричным конструкторам разрешено возвращать подкласс
Фабричным конструкторам не нужно инициализировать переменные экземпляра класса
-
Производный класс не может вызывать фабричный конструктор суперкласса. Как следствие, класс, предоставляющий исключительно фабричные конструкторы, не может быть расширен.
В противном случае компилятор будет жаловаться: “Ожидается порождающий конструктор, но была найдена фабрика”.
Порождающие конструкторы не могут устанавливать
finalcвойства в теле конструктораПорождающие конструкторы могут быть
constи не нуждаются в перенаправлении
Разница между static и factory
Вы можете спросить себя: “Но зачем мне нужно это ключевое слово? Разве я не могу просто использовать обычные статические методы?!”.
На самом деле, нет большой разницы между static методом и factory конструктором. Хотя синтаксис немного отличается.
Вообще говоря, static метод имеет более слабые ограничения, но также и меньше синтаксического сахара. Это потому, что каждый factory конструктор является (технически) static методом, но не каждый static метод является factory конструктором. Таким образом, если вы определяете factory конструктор, компилятор знает о ваших намерениях и может поддержать вас. Самое большое различие, заключается в том, что возвращаемый тип фабричного конструктора устанавливается для текущего класса или производных классов, в то время как для статического метода вы можете указать любой возвращаемый тип.
Если мы воспользуемся одним из приведенных выше примеров, то увидим, что мы можем достичь того же результата с помощью static метода:
class Cat {
Cat._({
required this.id,
required this.name,
required this.age,
required this.color
});
final int id;
final String name;
final int age;
final Color color;
static Cat catfromJson(Map<String, dynamic> json) {
DateTime now = DateTime.now();
late Color color;
if (now.hour < 12) {
color = const Color(0xFF000000);
}
else {
color = const Color(0xFFFFFFFF);
}
return Cat._(
id: json['id'],
name: json['name'],
age: json['age'],
color: color,
);
}
factory Cat.fromJson(Map<String, dynamic> json) {
DateTime now = DateTime.now();
late Color color;
if (now.hour < 12) {
color = const Color(0xFF000000);
}
else {
color = const Color(0xFFFFFFFF);
}
return Cat._(
id: json['id'],
name: json['name'],
age: json['age'],
color: color,
);
}
void meow() {
print('Meow');
}
void whoAmI() {
print('I am $name ($id) and I am $age years old. My color is $color.');
}
}
С точки зрения удобства читаемости кода, хорошей практикой является использование factory конструктора вместо static методов. Это делает создание объекта более очевидным.
Чтобы дать вам полный обзор, я перечислил все различия:
factoryконструктор в отличие отstaticметода может возвращать только экземпляр текущего класса или подклассовstaticметод может бытьasync. Посколькуfactoryконструкторам необходимо возвращать экземпляр текущего или подкласса, он не может вернутьFuture.staticметод не может быть неименованным, в то время какfactoryконструкторы могутЕсли вы укажете именованный
factoryконструктор, конструктор по умолчанию будет автоматически удаленФабричные конструкторы могут использовать специальный синтаксис для перенаправления
Для фабричного конструктора не обязательно указывать общие параметры
Фабричные конструкторы могут быть объявлены
constФабричный конструктор не может возвращать тип, допускающий значение null.
При создании документации
dartdoc,factoryконструкторы будут перечислены в разделе “Конструкторы”.staticметод будет найден в другом месте в нижней части документации
Таким образом, подвох кроется в деталях, но с точки зрения низкого уровня не имеет значения, используете ли вы static метод или factory конструктор.
Вывод
Ключевое слово factory может быть полезно, если создание экземпляра класса превышает определенную сложность. Это может иметь место при кэшировании или при использовании иной сложной логики.
Кроме того, ничто не противоречит использованию static метода. Хотя factory конструкторы предназначены именно для создания экземпляра, в то время как static метод имеет гораздо более широкую область применения.
В конечном счете, все сводится к личным предпочтениям.
PackRuble
Поток мыслей в оригинальной статье порядком сумбурный, однако это не отменяет практической значимости статьи :) Пишите/переводите ещё, материал интересный ????