Пишу свои мысли о web-разработке и о жизни. Работаю в веб-студии Феникс — phoenix-cg.ru Связаться со мной можно по почте viktor@koreysha.ru

Абстрактная фабрика и примеси в PHP (Часть 2)

22 марта 2016, 10:29

Первая часть

Однако, вернемся к нашему случаю. Допустим тут в комнату входит начитанный разработчик (НР), смотрит на код и вспоминает, что по условиям игры все светлые умеют лечить, а все темные становятся невидимыми. Добавим соответствующие методы.

<?php

abstract class Infantry {
    //общие для всей пехоты методы
    abstract public function Attack();
    abstract public function Defend();
}
abstract class Archer {
    abstract public function Shoot();
}
abstract class SiegeWeapons {
    abstract public function Fire();
}

class GoodInfantry extends Infantry {
    //Спецэфическая реализация для доброго пехотинца
    public function Attack();
    public function Defend();
    
    public function Treat();
}
class GoodArcher extends Archer{
    public function Shoot();
    
    public function Treat();
}
class GoodSiegeWeapons extends SiegeWeapons{
    public function Fire(); 
    
    public function Treat();
}

class EvilInfantry extends Infantry{
    //Спецэфическая реализация для злого пехотинца
    public function Attack();
    public function Defend();
    
    public function Invisible();
}
class EvilArcher extends Archer{
    public function Shoot();
    
    public function Invisible();
}
class EvilSiegeWeapons extends SiegeWeapons{
    public function Fire();
    
    public function Invisible();
}

abstract class AbstractFactory
{
    abstract public function createInfantry();
    abstract public function createArcher();
    abstract public function createSiegeWeapons();
}

class GoodFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new GoodInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new GoodArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new GoodSiegeWeapons();
    }
}

class EvilFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new EvilInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new EvilArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new EvilSiegeWeapons();
    }
}

Код стал не таким красивым по тому, что появились методы, которые приходится копировать. В php нет множественного наследования и приходится выбирать наследоваться белому лучнику от белых или от лучников. Мы априори выбрали второй вариант и теперь думаем, как сделать наш код красивее, а его поддержку удобнее. НР предлагает использовать примеси (trait), которые появились в php 5.4. Вынесем обшие методы в примесь.

<?php

abstract class Infantry {
    //общие для всей пехоты методы
    abstract public function Attack();
    abstract public function Defend();
}
abstract class Archer {
    abstract public function Shoot();
}
abstract class SiegeWeapons {
    abstract public function Fire();
}

trait Good {
    public function Treat();
}

trait Evil {
    public function Invisible();
}

class GoodInfantry extends Infantry {
    use Good;
    
    //Спецэфическая реализация для доброго пехотинца
    public function Attack();
    public function Defend();
}
class GoodArcher extends Archer{
    use Good;
    public function Shoot();
}
class GoodSiegeWeapons extends SiegeWeapons{
    use Good;
    public function Fire();
}

class EvilInfantry extends Infantry{
    use Evil;
    
    //Спецэфическая реализация для злого пехотинца
    public function Attack();
    public function Defend();
}
class EvilArcher extends Archer{
    use Evil;
    public function Shoot();
}
class EvilSiegeWeapons extends SiegeWeapons{
    use Evil;
    public function Fire();
}

abstract class AbstractFactory
{
    abstract public function createInfantry();
    abstract public function createArcher();
    abstract public function createSiegeWeapons();
}

class GoodFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new GoodInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new GoodArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new GoodSiegeWeapons();
    }
}

class EvilFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new EvilInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new EvilArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new EvilSiegeWeapons();
    }
}

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

P.S. Мне очень хотелось бы услышать критику моего изложения общеизвестных вещей и моего варианта использования примесей от коллег. Я открыл для себя паттерны проектирования пару лет назад, но использовать их начал только сейчас.
И если будут у кого-то вопросы, то с радостью попробую ответить.

Поделиться
Запинить
3 комментария
Олег Бабаков

Не знаю насколько правильно так делать и правильно ли, но можно сделать множественное наследие таким путём:

ChildClass extends Class implements Interface1 ?

Корейша Виктор

Олег, тут план в том, что есть реализация методов и в родительском классе и в примеси. А в твоем варианте в интерфейсе можно только указать обязательство реализовать такой-то метод, но не саму реализацию.

Александр Боровиков

В php нет множественного наследования и приходится выбирать наследоваться белому лучнику от белых или от лучников

Вот это совсем не тот выбор, который на самом деле стоит. Никакого наследования от белых здесь быть не может. У юнита должен быть владелец, т. е. некоторое свойство ownerFraction -> указатель на игрока. Это дает нам сразу пачку возможностей: добавить в игру третьего игрока, временно менять владельца (помните, как в Dune2000 ордосы могли временно захватывать чужих юнитов?)

Корейша Виктор

Спасибо за комментарий.
Я подразумевал, что владелец, совсем отдельная сущьность. Как, например, в Героях Магии и Меча есть несколько замков, при этом у меня могут быть сразу несколько разных либо несколько одинаковых. Там есть свойства общие для всего замка, а есть свойства, присущие всем лучникам. Или всем, кто летает.

Александр Боровиков

Вить, но ведь даже в таком контексте ты лишил фабрику главного — ее абстракции. Теперь всякий раз, когда мы хотим создать юнит мы должны написать что-то вроде
$factory = null;
if ($fraction->getSide() == 'light') {
$factory = new GoodFactory();
} elseif ($fraction->getSide() == 'dark' {
$factory = new EvilFactory();
}
$unit = null;
if ($unitType == 'knight') {
return $factory->createKnight();
} else ...
Заметь, что если мы заменим фабрику на прямое создание юнита через new, станет только проще. Но ведь фабрика должна повысить уровень абстракции, чтобы мы больше не думали о принадлежности юнита фракции и не думали о влиянии каких-то еще вещей на его особенности. Фабрика должна упрощать.
Некоторый смысл у фабрики появляется сразу же, как только мы перестанем плодить разные фабрики и вспомним об абстракции:
$factory = new UnitFactory();
return $factory->createUnit($unitType, $fraction->getSide());
Теперь нам не надо думать ни о том, влияет ли на что-либо фракция или нет, ни о типе создаваемого юнита. Разработчик в то же время вынужден передать тип фракции и идентификатор создаваемого юнита в фабричный метод. Да и код стал короче — все детали скрыты в самом методе createUnit.

Ваш комментарий
адрес не будет опубликован

ХТМЛ не работает

Ctrl + Enter
Популярное