Why initializing properties on prototypes can have nasty side effects in SAPUI5

Recently a colleague reviewed some of my SAPUI5 tutorials. He pointed out that declaring and initializing private properties directly on the prototype object can lead to issues. This could especially be the case if multiple instances of the control or controller are available at runtime. What he said made sense. I decided to write this little tutorial to create awareness of the issues that can occur.

Table of contents:


1. Example code

To illustrate the issues let’s implement a custom control and create four instances of that control. The control itself is pretty easy to understand:

  • It has one "real" property called "text" of type string
  • It has three "very" private attributes/properties on the same level as the metadata property:
    • _sPrivateString initialized with "Initial String"
    • _aPrivateArray initialized with ["One", "Two", "Three"]
    • _oPrivateObject initialized with { val : "Initial value"}
  • It has getters and setters for _sPrivateString, _aPrivateArray, and _oPrivateObject

Our custom control simply renders the values of the properties. The getters and setters for _sPrivateString, _aPrivateArray, and _oPrivateObject are not generated by the SAPUI5 runtime. Therefore we implement the getters and setters manually.

Let's walk through the code that uses our custom control step by step (see second script tag in the code box below). As you can see, we create four instances of our custom control and set the corresponding text properties to an appropriate value. This helps us to identify the instances easier when the controls are rendered.

Next we want to check if we could have issues with strings. We change our _sPrivateString for each instance by using the setter we implemented manually.

Then we want to change _aPrivateArray by using both getters and setter. First we set the _aPrivateArray of our second instance to a new array. After that we get a reference to the _aPrivateArray of our third instance and append a new string.

Finally, we want to change _oPrivateObject by using both getters and setter as well. First, we set the _oPrivateObject of our second instance to a new object. After that we get a reference to _oPrivateObject of our third instance and change its val property.

Last but not least we add the four instances to the DOM because we want to see something on the screen. Here is the complete code including a few lines that illustrate the issues:

Demo code (live demo)
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="X-UA-Compatible" content="IE=edge" >
        <meta http-equiv='Content-Type' content='text/html;charset=UTF-8'>

        <title>Demo - Declaring properties on prototype level in SAPUI5</title>

        <script 
            id="sap-ui-bootstrap"
            src="https://openui5.hana.ondemand.com/1.36.12/resources/sap-ui-core.js"
            data-sap-ui-libs="sap.m" 
            data-sap-ui-theme="sap_bluecrystal"></script>

            <script>
            //let's implement a simple custom control
            (function () {
                "use strict";
                sap.ui.core.Control.extend("nabisoft.Demo", {

                metadata : {
                    properties : {
                        text : {type : "string"}
                    }
                },
                
                _sPrivateString : "Initial String",
                _aPrivateArray  : ["One", "Two", "Three"],
                _oPrivateObject : { val : "Initial value"},

                init : function(){ },

                renderer : {
		 
                    render : function(oRm, oControl) {

                        oRm.write("<div");
                        oRm.writeControlData(oControl);
                        oRm.addClass("nsDemo");
                        oRm.writeClasses();
                        oRm.write(">");

                        oRm.write("<div>");
                        oRm.write(oControl.getText());
                        oRm.write("</div>");

                        oRm.write("<div>");
                        oRm.write("_sPrivateString = " + oControl._sPrivateString);
                        oRm.write("</div>");

                        oRm.write("<div>");
                        oRm.write("_aPrivateArray = " + oControl._aPrivateArray.join());
                        oRm.write("</div>");

                        oRm.write("<div>");
                        oRm.write("_oPrivateObject.val = "+ oControl._oPrivateObject.val);
                        oRm.write("</div>");

                        oRm.write("</div>");
                    }
                }
            });

            nabisoft.Demo.prototype.setPrivateString = function (sVal) {
                this._sPrivateString = sVal;
            };
            nabisoft.Demo.prototype.getPrivateString = function () {
                return this._sPrivateString;
            };

            nabisoft.Demo.prototype.setPrivateArray = function (aArray) {
                this._aPrivateArray = aArray;
            };
            nabisoft.Demo.prototype.getPrivateArray = function () {
                return this._aPrivateArray;
            };
				
            nabisoft.Demo.prototype.setPrivateObject = function (oObject) {
                this._oPrivateObject = oObject;
            };				
            nabisoft.Demo.prototype.getPrivateObject = function () {
                return this._oPrivateObject;
            };
        }());

        </script>

        <script>
            
            //now we create a simple demo that uses our custom control
            
            var oDemo1 = new nabisoft.Demo({
                text : "Demo 1"
            });

            var oDemo2 = new nabisoft.Demo({
                text : "Demo 2"
            });

            var oDemo3 = new nabisoft.Demo({
                text : "Demo 3"
            });

            var oDemo4 = new nabisoft.Demo({
                text : "Demo 4"
            });

            //strings
            oDemo2.setPrivateString("Demo 2 String");
            oDemo3.setPrivateString("Demo 3 String");
            oDemo4.setPrivateString("Demo 4 String");

            //arrays
            oDemo2.setPrivateArray(["Demo2", "Demo2", "Demo2"]);
            oDemo3.getPrivateArray().push("Demo3");

            //objects
            oDemo2.setPrivateObject({ val : "Demo2 value"});
            oDemo3.getPrivateObject().val = "Demo3 value";

            //add to DOM
            oDemo1.placeAt('content1');
            oDemo2.placeAt('content2');
            oDemo3.placeAt('content3');
            oDemo4.placeAt('content4');

        </script>

    </head>
    <body class="sapUiBody">
        <div style="padding:20px;">
            <div id="content1" style="border:1px solid black;"></div><br/>
            <div id="content2" style="border:1px solid black;"></div><br/>
            <div id="content3" style="border:1px solid black;"></div><br/>
            <div id="content4" style="border:1px solid black;"></div><br/>
        </div>
    </body>
</html>

2. The expected result

Can you guess what happens? Below you can see what most of us would expect. Since we have four instances and of our custom control we would expect that changing the properties of a certain instance only affect that certain instance. In our example the oDemo1 and oDemo4 instances are not touched at all. Therefore we would expect that the initial values get rendered. oDemo2 and oDemo3 are changed in our code, so we would expect that we can see their new values rendered. If this behavior is what you expected then you've been fooled.

Expected result
Expected result

3. The actual result

The correct result looks like what you can see in the screenshot below. Changing _sPrivateString of an instance with the setter has no infulence on the other three instances. Using the setter of oDemo2 to change _aPrivateArray has no influence on the other three instances as well. However, using the getter for _aPrivateArray of oDemo3 and appending a new value to the array definitely has a side effect. As you can content of _aPrivateArray is suddenly One,Two,Three,Demo3 for oDemo1, oDemo3, and oDemo4 although we used oDemo3 to change only the array of oDemo3. The array of oDemo2 has the correct content because we changed it to a new array. There is also something to note about _oPrivateObject: only oDemo2 and oDemo3 have the correct values for _oPrivateObject.val while oDemo1 and oDemo4 share the value of oDemo3 for their _oPrivateObject.val.

Actual result
Actual result

So what happened? It turns out sap.ui.core.Control.extend(…) has put our three private properties into the prototype object of our custom control. Here is the relevant API documentation:

sap.ui.core.Element.extend

"any-other-name: any other property in the oClassInfo is copied into the prototype object of the newly created class. Callers can thereby add methods or properties to all instances of the class. But be aware that the given values are shared between all instances of the class. Usually, it doesn't make sense to use primitive values here other than to declare public constants."

The documentation even gives us an important hint from which we can derive the following: our three properties will be shared between all instances of our custom control! In other words: we have some kind of "static" properties! We have to consider this especially in case of arrays and objects. With this piece of knowledge we can now explain what has happened.

When we call oDemo2.setPrivateArray(["Demo2", "Demo2", "Demo2"]); the instance property _aPrivateArray is changed. For our oDemo2 instance, this leads to the fact that _aPrivateArray on the prototype object is not visible via "this._aPrivateArray anymore. So far we have no issue. However, when we call oDemo3.getPrivateArray().push("Demo3"); we might tend to think that we are changing only the _aPrivateArray of our oDemo3 instance. That’s wrong! Instead, we are changing _aPrivateArray on the prototype object. Therefore, our oDemo1, oDemo3, and oDemo4 instances suddenly see a new value in _aPrivateArray. The same thing happens for objects as well (note: arrays are objects). When we call oDemo2.setPrivateObject({ val : "Demo2 value"}); the instance property _oPrivateObject of oDemo2 is changed. Again, this leads to the fact that _oPrivateObject on the prototype object is not visible via this._oPrivateObject anymore for our oDemo2 instance. When we call oDemo3.getPrivateObject().val = "Demo3 value"; we might tend to think that we are changing only the _oPrivateObject of our oDemo3 instance. Again, that’s wrong! Instead, we are changing _oPrivateObject on the prototype object. Therefore, our oDemo1, oDemo3, and oDemo4 instances suddenly see a new value in _oPrivateObject.val.


4. Learnings / Best Practices

Finding these kind of bugs can be very hard. If you are lucky then you will never face any issues. There are some ways or even "Best Practices to avoid the pitfall described above.

If you still want to do it like the example above, then make sure to only initialize arrays and objects with null or undefined. After that, initialize the real values inside the init() function:

Option 1
(function () {
    "use strict";
    sap.ui.core.Control.extend("nabisoft.Demo", {

        metadata : {
            properties : {
                text : {type : "string"}
            }
        },
                
        _sPrivateString : null,
        _aPrivateArray  : null,
        _oPrivateObject : null,

        init : function(){
            //initialize instance properties
            this._sPrivateString = "Initial String";
            this._aPrivateArray  = ["One", "Two", "Three"];
            this._oPrivateObject = { val : "Initial value"};
        },

        renderer : {
            render : function(oRm, oControl) {
                //...
            }
        }
    });
    //...
}());

A better approach would be to do it all inside the init() function without declaring the properties on the prototype object:

Option 2 (preferred)
(function () {
    "use strict";
    sap.ui.core.Control.extend("nabisoft.Demo", {

        metadata : {
            properties : {
                text : {type : "string"}
            }
        },

        init : function(){
            //private instance properties
            this._sPrivateString = "Initial String";
            this._aPrivateArray  = ["One", "Two", "Three"];
            this._oPrivateObject = { val : "Initial value"};
        },

        renderer : {
            render : function(oRm, oControl) {
                //...
            }
        }
    });
    //...
}());

Of course, if you really want to share static data between all instances you don't have to follow the two advices. It is only important to know what you are doing and to make sure that everybody on the project shares the same understanding.


Comments
RE: point out a trivial mistake
posted by Nabi
Fri Jun 12 07:57:06 UTC 2015
Thank you, Barry! That was a copy & paste error from my side. I will fix the code of both option 1 and option 2 soon!
point out a trivial mistake
posted by Barry
Fri Jun 12 04:31:24 UTC 2015
option 1, in the init function, you should have used "=" to initialize the three variables.