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

Чтобы умело управлять этой иерархией нужно добавить в класс 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). Обкатав код на практике и внеся кое-какие изменения, мы создадим конечный вариант создания скинов.
svkozlov
Извините, но практической ценности не вижу в вашей статье.