📜 ⬆️ ⬇️

Classes and factories. How to disassemble and assemble an object with inheritance on prototypes

Hi, Habra!
When developing games in JavaScript, it is often necessary to create a lot of objects. I told you about how to do this correctly and not drown in the code, about a month ago on Frontend Dev Conf in Minsk. Perhaps the report will be of interest to those who have not been to the conference and have encountered the problem of creating a multitude of objects, or are developing HTML5 games.



Under the cut text with pictures.

')

Classes and factories


As mentioned above, when creating games, it becomes necessary to create a variety of different objects. To achieve this goal, inheritance is used on prototypes. What does the classic class look like with this approach:

function animal() { ... } animal.prototype.left = function() { ... } animal.prototype.right = function() { ... } 

You can find this and similar examples in a variety of books describing OOP in JavaScript. The same principle is copied by most MVC frameworks. But what to do when we need to get a lot of objects of different classes? With the approach described above, we have to create many classes. As a result, we may have difficulties with the subsequent support of the project, finding bugs and understanding how it all works. Endless chains of dependent inheritance of some objects from others are created. The way out of this situation is the use of the factory.

In C ++, an object factory can only create objects of a specific type that use a single interface. The main advantages of this pattern in C ++ are the simplified creation of objects of various classes using a single interface. In JavaScript, we can move away from this restriction and in one place get objects with a completely different set of properties and methods.

To build a transparent structure of our application, we will compile a list of all properties and a list of all prototypes, divided into groups. For example:

 var properties = { speed: { x: 0, y: 0 limit: { x: 10, y: 10 } }, acceleration: { x: 0, y: 0 }, live: { health: 100, killing: 0, level: 0 } }; 

 var prototypes = { left: function() { ... }, right: function() { ... } }; 

To order an object in a factory, we just need to specify a list of properties and a list of prototypes that the object should inherit. For example:

 var objectA = factory([ "physics", "live" ], [ "left" ]); var objectB = factory([ "live", "acceleration" ], [ "right" ]); 

What happens at the factory? Something like this:

 var object = {}; //   for(var name in properties) { object[name] = properties[name]; } //   for(var method in prototypes) { object.prototype[method] = prototype[method]; } 




In fact, the real mechanism will be a bit more complicated. For example, it will be necessary to sort the properties for the presence of nested objects, assign some standard values, check the validity of the input data, learn how to dynamically create, save and retrieve the requested classes, but the main point will not change.

It remains to solve the last problem - beautifully describe the properties of all classes and objects in a simple form. This can be done using all the same lists. Consider this on the example of a block class:

 var classList = { ... block: { _properties: [ //  ,    "skin", "dimensions", "physics", "coordinates", "type" ], _prototypes: [], //  ,    _common: { //     _properties: { move: false //  move     false } }, floor: { //       roughness: 0.37 //     }, gold: { roughness: 0.34 }, sand: { roughness: 0.44 //   ,    , }, //    water: { roughness: 0.25 } } }; 

Having a similar list of classes and objects, we can also automatically create an API. For example:

 block.gold(); //    block.sand(); //    block.water(); //    

Thus, we got a factory, the code size of which does not change regardless of the number and variety of objects in the game and three lists of the JSON type:



Lists are convenient because they are easy to change, just cover the documentation, as well as increasing the number of objects we do not increase the amount of code (since lists are configs). It turns out that by writing once the factory code, we can create hundreds of various objects, with a transparent structure and not tangling at all.

Example property documentation file:

 var properties = { physics: { speed: { x: "   X", y: "   Y", limit: { x: "    X,     .", y: "    Y,     ." } }, acceleration: { x: "    X.   ...", y: "    Y.   ..." }, }, live: { health: "  .", killing: "   .", level: "  ." } }; 

In addition, using lists to easily create new kinds of objects. For this, just need to call the factory with different parameters. For example, we have a car, and we need to make it a tank. To do this, we can add to the description of the machine a pack of prototypes responsible for the weapon. Thus, we get some kind of machine with a weapon, and in fact - a tank.



How to make sure our factory is working properly?

To do this, we need to create several objects, run the debager and look at the addresses of the properties and methods of the objects in memory.



Interface Standardization



Standardization of object interfaces helps to write common modules for working with them. The essence of the method is to bring the API of all objects to some general standard form, despite their differences between themselves. Consider the method of the following example:

Problem:

There is a game character and the world around him. When a player presses the “use” button, at least two situations are possible:



Decision:



It is clear that the use () method of weapons and vehicles will differ, but how to implement this at the time of the inheritance of prototypes in a factory?

The answer is very simple. It is necessary to make a replacement list and check whether it is in the list before assigning a method name. For example:

 //       var replaceList = { ... weapon: "use", transport: "use" } //  ,      for(var method in prototypes) { var name = replaceList[method] || method; object.prototype[name] = prototype[method]; } 




Thus, despite the fact that the weapon inherits the prototype “weapon”, and transport - “transport”, in the prototypes of the objects there will be only one use method, for which each object will have its own functions. Such is polymorphism.

How to save and load objects



Now that we have a big world with a bunch of different objects (imagine Minecraft), it is time to solve the following problem - the implementation of the save / load functions.

At first glance, everything is simple. Since we have a lot of objects, we can convert them to a string (JSON.stringify), but there are two problems here:



Let us analyze the solution of each problem separately.

Ban on storage of objects.



The essence of the method lies in the fact that after creating an object in a factory, it will be assigned a unique ID, and it should go into a single register of objects. The task of a single registry is to store all objects and, upon request, issue them by ID. At the same time, any module and subsystem is prohibited to use the object without unnecessary need and it is strictly forbidden to keep reference to it The whole system operates exclusively on ID, and only in special cases requests the object itself. For example:

The man gets on the bus. The bus has an array of passengers. According to the rules, he must add the ID of the entered person to the passenger array. If the human object itself is added to the array, we will get an excessive nesting. This will create a lot of problems for us, ranging from mythical memory leaks, ending the need to iterate over objects in objects when loading. In addition, if the bus passenger dies for any reason at the time of the trip, we will have to remove the corpse. If we store only the passenger ID, when retrieving objects, we will see that the passenger with this ID no longer exists and proceed to the next iteration.

Or another example:

Mario takes the coin. In fact, Mario should put in his backpack only the ID of the coin. If he uses it somewhere, the system will request the coin object itself from the registry, but it will have to remove the link to it immediately after completing its actions.

Also, this system helps to avoid bugs when rendering. For example, when we for some reason delete the whole world, and the camera requests an object. If there is a registry, it will understand that the object no longer exists and will delete all its settings associated with this ID.

Restoring prototype chains.



When translating an object into a string, we lose the prototypes that the object possessed. Therefore, before converting, you need to add the prototype_list property to the object and list in it all the prototypes that the object possessed (besides, it must be done at the factory, since after all the objects will have a typical interface that does not report anything about their true stuffing) .

Further, during the loading operation, we will again send the objects to the factory, but only already to the restoration workshop. There objects will pass only the second part of the work - inheriting prototypes from the list.

Source: https://habr.com/ru/post/226261/


All Articles