(Original creator: JamesRodger)
Hi, my name is James Rodger and I've spent the last 8 years working on Uniface applications as a Uniface consultant. I really enjoy the challenge of writing enterprise software so I thought I would tackle a nice light issue for my first blog post.
One of the areas of software development that I've been trying to become more familiar with is software design patterns. These describe techniques for addressing common development challenges and are hugely helpful in designing good software. Sadly there seems to be a perception that patterns require an object oriented programming language so we don’t see much discussion of them in the Uniface world. While it's true that some patterns don't translate well into a non-OO language there are many which will apply to architectures written in any type of language. It's also true that Uniface specific features mean we don't need a lot of these patterns since we get a lot of the functionality for free, but there are still patterns which are worth considering.
I'm going to talk briefly about one pattern which addresses a common problem with a lot of applications that can easily be applied to Uniface, Dependency Injection (DI). It is an example of an inversion of control pattern which essentially states that things should be configured rather than configure themselves. In Java or PHP we might say that an object has a dependency on another if it uses the 'new' keyword. In Uniface we can say that a component is dependent on another if it uses the 'newinstance' keyword (Let's assume for the moment that we always create instances this way as opposed to using activate, which is a discussion for another time). If a component is dependent on another then we can never separate them. They can't be unit tested in isolation or swapped out with an alternate implementation easily.
Let's consider an example. Here we have some code which is calling a service DATA_SVC for some data.
variables
Handle vDataHandle
String vSomeData
endvariables
newinstance "DATA_SVC", vDataHandle
vDataHandle->getSomeData("1", vSomeData)
DATA_SVC.getSomeData is implemented as follows.
;-----------------------------------------------------
operation getSomeData
;-----------------------------------------------------
params
String pDataId : IN
String pData : OUT
endparams
variables
String vConfigHandle
String vOfficeCode
endvariables
newinstance "CONFIG_SVC", vConfigHandle
vConfigHandle->getOfficeCode(vOfficeCode)
DATA_CD.DATA/init = pDataId
OFFICE_CD.DATA/init = vOfficeCode
retrieve/e "DATA"
pData = DATA_VALUE.DATA
return 0
end ;-getSomeData
We create a new service CONFIG_SVC in order to lookup some additional information before using this and the pDataId argument to fetch some data and return it in pData.
There are a number of issues with this approach:
- If we want to change the service that we use for fetching configuration data (CONFIG_SVC) then we need to alter the newinstance statement in every service that uses it.
- We can't unit test DATA_SVC without also having to test CONFIG_SVC. In other words, we can't use a mock implementation of CONFIG_SVC.
There is one other issue here, we're not really considering the life cycle of the DATA_SVC service. The CONFIG_SVC instance we create only stays alive for the length of this operation, it would perhaps make more sense to create the CONFIG_SVC instance in the init operation and keep the handle in a component variable.
;-----------------------------------------------------
operation init
;-----------------------------------------------------
;-Create an instance of the service we'll be using
newinstance "CONFIG_SVC", $configHandle$
end ;-init
Now let's suppose that we need to support 3 different configuration methods: Database, files and in-memory. We might alter the init trigger to look like this.
;-----------------------------------------------------
operation init
;-----------------------------------------------------
;-Create an instance of the service we'll be using
selectcase $logical("CONFIG_PROVIDER")
case "DB"
newinstance "CONFIG_DB", $configHandle$
case "FILE"
newinstance "CONFIG_FILE", $configHandle$
case "MEMORY"
newinstance "CONFIG_MEMORY", $configHandle$
endselectcase
end ;-init
Again we can see some potential problems with this approach:
- If we need to support another configuration method we need to add more cases to our selectcase. In fact we'll have to add a case to every service using these configuration providers.
- We still have the issue that we can't test DATA_SVC in isolation. We could add a "TEST" case but this introduces code only used for unit testing into our business logic, which should be avoided.
So let's try and fix some of these problems using Dependency Injection. There are a lot of DI frameworks for other languages out there, so the temptation might be to try and write something similar. However, it's important to remember that DI is a concept before it's a framework so we're really free to implement it however works best for our application.
The key is to try and remove all the ‘newinstance’ statements from DATA_SVC so that it isn't responsible for setting itself up any more. I’m going to move this logic out of DATA_SVC and into another service which is going to be purely responsible for creating and configuring instances for us.
;-----------------------------------------------------
operation getDataServiceInstance
;-----------------------------------------------------
params
Handle pDataHandle : OUT
endparams
variables
Handle vConfigHandle
endvariables
;-Create a config service based on the logical CONFIG_PROVIDER
selectcase $logical("CONFIG_PROVIDER")
case "DB"
newinstance "CONFIG_DB", vConfigHandle
case "FILE"
newinstance "CONFIG_FILE", vConfigHandle
case "MEMORY"
newinstance "CONFIG_MEMORY", vConfigHandle
endselectcase
;-Create a new instance of DATA_SVC
newinstance "DATA_SVC", pDataHandle
;-Setup DATA_SVC by injecting the configuration service we created
pDataHandle->setup(vConfigHandle)
return 0
end ;-getDataServiceInstance
This is then invoked by the component that wants to use DATA_SVC, note that the newinstance has been replaced with a call to our factory service.
variables
Handle vDataHandle
String vSomeData
endvariables
;-Get an setup and configured instance of DATA_SVC
activate "FACTORY_SVC".getDataServiceInstance(vDataHandle)
;-Finally use DATA_SVC to fetch the data we need
vDataHandle->getSomeData("1", vSomeData)
And here are the improved DATA_SVC operations (Note that the init operation is now gone because there is nothing for it to setup):
;-----------------------------------------------------
operation setup
;-----------------------------------------------------
params
Handle pConfigHandle : IN
endparams
;-Assign injected handle
$configHandle$ = pConfigHandle
end ;-setup
;-----------------------------------------------------
operation getSomeData
;-----------------------------------------------------
params
String pDataId : IN
String pData : OUT
endparams
variables
String vConfigHandle
String vOfficeCode
endvariables
$configHandle$->getOfficeCode(vOfficeCode)
DATA_CD.DATA/init = pDataId
OFFICE_CD.DATA/init = vOfficeCode
retrieve/e "DATA"
pData = DATA_VALUE.DATA
return 0
end ;-getSomeData
We can see from the new DATA_SVC operations that all the plumbing code has been removed since this is now being handled elsewhere. This allows the code in DATA_SVC to concentrate on doing its job and should be easier to read and maintain as a result. In this example all we've really done is move this logic out of DATA_SVC and into a dedicated “Factory” service which is purely responsible for creating and configuring, what would be called in the OO world, the object graph. In our case this is an instance of a service with all its dependant services created, setup, injected and ready to go.
We also now have the ability to add new configuration services without altering in any way the services which consume them. We can add a case to our Factory service and that's the only place we need to make the change. Swapping out the Factory for a unit testing framework also allows us to inject mock configuration services so that we can truly test DATA_SVC in isolation.
Hopefully this has given a flavour of the sort of design pattern that can easily be applied to a Uniface application. As with all these things a great many people will have been instinctively doing this for many years, but there is value in being able to recognise common patterns when using them and to using a common vocabulary when discussing them. If only because it allows you to read the wealth of software literature out there and immediately apply it your Uniface coding.