Properties
Properties are something similar to fields, they store information about the object. Unlike fields, properties have getters and setters. Getter is an operation of getting the value of the property, and setter is an operation of settings the value of the property, pretty easy for now.
In UI5 framework there is a separate "feature" for defining properties: they are defined in "metadata" field of the class. (All possible values for properties can be found
here -> 'properties' : object) Let's transform Document class from part 1 and define sDocumentId field as property:
sap.ui.define([
"sap/ui/base/ManagedObject"
], function(
ManagedObject
) {
"use strict";
return ManagedObject.extend("com.blog.classes.Document", {
metadata: {
properties: {
documentId: {
type: "string",
defaultValue: ""
}
}
},
constructor: function(sDocumentId) {
ManagedObject.prototype.constructor.apply(this, []);
this.setDocumentId(sDocumentId);
return this;
},
getDataMap: function() {
return {
DOCUMENT_ID: this.sDocumentId
};
}
});
});
For each property UI5 framework automatically generates four methods: get<PropertyName>, set<PropertyName>, bind<PropertyName>, unbind<PropertyName>. (I guess that's one of the issues why it is hard to migrate the framework to typescript
😉 ) In this case it's setDocumentId, getDocumentId, bindDocumentId and unbindDocumentId.
What are the benefits of defining properties?
- Setters and getters can be redefined. Let's imagine the Document class has some kind of status, and depending on it it can be either editable, or not. It is possible to define such a property and use it:
return ManagedObject.extend("com.blog.classes.Document", {
metadata: {
properties: {
...
isEditable: {
type: "boolean"
},
status: {
type: "string"
}
}
},
...
getIsEditable: function() {
const aEditableStatuses = ["Initial", "In Process", "Reviewing"];
return aEditableStatuses.includes(this.getStatus());
},
setIsEditable: function() {
throw new Error("editable property is computed and can not be set")
}
});
Two properties were introduced: status and isEditable, where isEditable is status dependant. Getter was redefined in order to return true or false depending on document status. Setter was redefined and it throws exception, because it's logically impossible to set that property.
UI5 framework sometimes is not nice to developers, and for full compatibility it might be necessary to redefine setProperty, getProperty as well, because framework does store properties in private _mProperties field. Who knows when somebody decides to use "getProperty("isEditable") instead of "getIsEditable". That's why more bulletproof solution looks like this:
getProperty: function(sPropertyName) {
let vResult;
if (sPropertyName === "isEditable") {
vResult = this.getIsEditable();
} else {
vResult = ManagedObject.prototype.getProperty.apply(this, arguments);
}
return vResult;
},
setProperty: function(sPropertyName) {
let vResult;
if (sPropertyName === "isEditable") {
this.setIsEditable();
} else {
vResult = ManagedObject.prototype.setProperty.apply(this, arguments);
}
return vResult;
},
getIsEditable: function() {
const aEditableStatuses = ["Initial", "In Process", "Reviewing"];
return aEditableStatuses.includes(this.getStatus());
},
setIsEditable: function() {
throw new Error("editable property is computed and can not be set")
},
A bit of a pain, but it's not so often when you need to use such tricks. Most of the cases it's enough with standard set/get. If it comes into your mind that nobody ever will call get/set using getProperty/setProperty, throw this thought out. If something is technically possible, it will happen one day or another. Moreover, who knows if framework itself is using setProperty/getProperty 😉
- Properties can be binded to a model. Explanation about how binding works will be in the part 3.
Apart from UI5, there are also properties in other languages. Although if you are using typescript for UI5, properties will be still defined through metadata.
Typescript:
class Document {
private _status: string;
public get status(): string {
return this._status;
}
public set status(value: string) {
this._status = value;
}
}
const document = new Document();
//referring to properties
const status = document.status;
//setting status
document.status = "Error";
C#
class Document
{
private string _status;
public string status
{
get { return _status; }
set { _status = value; }
}
}
Document document = new Document();
//referring to properties
string status = document.status;
//setting status
document.status = "Error";
SAPUI5 Extension
From SAPUI5 extension perspective it does react on properties in metadata and creates completion items for generated methods automatically. Make sure that you don't use JSON notation for metadata definition, it's not supported by the extension. Too much ifs and places to edit in order to make it work
😉
Associations, aggregations, compositions
All the terms are about relationship between objects.
Association
Association is a general naming for any kind of relationships. If Object1 is somehow using Object2, it's association.
Aggregation is association, composition is association as well.
Associations can be defined in every class metadata, but I find them a bit hard to use, because they store only the ID of the associated object. That makes them usable only for controls, which is a bit odd decision as for me. I prefer using aggregations or just declaring the class field with reference to another object.
Aggregation
Aggregation is subtype of association, except it's more strictly defined. Aggregation means that Object1 and Object2
can exist independently. In UI5 there is a special place where it is possible to define aggregations, and it's again in metadata of any class. Let's assume that our document is somehow related to the product. Let's enhance Document class with productId property and productReader aggregation.
Let's create ProductReader class, which, well, will read details about the product:
ProductReader
sap.ui.define([
"sap/ui/base/ManagedObject"
], function(
ManagedObject
) {
"use strict";
return ManagedObject.extend("com.blog.classes.product.ProductReader", {
constructor: function(sProductId) {
ManagedObject.prototype.constructor.apply(this, arguments);
this._sProductId = sProductId;
return this;
},
getProductInfo: function() {
const oODataModel = this.getModel("ODataModel");
const sPath = oODataModel.createKey("/Products", {
ProductID: this._sProductId
});
return oODataModel.readAsync(sPath);
}
});
});
Document
metadata: {
properties: {
productId: {
type: "string"
}
},
aggregations: {
productReader: {
type: "com.blog.classes.product.ProductReader",
multiple: false
}
}
},
constructor: function(mDocument) {
ManagedObject.prototype.constructor.apply(this, []);
const sProductId = mDocument.ProductID;
this.setProductId(sProductId);
const oProductReader = new ProductReader(mDocument.ProductID);
this.setProductReader(oProductReader);
return this;
}
readProductInformation: async function() {
const oProductInfo = this.getProductReader();
const mProductInfo = await oProductInfo.getProductInfo();
//do something with product info
}
Now we have a Document class, which is somehow related to the product, and we are able to get information about the product using ProductReader class. Long story short: Document uses ProductReader class, but both of them can exist independently. That's an aggregation.
In metadata there are two ways of how aggregations are handled:
- If multiple is set to true, there can be many objects in the aggregation. Depending on that, different methods are generated (e.g. getProductReader if multiple is false, getProductReaders if multiple is true). See 'aggregations' : object for more info
- If multiple is set to false, there can be only one object in the aggregation. Our case is this one, we need only one product reader.
Aggregations can be binded as well. That's exactly what's happening in the views:
<List items="{Model>/ListItems"}> <!-- That's a binding itself
format: (aggregation name)="{(model name)>(binding path)}" -->
<items> <!-- That's aggregation name-->
<CustomListItem/> <!-- That's a template for aggregation binding -->
</items>
</List>
In other languages I didn't see any additional support for aggregations as in UI5 framework. It's a formal naming for the relationships, meaning that if you create e.g. field in the class:
this._oProductReader = new ProductReader();
It's still an aggregation. You might be interested to add it to metadata if you want a bunch of methods generated or you want to bind it.
Aggregated instances are not destroyed together with parent class, so in our case ProductReader instance should
not be destroyed together with Document class instance.
UML
Aggregations in UML class diagram looks as follows:
Composition
Composition is subtype of association, except it's also more strictly defined. Composition means that Object1 and Object2
can't exist independently. In UI5 there is no special place where it is possible to define compositions, but it's actually not needed.
Let's enhance Document class. Documents usually have statuses, which changes behavior of some actions. As an example, lets think of two statuses for document: In Process and Posted. The architecture looks as follows:
I must admit that I went a bit ahead of myself here, so don't be worried if something is not clear at this point. Here we do use state, factory design pattern, interfaces and polymorphism. Everything regarding that will be discussed later, it's not so critical to understand right now.
The idea here is as follows:
- For each state there is a separate class with "postDocument" method.
- For one point of creating states there is "StateFactory" with "createInstance" method. Its purpose is to create and return "PostedState" or "InProcessState" instance.
- Document class is using state factory to create state instance. Reference to state instance is saved in "_oState" field.
- When somebody is trying to post "Document", "_oState" instance is taken and "postDocument" method is called, which will differ depending on document state: In Process state posts the document, Posted state throws an exception.
It looks like overkill, but in real life there will be much more dependencies on document state which also could be implemented in State classes. (E.g. document.reverse, document.cancel etc)
State class instances can't exist without Document class instance, such relationships are called composition.
Instances of States are destroyed together with Document instance. (Don't forget to do that
😉 )
Compositions can't be binded.
Implementation would look like this:
Document
setState: function(sState) {
if (this._oState) {
this._oState.destroy();
}
this._oState = StateFactory.createInstance(StateFactory.mStates[sState]);
},
post: async function() {
await this._oState.postDocument(this);
}
IDocumentState
sap.ui.define([], function() {
"use strict";
return {
/**
* @param {com.blog.classes.Document} oDocument document to post
* @returns {Promise} when document was posted
*/
postDocument: async function(oDocument) {
throw new Error(`Can't call method of an interface for document "${oDocument.getDocumentId()}"`);
}
};
});
InProcessState
sap.ui.define([
"sap/ui/base/ManagedObject"
], function(
ManagedObject
) {
"use strict";
return ManagedObject.extend("com.blog.classes.state.InProcessState", {
metadata: {
interfaces: ["com.blog.classes.state.IDocumentState"]
},
/**
* @override
* @param {com.blog.classes.Document} oDocument
*/
postDocument: async function(oDocument) {
//perform posting
}
});
});
PostedState
sap.ui.define([
"sap/ui/base/ManagedObject"
], function(
ManagedObject
) {
"use strict";
return ManagedObject.extend("com.blog.classes.state.PostedState", {
metadata: {
interfaces: ["com.blog.classes.state.IDocumentState"]
},
/**
* @override
* @param {com.blog.classes.Document} oDocument
*/
postDocument: async function(oDocument) {
throw new Error(`Can't post document "${oDocument.getDocumentId()}", because it's already posted`);
}
});
});
StateFactory
sap.ui.define([
"sap/ui/base/ManagedObject",
"com/blog/classes/state/InProcessState",
"com/blog/classes/state/PostedState"
], function(
ManagedObject,
InProcessState,
PostedState
) {
"use strict";
const StateFactory = ManagedObject.extend("com.blog.classes.state.StateFactory", {});
StateFactory.mStates = {
InProcess: "InProcess",
Posted: "Posted"
};
/**
* @param {string} sState state
* @returns {com.blog.classes.state.IDocumentState} state object of the document
*/
StateFactory.createInstance = function(sState) {
let oState;
const mStateMapping = {};
mStateMapping[StateFactory.mStates.InProcess] = InProcessState;
mStateMapping[StateFactory.mStates.Posted] = PostedState;
if (mStateMapping[sState]) {
oState = new mStateMapping[sState]();
} else {
throw new Error(`State "${sState}" doesn't exist`)
}
return oState;
};
return StateFactory;
});
Conclusion
At the end of this blog it should be clear what does "property", "aggregation", "composition" and "association" stands for. Hopefully it helped anyone to understand OOP world better
🙂
Agenda can be found here