📜 ⬆️ ⬇️

Aggregation and awareness

The object aggregation mechanism is one of the great features of my jWidget JavaScript framework, which is not found in most other frameworks. I want to tell you more about it, because it helps to easily solve a wide range of typical tasks faced by the developers of web-application clients on the Model-View architecture. There will be few pictures, but a lot of interesting code.

I briefly described the mechanism of aggregation of objects in section 1. Classes and objects of the previous article . The article was published a year and a half ago. Since then, there have been 4 major updates to the framework, but the philosophy has been preserved. With experience, several stunning patterns of code structuring based on an aggregation mechanism were created, which significantly reduced the amount of code and made it easier. For a start, let me remind you what it is.


')

What is the mechanism of aggregation of objects



This is an approach to controlling the process of destruction of objects. Quote from the last article:

I drew this idea from the introduction to the book Object-Oriented Design. Design patterns from the "gang of four." It says that all pointers to objects are divided into two types: aggregation and awareness. Awareness means that the object that owns the pointer does not bear any responsibility for the object to which it refers. He simply has access to his public fields and methods, but the lifetime of this object is not under his control. Aggregation means that the object owning the link is responsible for the destruction of the object to which it refers. As a rule, an aggregated object lives while the owner object is alive, although there are more complex cases.

In jWidget, the aggregation is implemented through the own method of the JW.Class class. By passing object B to object A's own method, you made object A to own object B. When object A is destroyed, object B will be destroyed automatically. For convenience, the own method returns object B.


To get the most out of the aggregation mechanism, the framework / language should provide its implementation at the kernel level itself. So, in C ++, the aggregation mechanism is sewn up at the syntax level of the language, and in jWidget it is sewn up at the level of the most basic class JW.Class , from which all other classes inherit. Any object should be able to aggregate, and any object can be aggregated.

Example



Last time, the Chabrasoobshchestvo did not really like my example of the destruction of a soldier and his hands — they all immediately presented bloody scenes from American militants, so this time I’ll give a slightly less spectacular but no less telling example with a book and a cover.

var Book = function() { Book._super.call(this); this.cover = this.own(new Cover()); }; JW.extend(Book, JW.Class, { destroyObject: function() { console.log("Destroying book"); this._super(); } }); var Cover = function() { Cover._super.call(this); }; JW.extend(Cover, JW.Class, { destroyObject: function() { console.log("Destroying cover"); this._super(); } }); 


A cover is created in the book designer. The cover is aggregated in the book, so when you destroy a book, the cover is also automatically destroyed.

 var book = new Book(); book.destroy(); //   : // Destroying cover // Destroying book 


Real example



I will give an example from practice, more close to reality. Everyone loves to subscribe to jQuery events, but no one likes to unsubscribe from them. But in some cases it is necessary to unsubscribe, otherwise everything will explode.

 var MyView = function() { MyView._super.call(this); $(window).resize(JW.inScope(this._layout, this)); }; JW.extend(MyView, JW.Class, { _layout: function() { // ... } }); 


Will it really blow up?
Once we were developing an add-on to SketchUp, and we had a freelance documentator on the team. Once he documented the “Known Bugs” section in one of the releases, and read in the task that when grouping objects of a special kind, they may be incorrectly exported to a file. He asked me a question: “Well, what happens after all if you try to export such objects to a file?” I jokingly replied: “Well, I don’t know, it’s probably going to explode.” Well, he wrote that. Since then, the official documentation says “If you do that, then everything will explode.”


In this code, obviously, there is not enough formal reply from the “resize” event when the MyView object is destroyed. When destroying the MyView object, we must be sure that it will not leave behind any traces.

 var view = new MyView(); view.destroy(); 


But since we did not unsubscribe from the “resize” event, according to it the “_layout” method of the dead object will be called, which may cause unexpected side effects. Fix the mistake:

 var MyView = function() { MyView._super.call(this); $(window).bind("resize", JW.inScope(this._layout, this)); }; JW.extend(MyView, JW.Class, { _layout: function() { // ... }, destroyObject: function() { $(window).unbind("resize", JW.inScope(this._layout, this)); this._super(); } }); 


I intentionally made another typical mistake: the JW.inScope method creates a new function instance each time, so the “unbind” method does nothing. Fix this as well:

 var MyView = function() { this._layout = JW.inScope(this._layout, this); MyView._super.call(this); $(window).bind("resize", this._layout); }; JW.extend(MyView, JW.Class, { _layout: function() { // ... }, destroyObject: function() { $(window).unbind("resize", this._layout); this._super(); } }); 


Tired of ... A bunch of code for such a trifle? End this with aggregation!

 var MyView = function() { MyView._super.call(this); this.own($(window).jwon("resize", this._layout, this)); }; JW.extend(MyView, JW.Class, { _layout: function() { // ... } }); 


The jwon method returns a subscription to an event, the destruction of which, obviously, leads to a reply from the event.

Aggregation properties and collections



An important part of the aggregation mechanism of objects is the aggregation properties and collections. Calling the property's ownValue method causes it to aggregate any value that is assigned to it. So, if you change the property value or destroy the property itself, the current value will be destroyed.

 var property = new JW.Property().ownValue(); property.set(new SampleValue(1)); property.set(new SampleValue(2)); // SampleValue(1)   property.destroy(); // SampleValue(2)   


Similarly, calling the ownItems method of a collection causes it to aggregate any element that is added to it.

 var array = new JW.Array().ownItems(); array.add(new SampleValue(1)); array.add(new SampleValue(2)); array.set(new SampleValue(3), 0); // SampleValue(1)   array.remove(1); // SampleValue(2)   array.destroy(); // SampleValue(3)   


In the following examples, we will actively use these opportunities.

Now about the patterns.

Pattern 1: Easy object update



If the subobject that you want to be aggregated in the object re-creates over time, place it in an aggregating property.

Imagine that you are listening to a certain event, and whenever it occurs, in some way replace the contents of the object. When you create a new content you need to destroy the old.

 var MyView = function(event) { MyView._super.call(this); this.content = null; this.initContent(); this.own(event.bind(this.refreshContent, this)); }; JW.extend(MyView, JW.Class, { destroyObject: function() { this.doneContent(); this._super(); }, initContent: function() { this.content = new Content(); }, doneContent: function() { this.content.destroy(); }, refreshContent: function() { this.doneContent(); this.initContent(); } }); 


If you use the aggregating property of the simple object update pattern, this code can be reduced by 2 times:

 var MyView = function(event) { MyView._super.call(this); this.content = this.own(new JW.Property()).ownValue(); //   this.refreshContent(); this.own(event.bind(this.refreshContent, this)); }; JW.extend(MyView, JW.Class, { refreshContent: function() { this.content.set(new Content()); } }); 


If you really need to create new content only after destroying the old one, first set the property to null:

  refreshContent: function() { this.content.set(null); this.content.set(new Content()); } 


Pattern 2: Easy cancel operation



If at the moment of the destruction or change of the state of an object a certain operation can be performed, but you do not know for sure, then you should use the aggregating property to cancel it.

Often, especially when using a router based on the location.hash or History API, it becomes necessary to cancel the loading of the left page if the user quickly switches between pages. As a rule, loading is a sequential execution of AJAX requests, animations and other asynchronous operations. In addition, the page itself from time to time initiates a reload of its data. If the user navigates to another page while performing one of these operations, it must be canceled.

To solve this problem, we will get the “currentOperation” aggregation property and we will put the current asynchronous operation there.

 var MyPage = function() { MyPage._super.call(this); this.currentOperation = this.own(new JW.Property()).ownValue(); //   this.dataHunkIndex = 0; }; JW.extend(MyPage, JW.UI.Component, { afterRender: function() { this._super(); this._loadData(); }, _loadData: function() { this.currentOperation.set(new Request("/api/data", {hunk: this.dataHunkIndex}, this._onDataLoad, this)); }, _onDataLoad: function(result) { this.currentOperation.set(new Animation("fade in", this._onAnimationFinish, this)); }, _onAnimationFinish: function() { this.currentOperation.set(null); }, renderLoadNextHunkButton: function(el) { el.jwon("click", this._loadNextHunk, this); }, _loadNextHunk: function() { ++this.dataHunkIndex; this._loadData(); } }); 


Now we don’t need to worry about the integrity of the application state: when switching between pages and clicking on the “Load next hunk” button, the current operation will be canceled and the loading of new data will start from scratch.

Naturally, for each such operation, you must create a class, the destruction of which cancels the operation. An example implementation of the Request class for an AJAX request:

 var Request = function(url, data, success, scope) { Request._super.call(this); this.aborted = false; this.success = success; this.scope = scope; this.ajax = $.ajax({ url : url, data : data, success : this._onSuccess, error : this._onError, context : this }); }; JW.extend(Request, JW.Class, { destroyObject: function() { //    "abort" jQuery   , //     .     this.aborted = true; //    ,   "abort"  . //   -   this.ajax.abort(); this._super(); }, _onSuccess: function(result) { this.success.call(this.scope, result); }, _onError: function() { if (!this.aborted) { alert("Request has failed =(("); } } }); 


Pattern 3: Mass destruction of objects



If an object with a call to a certain function provokes the creation of a set of objects, then a single object should be returned to the functions that aggregates all these objects into itself.

Suppose that when an object is updated, a lot of objects are created that must be destroyed during the next update. Can I use an aggregation mechanism to reduce the amount of code in this case? Of course, using the simple object update pattern several times, we can create several properties, one for each sub-object. But what if you do not know in advance how many objects you need to create? This is a very common situation if objects are created by some kind of third-party factory. Consider the following code as an example:

 var Client = function(event, factory) { Client._super.call(this); this.factory = factory; this.objects = null; // Array this.initObjects(); this.own(event.bind(this.refreshObjects, this)); }; JW.extend(Client, JW.Class, { destroyObject: function() { this.doneObjects(); this._super(); }, initObjects: function() { this.objects = this.factory.createObjects(); }, doneObjects: function() { for (var i = this.objects.length - 1; i >= 0; --i) { this.objects[i].destroy(); } this.objects = null; }, refreshObjects: function() { this.doneObjects(); this.initObjects(); } }); var Factory = { createObjects: function() { return [ new Object1(), new Object2(), new Object3() ]; } }; 


A lot of incomprehensible code: refactoring suggests itself. Let the factory return the usual JW.Class, which aggregates within itself all 3 objects.

 var Client = function(event, factory) { Client._super.call(this); this.factory = factory; this.objects = this.own(new JW.Property()).ownValue(); this.refreshObjects(); this.own(event.bind(this.refreshObjects, this)); }; JW.extend(Client, JW.Class, { refreshObjects: function() { this.objects.set(this.factory.createObjects()); } }); var Factory = { createObjects: function() { //   var objects = new JW.Class(); objects.own(new Object1()); objects.own(new Object2()); objects.own(new Object3()); return objects; } }; 


Pattern 4: Destroying the object driver



If the function returns an object that has its own driver, aggregate that driver in the object itself.

Suppose you want to write a method that creates a property, subscribes to some events to update this property, and returns this property. A subscription to these events is called the driver of this property. The driver of the object A will be called any object that affects the change of the object A.

I will give an example of the driver. Let somewhere on the page there is a control for dynamic switching between color schemes of the application. The color scheme is defined by the dictionary "key - color". Each color scheme is unchanged, but you can switch between them. Subscribing to the change event of the selected color scheme is a property driver containing a color with a given key. The key that we need, we know in advance; the color depends on the chosen color scheme. In addition, when changing the selected color scheme, the property with color should be automatically updated. Let's write a class for such a driver.

 var ColorDriver = function(selectedScheme, colorKey, color) { ColorDriver._super.call(this); this.selectedScheme = selectedScheme; // JW.Property<Dictionary> this.colorKey = colorKey; //   color  this.color = color || this.own(new JW.Property()); this._update(); this.own(this.selectedScheme.changeEvent.bind(this._update, this)); }; JW.extend(ColorDriver, JW.Class, { _update: function() { this.color.set(this.selectedScheme.get()[this.colorKey]); } }); 


Note
In jWidget, the same can be done without creating a class, if you use the selectedScheme method. $$ mapValue . But at a low level, this method still uses the driver kill pattern, using JW.Mapper as the driver. Therefore, in order to demonstrate the pattern, it is quite reasonable to abandon the use of this method.


To make it easier, we will write a color management class that can create such properties and drivers.

 var ColorSchemeManager = function() { ColorSchemeManager._super.call(this); this.selectedScheme = this.own(new JW.Property()); }; JW.extend(ColorSchemeManager, JW.Class, { getColorDriver: function(key) { return new ColorDriver(this.selectedScheme, key); } }); 


When a color is no longer needed, its driver must be destroyed. It is impossible to aggregate the driver in the manager, since it is not the manager that provokes the creation of the driver. For the destruction of the driver should be responsible object that provokes its creation, i.e. customer.

 var Client = function(colorSchemeManager) { Client._super.call(this): this.colorSchemeManager = colorSchemeManager; }; JW.extend(Client, JW.UI.Component, { renderBackground: function(el) { var color = this.own(this.colorSchemeManager.getColorDriver("background")).color; this.own(el.jwcss("background-color", color)); } }); 


This will work, but there are several disadvantages to this solution:

  1. The driver is not at all interested in the client - the client only needs color
  2. Difficulties with polymorphism: if in some cases a driver is needed, and in others it is not needed, then to ensure polymorphism you will have to write a dummy driver who does nothing but keeps a reference to the color
  3. This code is hard to read.


To solve these problems, we use the driver kill pattern. The pattern recommends that the object driver be aggregated in the object itself. Since jWidget destroys the aggregated objects before the object itself, this does not produce any side effects.

 var ColorSchemeManager = function() { ColorSchemeManager._super.call(this); this.selectedScheme = this.own(new JW.Property()); }; JW.extend(ColorSchemeManager, JW.Class, { getColor: function(key) { //   var color = new JW.Property(); color.own(new ColorDriver(this.selectedScheme, key, color)); return color; } }); var Client = function(colorSchemeManager) { Client._super.call(this): this.colorSchemeManager = colorSchemeManager; }; JW.extend(Client, JW.UI.Component, { renderBackground: function(el) { var color = this.own(this.colorSchemeManager.getColor("background")); this.own(el.jwcss("background-color", color)); } }); 


Client code has become simpler and clearer.

Conclusion



With the object aggregation mechanism, you no longer need to explicitly implement and call object destructors. The implementation of destructors remains only in low-level classes, such as an AJAX request, event subscription, etc. The jWidget classes implement most of the necessary destructors, so all you have to do is correctly associate the objects with an aggregation relation.

Replacing all the explicit calls of class destructors by an object aggregation mechanism on one of the projects, I managed to remove 1000 lines of code from 15,000 (7% win).

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


All Articles