Изучив классы фреймворка, пакеты сторонних разработчиков или документацию 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.

Типы конструкторов будут выглядеть следующим образом:

Генеративный

Фабричный

неименованный

Cat(this.color);

factory Cat(Color color) {

return Cat(color);

}

именованный

Cat.colored(this.color);

factory Cat.colored(Color color) {

return Cat(color);

}

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

Единственным исключением для этого является случай, когда вы определяете неименованный factory конструктор, явно не определив неименованный порождающий конструктор. Когда вы не определяете неименованный конструктор, он будет сгенерирован для вас автоматически, а когда вы определите factory конструктор, он будет переопределен.

Ключевое слово в Dart

Ключевое слово factory не является точной реализацией 1:1 того, как в классических языков ООП, таких как C++ или Java.

Идея заключается в том, чтобы иметь отдельный класс, который обрабатывает создание объекта (например, CatFactory ????).

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

Когда вам следует его использовать

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

Официальная документация по Dart

В документации в основном упоминаются три варианта использования:

  • Возврат экземпляра из кэша

  • Возвращает экземпляр подтипа

  • Инициализация конечной переменной

Давайте объясним пример документов один за другим.

Экземпляр из кэша

Давайте представим, что у нас есть 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. Для того чтобы фабрика вернула новый экземпляр класса, она должна сначала вызвать порождающий конструктор.

Все незначительные и основные различия:

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

  • Фабричные конструкторы не могут использовать список инициализаторов (поскольку они напрямую не создают новый экземпляр)

  • Фабричным конструкторам в отличие от порождающего конструктора разрешено возвращать существующий объект

  • Фабричным конструкторам разрешено возвращать подкласс

  • Фабричным конструкторам не нужно инициализировать переменные экземпляра класса

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

    • В противном случае компилятор будет жаловаться: “Ожидается порождающий конструктор, но была найдена фабрика”.

  • Порождающие конструкторы не могут устанавливать final cвойства в теле конструктора

  • Порождающие конструкторы могут быть 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 метод имеет гораздо более широкую область применения.

В конечном счете, все сводится к личным предпочтениям.

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


  1. PackRuble
    24.04.2023 20:47
    +1

    Поток мыслей в оригинальной статье порядком сумбурный, однако это не отменяет практической значимости статьи :) Пишите/переводите ещё, материал интересный ????