Пока что наше приложение состоит только из одного класса Dresser (не считая чисто формального класса Main). И уже одного этого оказалось достаточно для целого игрового жанра (до какой степени все же игроки бывают неприхотливы). Однако, для приличной игры этого маловато. Как минимум нужен еще экран меню, в котором мы могли бы выбирать игру.

Допустим, у нас есть несколько скинов для одевалки, и мы хотим для всех них использовать один экземпляр класса логики Dresser. При смене мувиклипа все ссылки на старый должны удаляться, а на новый — добавляться. Для этого код парсинга графики вынесем из конструктора в сеттер:

class Dresser
{
    //...
    public var mc(default, set):MovieClip;
    public function set_mc(value:MovieClip):MovieClip
    {
        if (mc != value)
        {
            // Unassign previous mc
            if (mc != null)
            {
                for (prevButton in prevButtons)
                {
                    prevButton.removeEventListener(MouseEvent.CLICK, prevButton_clickHandler);
                }
                for (nextButton in nextButtons)
                {
                    nextButton.removeEventListener(MouseEvent.CLICK, nextButton_clickHandler);
                }
                items = null;
                prevButtons = null;
                nextButtons = null;
             }

            mc = value;

            // Assign new mc
            if (mc != null)
            {
                items = cast resolveNamePrefix(itemNamePrefix);
                prevButtons = cast resolveNamePrefix(prevButtonNamePrefix);
                nextButtons = cast resolveNamePrefix(nextButtonNamePrefix);
                for (item in items)
                {
                    item.stop();
                }
                for (prevButton in prevButtons)
                {
                    prevButton.buttonMode = true;
                    prevButton.addEventListener(MouseEvent.CLICK, prevButton_clickHandler);
                }
                for (nextButton in nextButtons)
                {
                    nextButton.buttonMode = true;
                    nextButton.addEventListener(MouseEvent.CLICK, nextButton_clickHandler);
                }
            }
        }
        return value;
    }
    public function new(?mc:MovieClip)
    {
        super();
        this.mc = mc;
    }
    //...
}

Редкая игра может состоять только из одного класса. Мы же тут изобретаем общий подход для всех игр. Поэтому придумаем для примера функционал хотя бы еще на один класс. Два класса — это уже хоть какой-то материал для обобщения. Хм..., допустим, мы захотим двигать мышкой (dragging) одежду (создадим новый жанр — раздевалка). Не будем лепить новый код в классе Dresser, а сразу создадим отдельный класс Drag. Сделаем его по образцу Dresser, то есть с сеттером mc:

class Drag
{
    private var stage:Stage;
    private var mouseDownX:Float;
    private var mouseDownY:Float;
    public var isDragging(default, null):Bool;

    public var mc(default, set):MovieClip;
    public function set_mc(value:MovieClip):MovieClip
    {
        if (mc != value)
        {
            // Unassign previous mc
            if (mc != null)
            {
                mc.removeEventListener(MouseEvent.MOUSE_DOWN, mc_mouseDownHandler);
                if (stage != null)
                {
                    stage.removeEventListener(MouseEvent.MOUSE_UP, stage_mouseUpHandler);
                    stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);
                }
            }

            mc = value;

            // Assign new mc
            if (mc != null)
            {
                stage = mc.stage;
                mc.addEventListener(MouseEvent.MOUSE_DOWN, skin_mouseDownHandler);
                if (stage != null)
                {
                    stage.addEventListener(MouseEvent.MOUSE_UP, stage_mouseUpHandler);
                }
            }
        }
        return value;
    }

    public function new(?mc:MovieClip)
    {
        this.mc = mc;
    }
    private function skin_mouseDownHandler(event:MouseEvent):Void
    {
        if (stage != null)
        {
            isDragging = true;
            mouseDownX = mc.parent.mouseX - mc.x;
            mouseDownY = mc.parent.mouseY - mc.y;
            stage.addEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);
        }
    }
    private function stage_mouseUpHandler(event:MouseEvent):Void
    {
        if (stage != null)
        {
            isDragging = false;
            stage.removeEventListener(MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler);
        }
    }
    private function stage_mouseMoveHandler(event:MouseEvent):Void
    {
        mc.x = mc.parent.mouseX - mouseDownX;
        mc.y = mc.parent.mouseY - mouseDownY;
    }
}
class Dresser
{
    //...
    private var drags:Array<Drag>;

    public var mc(default, set):MovieClip;
    public function set_mc(value:MovieClip):MovieClip
    {
        if (mc != value)
        {
            // Unassign previous mc
            if (mc != null)
            {
                for (drag in drags)
                {
                    drag.mc = null;
                }
                drags = null;
                //...
            }

            mc = value;

            // Assign new mc
            if (mc != null)
            {
                //...
                drags = [for (item in items) new Drag(item)];
            }
        }
        return value;
    }
    //...
}

Теперь, если нам понадобится добавить перетаскивание любого объекта на экране, нам достаточно будет написать всего лишь одну строку: new Drag(some_mc);. Вот мы уже и подошли к повторному использованию кода!

Не нужно быть гением, чтобы увидеть, что оба наших классов имеют нечто общее. У обоих есть очень похожий сеттер — mc. В разных классах они делают разное, но шаблон явно прослеживается. Также оба класса являются оберткой вокруг графического объекта. Оберткой, которая добавляет логику к графике. Причем не только добавляет (dresser.mc = some_mc;), но и может очищать от нее (dresser.mc = null;).

Если нам понадобится реализовать кнопку, чекбокс, радио-кнопку, текстовую метку, список, скроллер, да что угодно работающее с графикой — удобнее всего их будет сделать точно так же. Вообще, все приложение целиком можно собрать из таких вот объектов-компонентов. Поэтому, почему бы нам не создать для всех них один общий базовый класс. Назовем его компонент (Component — от лат. componens «составляющий», род. пад. componentis). В программировании компонентами обычно называются классы, предназначенные для повторного использования и развёртывания. Именно так мы их и намереваемся использовать. Все сходится — компоненты.

Итак, произведем Extract class refactoring:

class Component
{
    public var mc(default, set):MovieClip;
    // No need to override. Pattern: Template Method
    public function set_mc(value:MovieClip):MovieClip
    {
        if (mc != value)
        {
            // Unassign previous mc
            if (mc != null)
            {
                unassignMC();
            }

            mc = value;

            // Assign new mc
            if (mc != null)
            {
                assignMC();
            }
        }
        return value;
    }

    public function new(?mc:MovieClip)
    {
        this.mc = mc;
    }
    // Override
    private function assignMC():Void
    {
    }
    // Override
    private function unassignMC():Void
    {
    }
}
class Dresser extends Component
{
    private var drags:Array<Drag>;
    //...
    override private function assignMC():Void
    {
        super.assignMC();

        items = cast resolveNamePrefix(itemNamePrefix);
        prevButtons = cast resolveNamePrefix(prevButtonNamePrefix);
        nextButtons = cast resolveNamePrefix(nextButtonNamePrefix);
        for (item in items)
        {
            item.stop();
        }
        for (prevButton in prevButtons)
        {
            prevButton.buttonMode = true;
            prevButton.addEventListener(MouseEvent.CLICK, prevButton_clickHandler);
        }
        for (nextButton in nextButtons)
        {
            nextButton.buttonMode = true;
            nextButton.addEventListener(MouseEvent.CLICK, nextButton_clickHandler);
        }
        drags = [for (item in items) new Drag(item)];
    }
    override private function unassignMC():Void
    {
        for (prevButton in prevButtons)
        {
            prevButton.removeEventListener(MouseEvent.CLICK, prevButton_clickHandler);
        }
        for (nextButton in nextButtons)
        {
            nextButton.removeEventListener(MouseEvent.CLICK, nextButton_clickHandler);
        }
        for (drag in drags)
        {
            drag.mc = null;
        }
        items = null;
        prevButtons = null;
        nextButtons = null;
        drags = null;

        super.unassignMC();
    }
    //...
}

Сеттер мы сделали по паттерну шаблонный метод (Template method). Тогда нам не нужно при переопределении set_mc() делать каждый раз проверки if (mc != null). Во-первых, это скучная рутина — печатать одно и то же. Во-вторых, об этом нужно помнить. А идеальный метод не должен заставлять нас что-либо помнить, когда мы его переопределяем. Мы всегда должны в подклассе иметь возможность писать что-угодно, в том числе и — ничего. Всё, о чем не надо забывать, обязательно однажды будет забыто. Именно поэтому и были добавлены методы assignMC() и unassignMC(). Теперь переопределяться должны они, а не set_mc().

Однако, тут обнаруживается один недостаток. Не все графические объекты на экране могут быть мувиклипами (MovieClip). Бывают еще SimpleButton и Shape. MovieClip и SimpleButton имеют общим родительским классом InteractiveObject, а с Shape — аж только DisplayObject (поэтому Shape и не воспринимает события мышки). Значит, чтобы сделать класс Component по-настоящему универсальным, будем использовать в качестве базового класса для графики не MovieClip, а DisplayObject, а само свойство mc переименуем в более абстрактный skin:

class Component
{
    public var skin(default, set):DisplayObject;
    // No need to override. Pattern: Template Method
    public function set_skin(value:DisplayObject):DisplayObject
    {
        if (skin == value)
        {
            return value;
        }
        // Unassign previous skin
        if (skin != null)
        {
            unassignSkin();
        }

        skin = value;
        interactiveObject = Std.downcast(value, InteractiveObject);
        simpleButton = Std.downcast(value, SimpleButton);
        container = Std.downcast(value, DisplayObjectContainer);
        sprite = Std.downcast(value, Sprite);
        mc = Std.downcast(value, MovieClip);

        // Assign new skin
        if (skin != null)
        {
            assignSkin();
        }
        return value;
    }
    public var interactiveObject(default, null):InteractiveObject;
    public var simpleButton(default, null):SimpleButton;
    public var container(default, null):DisplayObjectContainer;
    public var sprite(default, null):Sprite;
    public var mc(default, null):MovieClip;

    public function new(?skin:MovieClip)
    {
        this.skin = skin;
    }
    // Override
    private function assignSkin():Void
    {
    }
    // Override
    private function unassignSkin():Void
    {
    }
}

Чтобы не заниматься дополнительным приведением типов, если нам вдруг понадобятся свойства мувиклипа или кнопки, мы сделали все возможные приведения типов заранее и сохранили ссылки на них в свойствах: interactiveObject, simpleButton, container, sprite, mc.

Вот мы и получили компонент в самом чистом виде — как простая обертка для графики. Позже мы добавим сюда еще кое-какие свойства и методы, но это будет только расширение и углубление его основной задачи — быть оберткой. (Можно, конечно, обойтись и без них, но с ними удобнее.) Сущность класса выражена полностью.

Концепция компонентов позволяют нам писать любую логику отображения: от простых UI-элементов до сложных панелей, состоящих из других компонентов. Так, можно, как из кубиков, собрать что-угодно вплоть до целого приложения. Со временем у нас накопятся целые библиотеки компонентов, так что мы сможем собирать новые игры из готовых частей, не создавая ни одного нового класса. Представьте себе приложение состоящее из одного Main-класса, конфигурационного JSON- или YAML-файла и ассетов... Вот житуха! Но об этом пока еще рано говорить.

Исходники

< Назад | Начало | Вперед >

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