Blogs

Dependency Injection in Uniface

By Uniface Test posted 06-26-2013 06:00

  

(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:

  1. 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.
  2. 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:

  1. 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.
  2. 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.

14 comments
4 views

Comments

05-10-2019 08:00

The call to $configHandle$->getOfficeCode will fail, the retrieve will fetch half the database and crash the server! (Maybe). It needs error handling that I've omitted for clarity, ideally it would drop out on the call to $configHandle$ with a sensible error message. It would also be a matter of developer education, they would need to request handles from the DI container or factory rather than trying to create their own instances of them.

05-10-2019 08:00

On your final example which does not have an INIT any more, but there is a need to start some SETUP: what happens, if some consumer of your service decides to just a plain activate "DATA_SVC".getSomeData ?

05-10-2019 08:00

Hi Uli, I agree, application partitioning is not for people with easily upset stomachs (as we'd say in Norwegian)... The web based paradigm really highlights the stateless nature of application development - and I'm not sure (from a purist perspective) we can ever accomplish proper 'dependency injection'.
Transaction boundaries, data harvesting (collecting data - any data - because sometime in the furture we'll find a way to mine it), aborted shopping carts - why did the customer jump? - these are questions that didn't exist in the good, old C/S days.... Today they do - and many more questions.... Most of these questions 'breaks' the clean-cut business process rules we used to have... ==> "customer doesn't complete order, roll back and remove ....."
A newinstance statement on its own - well, sync/async? attached? transaction?
Frameworks will eventually become too complex, too complicated and so darn expensive to use (development cost, maintenance cost and finally educationally when bringing on new developers)...

05-10-2019 08:00

Initially - none other than passing a handle around...   then,  having a design
where Uniface is called externally by a web service (SOAP / REST) or
integration with a TP monitor (haven't checked the PAM if we still have a
Tuxedo ish implementation) - where is transaction boundaries defined etc etc.
I'm not arguing against any particular framework / paradigm - but one
size doesn't fit all...
So, I'm in favour of designs based on the past experiences / knowledge,
current requirements and with an eye to the future to have some sort of hope that
whatever I develop today can be used tomorrow without a complete refactoring
process.. And then I wake up and smell the coffee.... :-)

05-10-2019 08:00

May I recommend: Lars Vogel: Eclipse 4 Application Development ISBN 9783943747034? It has a detailed description of DI in the e4 ecosystem.

05-10-2019 08:00

Thanks for the feedback. In DI terms the thing that's responsible for configuring other objects would be called a container or injector service. However, in this simple example that's a bit of an overly grand term so I went with factory because that's really all it's doing. I'm mixing my metaphors a little bit perhaps. On a similar note, there's also a lot of interesting discussion out there about DI vs. Service Locator patterns and I think my example falls into this trap a little as well.
 
You can see the loose coupling and dependency injection in the final version of DATA_SVC. It's now loosely coupled with the config provider services because it no longer creates its own concrete instance of one, instead it's injected with one externally. This is actually very similar to the examples presented in the Wikipedia article, "Highly coupled dependency" being our starting point and "Manually injected dependency" being what we end up with. The only difference is that we need to use a setup operation instead of a constructor (and I use a "factory" instead of leaving all the injection code in the "Main" method).
 
Thanks for the reference to the DI features of Eclipse e4, I've looked at things like Google Guice and PHP-DI but haven't come across the Eclipse one before. Exploring how you might scale out this sort of approach was a bit out of scope for this blog post but it's something I'd like to look into in the future. For instance, whether to use annotations or XML configuration and how a proper injector service might work with those to setup the application components.

05-10-2019 08:00

In your example, you use: selectcase $logical("CONFIG_PROVIDER" ,so you will use always the same component.
You call it a factory service, "Dependency Injection" is something completely different.
When I read "Dependency Injection" on Wikipedia, I see a mechanism which is very different from the one your code is doing. I can not see "loose coupling" in the same way as DI is working in a eclipse e4 based application.

05-10-2019 08:00

Hi Knut, application partitioning is a very serious topic. It has a lot of important points like pointers, instances and names which do not exist if you offload a couple of processing onto a remote location. What is possible is a "chain-of-commands" where you start on a toplevel service which passes your request to the appropriate next-level service ... I used this mechanism based on instancenames and with listst as arguments in the past (after encountered that  fixed instancenames may have some strange behaviour if the nexinstance is executed in a remote realm). Pointers and Structs may cause another level of danger into the "let's partition our running application" approach.

05-10-2019 08:00

the previous reply should be for James' reply on my ulrich-merkel June 26, 2013 at 8:38 am comment. Some magic on this application here shifted it away (and eat up my layout)

05-10-2019 08:00


In your example, you use: selectcase $logical("CONFIG_PROVIDER"), so you will use always the same component.
You call it a factory service, "Dependency Injection" is something completely different.
When I read "Dependency Injection" on Wikipedia, I see a mechanism which is very different from the one your code is doing. I can not see "loose coupling" in the same way as DI is working in a eclipse e4 based application.

05-10-2019 08:00

That's a very good point. The simple example above doesn't even attempt to address that problem, and certainly if DATA_SVC was to run remotely you'd have issues. It's an interesting one that I'm going to have to have a think about, I'm sure it should be possible to design a DI framework that was aware that a service may run remotely and take appropriate steps to make it work.
What do you mean by 'one-handle-fits-all'? Is there anything in particular you see as a big risk?

05-10-2019 08:00

There's nothing new under the sun is there! I'd say this is a great example where Uniface gives us some powerful functionality for free that is going to work well in a lot of cases. The startup shell effectively becomes our DI container, sets everything up for us and makes it available to our application.
However, what if we needed to use 2 different versions of these services for some reason (say to present to the user legal matters for Bavaria and Norway)? Or if the service was particularly stateful it might be nice to have 2 instances of the same one going at the same time. In these cases we'd need more functionality than accessing through a single instance name of "legalmatters" could provide. Having a factory service that was capable of configuring and supplying instances for us on demand might then have more value.

05-10-2019 08:00

This approach will cause some serious headaches the very moment application partitioning is introduced.
The design paradigm needs to resolve "should / could this be run in multiple, distributed servers?".
Relying on 'one-handle-fits-all' is, imho, simply too dangerous....

05-10-2019 08:00

looks like the very old practice to start a goup of services in the startup shell with a fixed instancename where you have some selection which uniface component you use in the newinstancee. So the service with the instance name "lagalmatters" can hold the code of the component "legalmatters_bavaria", "legalmatters_norway" or whatever. All consumers just use the activate "legalmatters".