Дисциплины - Объектно-ориентированное программирование

Шаблоны проектирования практических задач - Порождающие шаблоны проектирования - Фабрика и фабричный метод

Фабрика — это общая концепция проектирования функций, методов и классов, когда какая-то одна часть программы отвечает за создание других частей программы.

В программировании "фабрика" означает:

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

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

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

  • Представлен новый формат: нужно вносить изменения в метод, чтобы имплементировать сериализацию в данный формат.
  • Изменяется объект Song: добавление или удаление свойств класса Song потребует изменения реализации для размещения новой структуры.
  • Изменяется строковое представления формата (JSON и JSON API): необходимо изменять метод .serialize() вместе со строковым представлением формата, потому что представление жестко запрограммировано в реализации метода .serialize().

В идеале, любое изменение вносится без использования метода .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() является компонентом-создателем. Создатель решает, какую реализацию использовать.

Количество комментариев: 0

Для того, чтобы оставить коментарий необходимо зарегистрироваться
814301 БГУИР
814302 БГУИР
814303 БГУИР
894351 БГУИР
90421 БГУИР


Изображения Видео

1. Абстрактная фабрика https://www.youtube.com/watch?v=1mVONOCxfLg
2. Фабричный метод https://www.youtube.com/watch?v=5UqUDR6_2cY
3. Шаблон декоратор https://www.youtube.com/watch?v=Lwb9bm8yKD0
4. Dessign patterns on PHP https://github.com/domnikl/DesignPatternsPHP
5. Приёмы объектно-ориентированного проектирования. Паттерны проектирования Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес; [пер. с англ.: А. Слинкин науч. ред.: Н. Шалаев]. — Санкт-Петербург [и др.] : Питер, 2014. — 366 с. : ил. ; 24 см.
6. Приемы объектно-ориентированного проектирования. Паттерны проектирования Э. Гамма, Р. Хелм, Р. Джонсон, Д. Влиссидес; [пер. с англ.: А. Слинкин науч. ред.: Н. Шалаев]. — Санкт-Петербург [и др.] : Питер, 2014. — 366 с. : ил. ; 24 см.
7. Ajax http://erud.by/ajax
8. Ajax http://erud.by/ajax
9. Ajax http://erud.by/ajax
10. Документация Laravel http://laravel.com
Задание к курсовой работе
Задание к курсовой работе
Вопросы к экзамену