Пока что наше приложение состоит только из одного класса 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-файла и ассетов… Вот житуха! Но об этом пока еще рано говорить.
ссылка на оригинал статьи https://habr.com/ru/post/675600/
Добавить комментарий