22 Mrz Design Pattern in OO ABL – Part 1 of 3
Wikipedia says: ‘In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design.’
Patterns are conceptual work, the idea has been known to the public ever since the publication of the book ‘Design Patterns: Elements of Reusable Object-Oriented Software’, in 1995.
In a larger project design patterns help to keep the code reusable, readable, and to avoid problems in the long term. E.g. using the “Builder Pattern” allows changing and adding parameters of often used objects late in a project.
The article will describe how to implement some of the patterns in OpenEdge Object Oriented ABL – shortly named OO ABL. The chosen patterns are very common and often used. The view to the patterns is driven by daily, practical work.
It is based on a presentation by Michael Barfs, first held at Germany’s Progress User Group meeting in March 2018.
This is the first part of our three-part series on Design Patterns. The second article - available here - covers the pattern 'Lazy Loading' and 'Adapter'*. The third article - available here - talks about 'Abstract Fabric' and 'Proxy'.
PATTERN 1 – BUILDER (CREATIONAL PATTERN)
Imagine you have an object like this:
For several ways to create an instance of the ‘user’ there are several constructors in the class with more or less parameters.
So if you would like to create a user object with all parameters, the code looks like this:
DEFINE VARIABLE oUser AS User NO-UNDO.
oUser = NEW User(
"Joe",
"Miller",
23,
"+49 40-99 99 99-99",
"MyStreet 14, 20355 Hamburg"
).
With builder, the object diagram looks like this:
Besides the User object there is an UserBuilder object. This object has all the properties from the User object, but the constructor only has the mandatory properties (First and Last name). For every other property there is a method with a descriptive name, e.g. for iAge there is SetAge.
The secret is hidden in the setter methods:
CLASS UserBuilder:
...
METHOD PUBLIC UserBuilder setAge(iAge AS INTEGER):
THIS-OBJECT:iAge = iAge.
RETURN THIS-OBJECT.
END METHOD.
...
END CLASS.
The setter method sets the according property and returns itself (THIS-OBJECT). That means, the output is the builder object and this can be used to set another property or to complete the object.
DEFINE VARIABLE oUser AS User NO-UNDO.
oUser =
(NEW UserBuilder("Michael", "Barfs")
:setAge(23)
:setPhone("+49 40-30 68 03-26")
:setAddress("Valentinskamp 30, 20355 Hamburg")
:build()).
:build()).
This structure is called chaining. By the way, the created UserBuilder object is thrown away immediately and will hopefully be removed by the garbage collector.
- Readable – the property is named by the setter method
- Less confusion with parameters – they are named
- Auto-complete suggests property methods
- Shorter than setting properties in user object
- Simple to change the base object without changing existing calls
To make the advantages clearer, here is an example of a procedural code with a lot of parameters.
The code is a shortened version of the full call.
RUN StatusCreate IN l-Import-Library-Handle
( INPUT l-DB-Cust,
INPUT "",
INPUT 150,
...
INPUT
"QtyType=" + OrderQtyQualifier + "{&T}"
+ "UTCTime=" + l-UTCTime
+ "{&T}"
+ "ConC-ID=" + SSCO-Ord.ConC-ID
...
) NO-ERROR.
) NO-ERROR.
The parameters are in a given order (like a very large constructor). If a parameter is added, removed or changed, every existing calls needs to be changed, too.
The programmers were so desperate, that they added a universal parameter (List of key=value{Delimiter}key=value…). That was done to allow changing the parameters without changing existing code. These kinds of lists are very good as a short term fix, but bad in long-term maintenance.
So that is was working solution, but not a good one.
Here is the approach with the Builder Pattern (excerpt).
DEFINE VARIABLE oStatusAnlage AS StatusAnlage NO-UNDO.
oStatusAnlage =
(NEW StatusAnlageBuilder()
:setCustCode(l-DB-Cust)
:setStatusNumeric(150)
...
:setQtyType(OrderQtyQualifier)
:setUTCTime(l-UTCTime)
:setConCID(SSCO-Ord.ConC-ID)
...
:build()).
The code is much more readable and you can add parameters without changing existing code.
The Builder Pattern will make the code more readable, better working due to auto-complete and allows changes later in a project.
PATTERN 2 – SINGLETON (CREATIONAL PATTERN)
There are no global objects in OO languages, but many people see the need for kinds of global structures, e.g. read only setup information.
Some people disagree with the idea of the Singleton Pattern, since they see it as an Anti Pattern. Here is a good discussion about the pros and cons of using Singleton and what to consider .
If you use a Singleton for read-only content and Dependency Injection for dependent state information, it should be OK.
The Singleton is using the effect, that static elements (object, properties) are kind of global. They are instantiated during first access and they exist as long as the session is running. They can not be deleted.
Here is the base code.
CLASS Configuration:
...
DEFINE PUBLIC STATIC PROPERTY oInstance AS Configuration
PUBLIC GET():
IF NOT VALID-OBJECT(oInstance) THEN
oInstance = NEW Configuration().
RETURN oInstance.
END GET.
PRIVATE SET.
CONSTRUCTOR PRIVATE Configuration():
loadConfig().
END CONSTRUCTOR.
...
END CLASS.
Note the PUBLIC STATIC property and the PRIVATE constructor.
Note also that the configuration object is created during the first call of the object instance (oInstance).
Here is the usage of the configuration object.
DEFINE VARIABLE oConf AS Configuration NO-UNDO.
DEFINE VARIABLE cBaseExportPath AS CHARACTER NO-UNDO.
oConf = Configuration:oInstance.
cBaseExportPath = oConf :getValue("BaseExportPath")
...
Advantages of Singleton Pattern:
- Solves problem of global settings
- Inheritance is possible (which is not possible from a static object)
- Has some logic during instantiation
- Can be re-instantiated (which is not possible with a pure static object)
The Singleton Pattern will make kinds of global settings available.Programmers must care about dependencies! It would be best to use Singleton for read only and Dependency Injection for a local active program state.
PATTERN 3 – MULTITON (CREATIONAL PATTERN)
The Multiton returns an object based on an index value. It will guarantee that every call using this index value will return the same object. This is useful, when you have multiple run time objects that are accessing one piece of data.
Here is the object structure.
Sample code of the declaration part. Note that the temp-table is static (global).
DEFINE PUBLIC PROPERTY iCustNum AS INTEGER NO-UNDO GET.
PRIVATE SET.
DEFINE PUBLIC PROPERTY cName AS CHARACTER NO-UNDO GET.
PRIVATE SET.
DEFINE PRIVATE STATIC TEMP-TABLE ttCustomer
FIELD custNum AS INTEGER
FIELD obj AS Progress.Lang.ObjectINDEX ID custNum.....
END CLASS.
The access method is public, but static.
When an instance is asked for an entry, an existing object or new object will be returned.
The reference in the temp-table is permanent, so garbage collection will not happen.
METHOD PUBLIC STATIC Customer getInstance(iCustNum AS INTEGER):
FIND FIRST ttCustomer WHERE ttCustomer.custNum = iCustNum NO-LOCK NO-ERROR.
IF NOT AVAILABLE ttCustomer THEN
DO:
CREATE ttCustomer.
ASSIGN
ttCustomer.custNum = iCustNum
ttCustomer.obj = NEW Customer(iCustNum)
.
END.RETURN CAST(ttCustomer.obj, Customer).
END METHOD.
The constructor is private, it will be called from the code above and not from the code using the Multiton.
This sample code is loading data from the database.
CONSTRUCTOR PRIVATE Customer(iCustNum AS INTEGER):
DEFINE BUFFER bCustomer FOR Customer.
FIND FIRST bCustomer WHERE bCustomer.CustNum = iCustNum NO-LOCK NO-ERROR.
IF AVAILABLE bCustomer THEN DO:
THIS-OBJECT:cName = bCustomer.Name.
THIS-OBJECT:iCustNum = bCustomer.CustNum.
END.
END CONSTRUCTOR.
Finally, here is the usage.
We give a customer number to the Multiton and receive a customer object.
DEFINE VARIABLE oCust AS Customer NO-UNDO.
oCust = multiton.Customer:getInstance(1537).
Remarks:
-
-
- The basic idea of Multiton is to have thread safe objects
(which is at the moment not important for OOABL) - The objects are like global, so they accumulate
(the programmer may delete them) - Creating/Accessing 10000 objects is OK, 100000 is not (much slower)
(Performance test in OE 11.7x)
- The basic idea of Multiton is to have thread safe objects
-
Disadvantages
-
-
- Vulnerable to side effects, e.g. like global shared buffer
- Unit tests becomes more complex
- OOABL performance problems with lots of objects
-
The Multiton can be used without any considerations for reading data, like in reports or display only.
The Multiton Pattern will allow program-wide access to unique resources. This could be streams, WebServices, ESB access, DB connections and data.
Download full articles and source codes!
Lets see our three-part series in a single file and download our Code Samples.