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

Простейшая иерархия компонентов на примере Dress-Up-игры
Простейшая иерархия компонентов на примере Dress-Up-игры

Чтобы умело управлять этой иерархией нужно добавить в класс Component несколько новых свойств и методов.

Свойство parent

Ранее мы уже обнаружили полезность хранения в компоненте всех его дочерних элементов (children). Это дает нам возможность, например, автоматически устанавливать и очищать в них скины. (Достаточно лишь задать свойство skinPath.) Но имея одно только свойство children, мы можем двигаться лишь вниз по иерархии. Чтобы иметь возможность перемещаться также и в обратном направлении, нам нужно сохранять ссылку и на непосредственного своего родителя. У родителя будет ссылка на своего родителя и так далее. Так, по цепочке мы сможем добраться до корневого элемента всей структуры.

class Component
{
    public var parent(default, null):Component;
    public var children(default, null):Array<Component> = [];
    // To prevent changing children directly
    //private var _children:Array<Component> = [];
    //public var children(get, null):Array<Component>;
    //public function get_children():Array<Component>
    //{
    //    return _children.copy();
    //}
    //...
    public function addChild(child:Component):Component
    {
        if (child.parent == this || child == this)
        {
            return null;
        }
        // Remove from previous
        if (child.parent != null)
        {
            child.parent.removeChild(child);
        }
        // Add
        child.parent = this;
        children.push(child);
        //_children.push(child); // Use if children getter returns copy
        return child;
    }
    public function removeChild(child:Component):Component
    {
        if (child.parent != this || child == this)
        {
            return null;
        }
        child.parent = null;
        children.remove(child);
        //_children.remove(child); // Use if children getter returns copy
        return child;
    }
}

Методы addChild и removeChild

Так как добавление и удаление дочерних компонентов выполняется теперь не в одну (children.push(child)), а в несколько операций, то создадим специальные методы addChild() и removeChild(). Тогда добавление и удаление компонентов будет по-прежнему производится всего за один шаг.

Сюда же можно добавить кое-какие проверки, чтобы предотвратить, насколько это возможно, разные случаи неправильного использования класса. Например, если добавляется компонент, который уже был ранее добавлен в другой, то его нужно сначала удалить из предыдущего. В addChild() для этого есть проверка if (child.parent != null) и вызов child.parent.removeChild(child);. Благодаря этому у нас не получится так, что какой-то компонент одновременно будет находится в нескольких родителях.

Также, чтобы избежать возможности изменять children в обход методов addChild() и removeChild(), и тем самым нарушать логику работы компонентов, можно children сделать геттером, в котором будет возвращаться не сам список, а его копия: children.copy(). Изменения копии никак не повлияют на оригинал и, соответственно, не затронут внутреннего состояния компонента. Однако, пока что мы не станем этого делать и оставим так, как есть. Во-первых, скорость выполнения нам важнее, а во-вторых, допустим маловероятным, что кто-то будет делать такую глупость, как ручное изменение массива children.

Итак, с этого момента мы должны все компоненты при создании добавлять в другой компонент. Желательно всегда в тот, в котором он и создавался:

class Dresser extends Component
{
    //...
    public function new(?skin:DisplayObject, ?assetName:String)
    {
        closeButton = new Button();
        closeButton.skinPath = "closeButton";
        closeButton.clickHandler = closeButton_clickHandler;
        addChild(closeButton);
        super(skin, assetName);
    }
    //...
}

Создание компонентов по скину

Количество кнопок закрытия (closeButton) нам заранее известно — 1, поэтому мы заранее можем создать компонент для него еще в конструкторе. Но количество одежек в Dress-Up-играх может варьироваться от игры к игре. А значит, мы вынуждены создавать компоненты, исходя из текущего скина. Следовательно, этот код должен располагаться в assignSkin(). Так, вложенные компоненты могут создаваться в двух местах: если они известны заранее, то в конструкторе, а если зависят от скина — то в assignSkin().

class Dresser extends Component
{
    private var drags:Array<Drag>;
    //...
    override private function assignSkin():Void
    {
        super.assignSkin();

        items = cast resolveSkinPathPrefix(itemPathPrefix);
        //...
        drags = [];
        for (item in items)
        {
            var drag = new Drag();
            drag.skin = item;
            addChild(drag);
            drags.push(drag);
        }
    }
    override private function unassignSkin():Void
    {
        for (drag in drags)
        {
            drag.skin = null;
            removeChild(drag);
        }
        //...
        items = null;

        super.unassignSkin();
    }
    //...
}

Метод dispose

Тут можно внести еще пару усовершенствований. Во-первых, чтобы уничтожить компонент (в unassignSkin()) нам приходится выполнять два действия: очистка скина и удаление компонента из родителя. Одно действие уничтожения разбито на два шага, а это значит, что время от времени мы будем забывать прописать один из них. Из-за этого на ровном месте будут возникать обидные баги по невнимательности, которые не только тормозят разработку, но еще дико раздражают и выбивают из рабочего настроения.

Поэтому всегда пользуемся простым правилом: одно логическое действие — одна физическая функция. Устойчивое сочетание вызова removeChild() и skin = null фактически предопределяет появление нового метода — dispose():

class Component
{
    //...
    public function dispose():Void
    {
        if (parent != null)
        {
            parent.removeChild(this);
        }
        skin = null;
        //for (child in children.copy())
        //{
        //    child.dispose();
        //}
    }
    //...
}
class Dresser extends Component
{
    //...
    override private function unassignSkin():Void
    {
        for (drag in drags)
        {
            drag.dispose();
        }
        drags = null;
        //...

        super.unassignSkin();
    }
    //...
}

В dispose() можно добавить и уничтожение всех дочерних компонентов (см. закомментированный код), но тогда его нельзя будет использовать повторно, ведь чтобы создать их заново, нужно вызвать конструктор, а это невозможно. Однако, когда мы вынесем создание дочерних компонентов из конструктора в отдельный метод инициализации (init()), мы этот код раскомментируем.

Автоматическое удаление компонентов

Во-вторых, хорошо бы, чтобы компоненты, созданные в assignSkin() автоматически удалялись в unassignSkin(). Тогда и список drags будет не нужен. Для этого можно перед вызовом assignSkin() устанавливать флаг isAssigningSkin в true, а после окончания вызова снова сбрасывать. Так мы наверняка можем знать, какие вызовы addChild() были произведены в конструкторе (isAssigningSkin==false), а какие в assignSkin() (isAssigningSkin==true). Все компоненты добавленные из assignSkin() помещаются дополнительно в массив temporaryChildren, все элементы которого будут удалены при вызове unassignSkin().

class Component
{
    private var isAssigningSkin = false;
    private var temporaryChildren:Array<Component> = [];
    //...
    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)
        {
            isAssigningSkin = true;
            assignSkin();
            isAssigningSkin = false;
        }
        return value;
    }
    public function addChild(child:Component):Component
    {
        //...
        if (isAssigningSkin)
        {
            temporaryChildren.push(child);
        }
        return child;
    }
    //...
    private function unassignSkin():Void
    {
        // Clear up all children
        for (child in children.copy())
        {
            child.skin = null;
        }
        // Remove children added inside assignSkin()
        for (child in temporaryChildren.copy())
        {
            child.dispose();
        }
        temporaryChildren = [];
    }
}

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

Налицо та же проблема. Одно логическое действие — добавление дочерних компонентов — требует выполнения двух физических операций: написание кода для создания и уничтожения компонентов. Сделав выполнение второй операции автоматическим, мы тем самым вернулись к правилу: одно действие — один вызов функции. Логика должна однозначно соответствовать коду — никакой раздвоенности.

В следующей части мы перейдем к практическому применению вложенных компонентов — к экранам (screens). Обкатав код на практике и внеся кое-какие изменения, мы создадим конечный вариант создания скинов.

Исходники

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

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


  1. svkozlov
    08.07.2022 13:47

    Извините, но практической ценности не вижу в вашей статье.