Статьи

Загрузка данных с помощью команд

Загружать внешние данные (например, SWF-файлы) во время выполнения очень часто, но только когда данные полностью загружены, мы можем читать или манипулировать их содержимым. Обычно мы должны прослушивать событие complete, отправляемое объектом Loader или URLLoader, который загружает данные для обработки завершения. Часто мы пишем код, который загружает данные в одну функцию, и пишем код, который обрабатывает завершение загрузки в другой функции, но это можно улучшить, сгруппировав весь процесс загрузки вместе.

Этот учебник демонстрирует, как создать расширение загрузки для командной структуры в моем предыдущем уроке, Thinking in Commands часть 1 из 2 , чтобы упаковать обработку завершения загрузки в одно место. Это расширение загрузки также можно комбинировать со структурой управления сценами, описанной в разделе « Мышление в командах» 2 из 2 Многие классы, используемые в этом уроке, описаны в предыдущем уроке, поэтому я настоятельно рекомендую вам прочитать предыдущие уроки перед тем, как продолжить.

Кроме того, в этом руководстве представлена ​​концепция менеджера данных, центрального «банка», в котором хранятся ссылки на объекты данных. Вы можете зарегистрировать данные в диспетчере данных с помощью уникальной строки ключей, а затем получить доступ к данным, предоставив соответствующую строку ключей. Это избавляет вас от необходимости хранить ссылки на объекты данных и некоторые проблемы с переменной областью.

Кстати, вам понадобится платформа GreenSock Tweening , чтобы завершить эти примеры.


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

Давайте посмотрим на логику наивного подхода загрузки SWF.

Загрузчик загружает SWF-файл из URL-адреса, и функция onComplete () вызывается методом dispatchEvent (), который отправляет событие complete, при этом загрузчик вызывает метод dispatchEvent () изнутри. На самом деле, он вызывается объектом LoaderInfo, который принадлежит объекту Loader , но для простоты, скажем так, метод dispatchEvent () вызывается Loader.

Затем, внутри функции onComplete (), функция doMoreStuff () вызывается после завершения обработки загрузки и, как следует из названия функции, делает больше вещей.

Логический поток высокого уровня очень линейный: сначала вызовите метод Loader.load () , затем onComplete () , а затем — doMoreStuff () . Однако, как вы заметите из диаграммы, каждый вызов функции встроен в тело функции предыдущего, что приводит к «вложенному» коду. По моему мнению, если логический поток определенной функциональности является линейным, связанный код должен быть написан линейным способом, а не вложенным. В противном случае код может запутаться, если уровень гнезда вызова слишком высок.

Это когда командный подход вступает в игру. Из приведенной ниже диаграммы видно, что код довольно линейный с использованием команд, так как все команды линейно объединяются последовательной командой. Хотя программа «переключается» на функции setProperties () , addChildLoader () и doMoreStuff () ; их вызов является линейным.


Хорошо, прежде чем мы приступим к загрузке, давайте сначала посмотрим на класс DataManager . Диспетчер данных позволяет связать ключевую строку с объектом данных, и вы можете получить ссылку на этот объект данных в любом месте вашего кода. С менеджером данных вам не нужно беспокоиться о сохранении ссылок на данные и областей переменных. Все, что вам нужно сделать, это зарегистрировать часть данных для менеджера с помощью ключевой строки.

Кодирование довольно простое, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package data {
    import flash.utils.Dictionary;
     
    public class DataManager {
         
        //a dictionary that maintains the string-data relations
        private static var _data:Dictionary = new Dictionary();
         
        //returns the data object associated with a key string
        public static function getData(key:String):* {
            return _data[key];
        }
         
        //registers a data object with a key string
        public static function registerData(key:String, data:*):void {
            _data[key] = data;
        }
         
        //unregisters a key string
        public static function unregisterData(key:String):void {
            delete _data[key];
        }
         
        //unregisters all key strings
        public static function clearData():void {
            for (var key:String in _data) {
                delete _data[key];
            }
        }
    }
}

Поэтому, когда мы хотим зарегистрировать ключевую строку «myData» в объекте данных, скажем, в спрайте, мы можем написать код следующим образом:

1
2
var sprite:Sprite = new Sprite();
DataManager.registerData(«myData», sprite);

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

1
2
var sprite:Sprite = DataManager.
container.addChild(sprite);

Теперь давайте посмотрим, как наивный подход к загрузке загружает внешнее изображение. Код загрузки находится в одной функции, а код обработки завершения — в другой. Мы собираемся загрузить три изображения и добавить их на сцену после завершения загрузки. Также мы будем следить за ходом загрузки, прослушивая события прогресса.


Откройте Flash и создайте новый документ Flash.

Нарисуйте индикатор прогресса на сцене; это для представления прогресса загрузки. Преобразуйте весь индикатор выполнения в символ и присвойте ему имя экземпляра «progressBar_mc». Внутри символа индикатора выполнения преобразуйте внутренний индикатор выполнения в другой символ и присвойте ему имя экземпляра «innerBar_mc».


Поместите три изображения в одну папку с файлом FLA с именами «image1.jpg», «image2.jpg» и «image3.jpg». Вот как выглядят три изображения.


Создайте новый файл AS для класса документа для файла FLA. Код довольно прост, и все детали объясняются в комментариях. Сначала создаются три загрузчика и начинается загрузка. При каждом событии прогресса индикатор выполнения обновляется. Когда загрузка завершится, индикатор выполнения исчезнет, ​​и три изображения исчезнут одно за другим.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package {
    import com.greensock.TweenMax;
    import flash.display.DisplayObject;
    import flash.display.Loader;
    import flash.display.MovieClip;
    import flash.events.Event;
    import flash.events.ProgressEvent;
    import flash.net.URLRequest;
     
    public class NaiveLoading extends MovieClip {
         
        private var loader1:Loader;
        private var loader2:Loader;
        private var loader3:Loader;
         
        public function NaiveLoading() {
             
            //shrink progress bar to zero scale
            progressBar_mc.innerBar_mc.scaleX = 0;
             
            //create loaders
            loader1 = new Loader();
            loader2 = new Loader();
            loader3 = new Loader();
             
            //add progress listeners
            loader1.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, onProgress);
            loader2.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, onProgress);
            loader3.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, onProgress);
             
            //add completion listeners
            loader1.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
            loader2.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
            loader3.contentLoaderInfo.addEventListener(Event.COMPLETE, onComplete);
             
            //start loading
            loader1.load(new URLRequest(«image1.jpg»));
            loader2.load(new URLRequest(«image2.jpg»));
            loader3.load(new URLRequest(«image3.jpg»));
        }
         
        private function onProgress(e:ProgressEvent):void {
             
            //calculate total bits to load
            var bytesTotal:uint = 0;
            bytesTotal += loader1.contentLoaderInfo.bytesTotal;
            bytesTotal += loader2.contentLoaderInfo.bytesTotal;
            bytesTotal += loader3.contentLoaderInfo.bytesTotal;
             
            //calculate total bits loaded
            var bytesLoaded:uint = 0;
            bytesLoaded += loader1.contentLoaderInfo.bytesLoaded;
            bytesLoaded += loader2.contentLoaderInfo.bytesLoaded;
            bytesLoaded += loader3.contentLoaderInfo.bytesLoaded;
             
            //update progress bar scale
            progressBar_mc.innerBar_mc.scaleX = bytesLoaded / bytesTotal;
        }
         
        private var _completeCount:int = 0;
        private function onComplete(e:Event):void {
            _completeCount++;
            if (_completeCount < 3) return;
             
            //remove progress listeners
            loader1.contentLoaderInfo.removeEventListener(ProgressEvent.PROGRESS, onProgress);
            loader2.contentLoaderInfo.removeEventListener(ProgressEvent.PROGRESS, onProgress);
            loader3.contentLoaderInfo.removeEventListener(ProgressEvent.PROGRESS, onProgress);
             
            //remove completion listeners
            loader1.contentLoaderInfo.removeEventListener(Event.COMPLETE, onComplete);
            loader2.contentLoaderInfo.removeEventListener(Event.COMPLETE, onComplete);
            loader3.contentLoaderInfo.removeEventListener(Event.COMPLETE, onComplete);
             
            var image1:DisplayObject = loader1.content;
            var image2:DisplayObject = loader2.content;
            var image3:DisplayObject = loader3.content;
             
            //adjust loaded image positions
            image1.x = 30, image1.y = 30;
            image2.x = 230, image2.y = 30;
            image3.x = 430, image3.y = 30;
             
            //add loaded images to display list
            addChild(image1);
            addChild(image2);
            addChild(image3);
             
            //fade out progress bar
            TweenMax.to(progressBar_mc, 0.5, {autoAlpha:0, blurFilter:{blurX:20, blurY:20}});
             
            //fade in loaded images
            TweenMax.from(image1, 0.5, {delay:0.5, alpha:0, blurFilter:{blurX:20, blurY:20}});
            TweenMax.from(image2, 0.5, {delay:0.7, alpha:0, blurFilter:{blurX:20, blurY:20}});
            TweenMax.from(image3, 0.5, {delay:0.9, alpha:0, blurFilter:{blurX:20, blurY:20}});
        }
    }
}

Нажмите CTRL + ENTER, чтобы проверить фильм. Вы увидите, что индикатор выполнения немедленно исчезает, а три изображения исчезают. Это потому, что изображения являются локальными файлами, то есть они могут быть загружены практически сразу. Для имитации скорости онлайн-загрузки сначала выберите View> Download Settings> DSL в качестве моделируемой скорости загрузки, а затем снова нажмите CTRL + ENTER, не закрывая окно тестирования, чтобы начать имитацию онлайн-загрузки. На этот раз вы увидите, как прогресс будет постепенно расширяться, прежде чем он исчезнет.

Хорошо, пришло время загружать изображения с помощью командной структуры.


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

Здесь мы собираемся создать две команды для регистрации и отмены регистрации данных для класса менеджера данных. Команда RegisterData регистрирует данные в диспетчере, а команда UnregisterData отменяет регистрацию данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package commands.data {
    import commands.Command;
    import data.DataManager;
     
    //this command registers data to the data manager
    public class RegisterData extends Command {
         
        public var key:String;
        public var data:*;
         
        public function RegisterData(key:String, data:*) {
            this.key = key;
            this.data = data;
        }
         
        override protected function execute():void {
            DataManager.registerData(key, data);
            complete();
        }
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package commands.data {
    import commands.Command;
    import data.DataManager;
     
    //this command unregisters data from the data manager
    public class UnregisterData extends Command {
         
        public var key:String;
         
        public function UnregisterData(key:String) {
            this.key = key;
        }
         
        override protected function execute():void {
            DataManager.unregisterData(key);
            complete();
        }
    }
}

Эта команда инкапсулирует метод load () экземпляра Loader . Вы можете предоставить команду onProgress, которая выполняется при каждом событии progress, и onComplete, выполняемая после завершения загрузки. Обратите внимание, что метод complete () вызывается после завершения загрузки. Эта строка кода чрезвычайно важна. Если вы не вызовете метод, команда никогда не будет считаться завершенной, что приведет к заклиниванию всего приложения в худшем случае.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package commands.loading {
    import commands.Command;
    import flash.display.Loader;
    import flash.events.Event;
    import flash.events.ProgressEvent;
    import flash.net.URLRequest;
    import flash.system.LoaderContext;
     
    public class LoaderLoad extends Command {
         
        public var loader:Loader;
        public var url:URLRequest;
        public var context:LoaderContext;
         
        public var onProgress:Command;
        public var onComplete:Command;
         
        public function LoaderLoad(loader:Loader, url:URLRequest, context:LoaderContext = null, onProgress:Command = null, onComplete:Command = null) {
            this.loader = loader;
            this.url = url;
            this.context = context;
            this.onProgress = onProgress;
            this.onComplete = onComplete;
        }
         
        override protected function execute():void {
             
            //add listeners
            loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, progressListener);
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE, completeListener);
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadingComplete);
             
            //start loading
            loader.load(url, context);
        }
         
        private function loadingComplete(e:Event):void {
             
            //remove listeners
            loader.contentLoaderInfo.removeEventListener(ProgressEvent.PROGRESS, progressListener);
            loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, completeListener);
            loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, loadingComplete);
             
            complete();
        }
         
        private function progressListener(e:ProgressEvent):void {
             
            //execute the onProgress command
            if (onProgress) onProgress.start();
        }
         
        private function completeListener(e:Event):void {
             
            //execute the onComplete command
            if (onComplete) onComplete.start();
        }
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package commands.utils {
    import commands.Command;
     
    //this command invokes a function
    public class InvokeFunction extends Command{
         
        public var func:Function;
        public var args:Array;
         
        public function InvokeFunction(func:Function, args:Array = null) {
            this.func = func;
            this.args = args;
        }
         
        override protected function execute():void {
            func.apply(null, args);
            complete();
        }
    }
}

Вот и все. Время для примера.


Скопируйте FLA-файл из предыдущего примера в новую папку и скопируйте вместе с ним файлы изображений.


Создайте новый AS-файл для класса документа скопированного FLA-файла с именем «LoadingDataWithCommands». Не забудьте изменить имя класса документа в FLA-файле на новое.

Код для класса документа довольно чистый. Он просто устанавливает текущую сцену в LoadingScene с менеджером сцены. Мы используем каркас сцены, представленный в моем предыдущем уроке ( часть 2 ). Вы можете проверить это, если вы забыли, как его использовать.

01
02
03
04
05
06
07
08
09
10
11
12
13
package {
    import flash.display.MovieClip;
    import scenes.SceneManager;
     
    public class LoadingDataWithCommands extends MovieClip {
         
        public function LoadingDataWithCommands() {
             
            var sceneManager:SceneManager = new SceneManager();
            sceneManager.setScene(new LoadingScene(this));
        }
    }
}

Всего есть две сцены. LoadingScene загружает изображения и обновляет индикатор выполнения. После завершения загрузки сцена переходит к MainScene , который исчезает в загруженных изображениях.


Расширьте класс Scene, чтобы создать новый класс с именем LoadingScene . Свойство контейнера содержит ссылку на главный спрайт. Это позволяет нам получить доступ к индикатору прогресса позже.

01
02
03
04
05
06
07
08
09
10
11
12
package {
    import scenes.Scene;
     
    public class LoadingScene extends Scene {
         
        private var container:LoadingDataWithCommands;
         
        public function LoadingScene(container:LoadingDataWithCommands) {
            this.container = container;
        }
    }
}

Теперь создайте команду intro для сцены загрузки. Вступление создаст три загрузчика и начнет процесс загрузки. Это делается путем переопределения метода createIntroCommand () . Следующий код входит в тело класса, так же, как конструктор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//the intro command begins the loading of the three images
override public function createIntroCommand():Command {
    var loader1:Loader = new Loader();
    var loader2:Loader = new Loader();
    var loader3:Loader = new Loader();
     
    var command:Command =
        new ParallelCommand(0,
             
            //shrink the progress bar to zero scale
            new SetProperties(container.progressBar_mc.innerBar_mc, {scaleX:0}),
             
            //loading-related commands executed in series
            new SerialCommand(0,
                 
                //registers the three loaders to the data manager
                new ParallelCommand(0,
                    new RegisterData(«loader1», loader1),
                    new RegisterData(«loader2», loader2),
                    new RegisterData(«loader3», loader3)
                ),
                 
                //start three loading commands in parallel
                new ParallelCommand(0,
                    new LoaderLoad(
                        loader1, new URLRequest(«image1.jpg»), null,
                        new InvokeFunction(onProgress) //onProgress command
                    ),
                    new LoaderLoad(
                        loader2, new URLRequest(«image2.jpg»), null,
                        new InvokeFunction(onProgress) //onProgress command
                    ),
                    new LoaderLoad(
                        loader3, new URLRequest(«image3.jpg»), null,
                        new InvokeFunction(onProgress) //onProgress command
                    )
                )
            )
        );
     
    return command;
}

Затем переопределите метод onSceneSet () . Этот метод вызывается, когда вводная команда завершена, указывая, что загрузка завершена. В рамках этого метода мы говорим диспетчеру сцены перейти на главную сцену. Перед переходом сцены команда outro выполняется первой.

1
2
3
override public function onSceneSet():void {
    sceneManager.setScene(new MainScene(container));
}

А затем переопределите команду createOutroCommand . Эта команда должна исчезнуть с индикатора выполнения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
//the outro command fades out the progress bar
override public function createOutroCommand():Command {
    var command:Command =
        new SerialCommand(0,
             
            //fade out progress bar
            new TweenMaxTo(container.progressBar_mc, 0.5, {autoAlpha:0, blurFilter:{blurX:20, blurY:20}}),
             
            //remove progress bar from display list
            new RemoveChild(container, container.progressBar_mc)
        );
     
    return command;
}

Наконец, создайте метод onProgress, вызываемый командами InvokeFunction .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
private function onProgress():void {
     
    //retrieve loader references from the data manager
    var loader1:Loader = DataManager.getData(«loader1») as Loader;
    var loader2:Loader = DataManager.getData(«loader2») as Loader;
    var loader3:Loader = DataManager.getData(«loader3») as Loader;
     
    //calculate total bits to load
    var bytesTotal:uint = 0;
    bytesTotal += loader1.contentLoaderInfo.bytesTotal;
    bytesTotal += loader2.contentLoaderInfo.bytesTotal;
    bytesTotal += loader3.contentLoaderInfo.bytesTotal;
     
    //calculate total bits loaded
    var bytesLoaded:uint = 0;
    bytesLoaded += loader1.contentLoaderInfo.bytesLoaded;
    bytesLoaded += loader2.contentLoaderInfo.bytesLoaded;
    bytesLoaded += loader3.contentLoaderInfo.bytesLoaded;
     
    //update progress bar scale
    container.progressBar_mc.innerBar_mc.scaleX = bytesLoaded / bytesTotal;
}

Теперь создайте новый класс для главной сцены, расширяя класс Scene .

01
02
03
04
05
06
07
08
09
10
11
12
package {
    import scenes.Scene;
     
    public class MainScene extends Scene {
         
        private var container:LoadingDataWithCommands;
         
        public function MainScene(container:LoadingDataWithCommands) {
            this.container = container;
        }
    }
}

Переопределите метод createIntroCommand () . Этот метод будет добавлять загрузчики в список отображения и постепенно уменьшать их. Кроме того, строки ключей данных не регистрируются в диспетчере данных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
override public function createIntroCommand():Command {
     
    //retrieve loader references from the data manager
    var loader1:Loader = DataManager.getData(«loader1») as Loader;
    var loader2:Loader = DataManager.getData(«loader2») as Loader;
    var loader3:Loader = DataManager.getData(«loader3») as Loader;
     
    var command:Command =
        new ParallelCommand(0,
             
            //loaded-image-handling commands
            new SerialCommand(0,
                 
                //adjust loaded image positions
                new ParallelCommand(0,
                    new SetProperties(loader1, {x:30, y:30}),
                    new SetProperties(loader2, {x:230, y:30}),
                    new SetProperties(loader3, {x:430, y:30})
                ),
                 
                //add loaded images to display list
                new ParallelCommand(0,
                    new AddChild(container, loader1),
                    new AddChild(container, loader2),
                    new AddChild(container, loader3)
                ),
                 
                //fade in loaded images
                new ParallelCommand(0,
                    new TweenMaxFrom(loader1, 0.5, {blurFilter:{blurX:20, blurY:20}}),
                    new TweenMaxTo(loader1, 0.5, {autoAlpha:1}),
                    new TweenMaxFrom(loader2, 0.5, {delay:0.2, alpha:0, blurFilter:{blurX:20, blurY:20}}),
                    new TweenMaxTo(loader2, 0.5, {delay:0.2, autoAlpha:1}),
                    new TweenMaxFrom(loader3, 0.5, {delay:0.4, alpha:0, blurFilter:{blurX:20, blurY:20}}),
                    new TweenMaxTo(loader3, 0.5, {delay:0.4, autoAlpha:1})
                )
            ),
             
            //unregsiter data from the data manager
            new ParallelCommand(0,
                new UnregisterData(«loader1»),
                new UnregisterData(«loader2»),
                new UnregisterData(«loader3»)
            )
        );
         
    return command;
}

Хорошо. Были сделаны! Протестируйте фильм и смоделируйте онлайн-загрузку. Вы увидите тот же результат, что и в предыдущем примере, но на этот раз все это делается с помощью командной структуры и инфраструктуры сцены.


В этом уроке я показал вам, как загружать внешние образы с помощью командной структуры. Команда LoaderLoad также может использоваться для загрузки внешних SWF-файлов. Более того, вы можете создавать свои собственные команды для загрузки внешних данных, отличных от изображений и SWF-файлов, путем инкапсуляции класса URLLoader в ваши команды.

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

В первом примере весь код сжимается в один класс, что усложняет дальнейшее обслуживание, если объем кода станет чрезвычайно большим. Во втором примере, с другой стороны, логически независимый код разделяется на разные сцены, что облегчает последующую модификацию. Кроме того, благодаря интеграции с каркасом команд и каркасом сцены мы создали пространство для будущего расширения, где мы можем добавлять больше сцен и команд ввода / вывода, не нарушая нерелевантный код.

Это конец этого урока. Надеюсь, вам понравилось. Спасибо за прочтение!