Blog

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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. [uniface] variables Handle vDataHandle String vSomeData endvariables newinstance

Code Block
variables
  Handle vDataHandle
  String vSomeData
endvariables
 
  newinstance "DATA_SVC", vDataHandle
  vDataHandle->getSomeData("1", vSomeData)

...


DATA_SVC.getSomeData is implemented as follows. [uniface]

Code Block
;-----------------------------------------------------
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:

...

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. [uniface]

Code Block
;-----------------------------------------------------
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. [uniface]

Code Block
;-----------------------------------------------------
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:

...

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. [uniface]

Code Block
;-----------------------------------------------------
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. [uniface] variables Handle vDataHandle String vSomeData endvariables

Code Block
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): [uniface]

Code Block
;-----------------------------------------------------
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.