Фабрика — это общая концепция проектирования функций, методов и классов, когда какая-то одна часть программы отвечает за создание других частей программы.
В программировании "фабрика" означает:
Простая фабрика — это класс, в котором есть один метод с большим условным оператором, выбирающим создаваемый продукт. Этот метод вызывают с неким параметром, по которому определяется какой из продуктов нужно создать.
Фабричный метод (англ. Factory method) — порождающий шаблон проектирования, предоставляющий подклассам интерфейс для создания экземпляров некоторого класса. В момент создания наследники могут определить, какойкласс создавать. Иными словами, Фабрика делегирует создание объектов наследникам родительского класса. Этопозволяет использовать в коде программы не специфические классы, а манипулировать абстрактными объектами наболее высоком уровне. Также известен под названием виртуальный конструктор (англ. Virtual Constructor).
Фабричный метод используется для создания общего интерфейса. Например, приложению требуется объект с определенным интерфейсом для выполнения задач. Реализация интерфейса определяется некоторым параметром.
Вместо использования сложной структуры из условий if/elif/else
для определения реализации, приложение делегирует это решение отдельному компоненту, который создает конкретный объект. При таком подходе код приложения упрощается, становится более удобным для повторного использования и поддержки.
Фабричный метод позволяет классу делегировать создание подклассов. Используется, когда:
Фабрика в JavaScript и Node.js
В качестве поясняющего примера рассмотрим следующую фабрику, или функцию возвращающую объект:
function createImage(name){ return new Image(name); } const image = createImage('photo.jpg');
На первый взгляд, фабрика createImage кажется не нужной. Почему бы просто не создать объект класса Image с помощью оператора new?
const image = new Image('photo.jpg');
Однако, использование фабрики позволяет не привязываться к конкретному типу объектов, в данном случае к типу Image. Фабрика обеспечивает большую гибкость. Например, если потребуется выполнить рефакторинг класса Image и разделить его на несколько более специализированных классов, по одному для каждого из поддерживаемых форматов изображений.
function createImage(name){ if(name.match(/\.jpg$/)){ return new JpegImage(name); }else if(name.match(/\.gif$/)){ return new GifImage(name); }else if(name.match(/\.png$/)){ return new PngImage(name); }else{ throw new Exception('Unsupported format'); } }
Фабрика в PHP
class PhoneFactory { public function createPhone() { return new \Phone(); } } $phoneFactory = new PhoneFactory(); $phoneA = $phoneFactory->createPhone(); $phoneB = $phoneFactory->createPhone();
Фабрика в Laravel
Для создания фабрики у Laravel имеется специальная artisan-команда make:factory
:
php artisan make:factory PostFactory
В папке database/factories появится файл PostFactory со следующим содержимым:
use Faker\Generator as Faker; $factory->define(Model::class, function (Faker $faker) { return [ // ]; });
Если нужно создать фабрику, и связать ее с моделью, можно использовать параметр --model
php artisan make:factory KeywordFactory --model=Keyword
В папке database/factories появится файл KeywordFactory со следующим содержимым:
use Faker\Generator as Faker; $factory->define(App\Keyword::class, function (Faker $faker) { return [ // ]; });
Реализация фабричного метода осуществляется с помощью специального хелпера factory(), который может быть использован:
1. для тестирования моделей Eloquent.
При тестировании вам может понадобиться вставить несколько записей в вашу БД перед выполнением теста. При создании этих данных Laravel позволяет вам вместо указания значений каждого столбца вручную определить стандартный набор атрибутов для каждой из моделей Eloquent с помощью фабрик. Файл database\factories\ModelFactory.php содержит определение одной фабрики:
/** @var \Illuminate\Database\Eloquent\Factory $factory */ $factory->define(App\User::class, function (Faker\Generator $faker) { static $password; return [ 'name' => $faker->name, 'email' => $faker->unique()->safeEmail, 'password' => $password ?: $password = bcrypt('secret'), 'remember_token' => str_random(10), ]; });
Генерация фэйковых данных:
use Faker\Generator as Faker; use Illuminate\Support\Str; $factory->define(App\User::class, function (Faker $faker) { return [ 'name' => $faker->name, 'email' => $faker->unique()->safeEmail, 'email_verified_at' => now(), 'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret 'remember_token' => Str::random(10), ]; });
2. В методах загрузки первоначальных данных.
public function run() { factory(App\User::class, 50)->create()->each(function ($user) { $user->posts()->save(factory(App\Post::class)->make()); }); }
После определения фабрик вы можете использовать глобальную функцию factory() в своих тестах или файлах начальных данных для генерирования экземпляров модели. Рассмотрим несколько примеров создания моделей.
1. С использованием метода make()
для создания моделей, но не сохранения их в базе данных:
public function testDatabase() { $user = factory(App\User::class)->make(); // Использование модели в тестах... }
Создание трёх экземпляров App\User...
$users = factory(App\User::class, 3)->make();
Переопределение атрибутов:
$user = factory(App\User::class)->make([ 'name' => 'Abigail', ]);
2. Метод create()
не только создаёт экземпляры моделей, но также сохраняет их в БД с помощью метода Eloquent save():
public function testDatabase() { // Создание одного экземпляра App\User... $user = factory(App\User::class)->create(); // Создание трёх экземпляров App\User... $users = factory(App\User::class, 3)->create(); // Использование модели в тестах... }
При использовании метода create() для создания нескольких моделей возвращается экземпляр коллекции Eloquent, позволяя использовать все удобные функции для работы с коллекцией, такие как each()
.
Реализация отношений с помощью each().
$users = factory(App\User::class, 3) ->create() ->each(function ($u) { $u->posts()->save(factory(App\Post::class)->make()); });
Фабричный метод в Python
Рассмотрим код приложения, преобразовывающего объект Song в String. Преобразование объекта называется сериализацией. Эти требования часто реализованы в одной функции или методе, которые содержат всю логику:
#serializer_demo.py import json import xml.etree.ElementTree as et class Song: def __init__(self, song_id, title, artist): self.song_id = song_id self.title = title self.artist = artist class SongSerializer: def serialize(self, song, format): if format == 'JSON': song_info = { 'id': song.song_id, 'title': song.title, 'artist': song.artist } return json.dumps(song_info) elif format == 'XML': song_info = et.Element('song', attrib={'id': song.song_id}) title = et.SubElement(song_info, 'title') title.text = song.title artist = et.SubElement(song_info, 'artist') artist.text = song.artist return et.tostring(song_info, encoding='unicode') else: raise ValueError(format)
В приведенном выше примере есть базовый класс Song для представления песни и класс SongSerializer, который преобразовывает объект Song в его строковое представление в соответствии со значением параметра format.
Метод .serialize() поддерживает два разных формата: JSON и XML. Любой другой указанный формат не поддерживается, поэтому возникает исключение ValueError.
Воспользуемся интерактивной оболочкой Python, чтобы увидеть, как работает код:
>>> import serializer_demo as sd >>> song = sd.Song('1', 'Water of Love', 'Dire Straits') >>> serializer = sd.SongSerializer() >>> serializer.serialize(song, 'JSON') '{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}' >>> serializer.serialize(song, 'XML') 'Water of LoveDire Straits' >>> serializer.serialize(song, 'YAML') Traceback (most recent call last): File "", line 1, in File "./serializer_demo.py", line 30, in serialize raise ValueError(format) ValueError: YAML
Код выполняет много действий. Согласно принципу единственной ответственности, модуль, класс и даже метод должны выполнять одну функцию в коде и иметь причину изменения своих состояний.
Метод .serialize() в SongSerializer может изменяться по многим причинам. Такое поведение способно привести к появлению проблем. Давайте рассмотрим все возможные ситуации, когда придется вносить изменения в реализацию:
В идеале, любое изменение вносится без использования метода .serialize().
Если вы видите сложный код с условиями, определите общие цели каждого логического ответвления.
Код, в котором используются if/elif/else
, обычно имеет общую цель, которая реализуется по-разному. Приведенный выше код преобразует объект Song в строчный формат, используя разные методы в каждой логической ветке.
Нужно найти общий интерфейс, который можно использовать для замены каждой ветви. В приведенном выше примере требуется такой интерфейс, который принимает объект Song и возвращает строку.
Когда есть общий интерфейс, мы предоставляем отдельные реализации для каждого способа.
В примере выше мы сначала осуществляем сериализацию в JSON и XML, а затем предоставляем отдельный компонент, который решает, какую реализацию использовать на основе указанного формата. Этот компонент оценивает значение format и возвращает конкретную реализацию, определенную его значением.
Внесем изменения в существующий код без изменения поведения − рефакторинг кода.
Рефакторинг кода в желаемый интерфейс. Желаемый интерфейс − это объект или функция, которая принимает объект Song и возвращает строковое представление.
Первым шагом является рефакторинг одного из логических ответвлений в этот интерфейс. Добавляем новый метод ._serialize_to_json() и перемещаем в него код сериализации JSON. Затем изменяем клиент для вызова, вместо реализации в теле оператора if
:
class SongSerializer: def serialize(self, song, format): if format == 'JSON': return self._serialize_to_json(song) # The rest of the code remains the same def _serialize_to_json(self, song): payload = { 'id': song.song_id, 'title': song.title, 'artist': song.artist } return json.dumps(payload)
После внесения этих изменений вы сможете убедиться, что поведение осталось прежним. Затем делаем то же самое для XML, представляя новый метод ._serialize_to_xml(), перемещая реализацию в него и изменяя ветку elif
для вызова.
В следующем примере показан переработанный код:
class SongSerializer: def serialize(self, song, format): if format == 'JSON': return self._serialize_to_json(song) elif format == 'XML': return self._serialize_to_xml(song) else: raise ValueError(format) def _serialize_to_json(self, song): payload = { 'id': song.song_id, 'title': song.title, 'artist': song.artist } return json.dumps(payload) def _serialize_to_xml(self, song): song_element = et.Element('song', attrib={'id': song.song_id}) title = et.SubElement(song_element, 'title') title.text = song.title artist = et.SubElement(song_element, 'artist') artist.text = song.artist return et.tostring(song_element, encoding='unicode')
Новая версия воспринимается проще, но ее можно улучшать дальше с помощью базовой реализации фабричного метода.
Базовая реализация фабричного метода. В данном случае, применение фабричного метода заключается в том, чтобы предоставить отдельному компоненту ответственность за решение, какую реализацию следует использовать на основе определенного параметра. Этим параметром в нашем примере является format.
Чтобы завершить реализацию фабричного метода, вы добавляете новый метод ._get_serializer(), который принимает желаемый формат. Он оценивает значение format и возвращает соответствующую функцию сериализации:
class SongSerializer: def _get_serializer(self, format): if format == 'JSON': return self._serialize_to_json elif format == 'XML': return self._serialize_to_xml else: raise ValueError(format)
Завершим реализацию фабричного метода:
class SongSerializer: def serialize(self, song, format): serializer = self._get_serializer(format) return serializer(song) def _get_serializer(self, format): if format == 'JSON': return self._serialize_to_json elif format == 'XML': return self._serialize_to_xml else: raise ValueError(format) def _serialize_to_json(self, song): payload = { 'id': song.song_id, 'title': song.title, 'artist': song.artist } return json.dumps(payload) def _serialize_to_xml(self, song): song_element = et.Element('song', attrib={'id': song.song_id}) title = et.SubElement(song_element, 'title') title.text = song.title artist = et.SubElement(song_element, 'artist') artist.text = song.artist return et.tostring(song_element, encoding='unicode')
Окончательная реализация показывает различные компоненты фабричного метода. Методы ._serialize_to_json() и ._serialize_to_xml() являются конкретными реализациями продукта. Наконец, метод ._get_serializer() является компонентом-создателем. Создатель решает, какую реализацию использовать.