Monday, August 19, 2019

Inversion of control framework for Dynamics Ax

 

We all agree that tightly coupled code is bad because is a nightmare to maintain when the application is growing bigger and bigger (and an ax application is always big!). If a class depends on another class, then we need to change one class if something changes in that dependent class. We should always try to write loosely coupled class.

The solution is Dependency injection.

There is a lot of literature on the subject, I suggest you read this post http://dev.goshoom.net/en/2017/05/ioc-containers-for-extensibility/ to familiarise.

The problem, as suggested in the blog above, is that in Microsoft Dynamics Ax we don’t have any dependency injection container and that make dependency injection less appealing.

But I come from c# development where DI is a standard, so I tried to find a solution, might not be perfect, but I taught that sharing with the community my experiment is a good way to get feedback on what I did.

Let’s begin (I suppose you are already familiar with DI before you continue…):

Create an interface

interface EAF_DIF_IServiceClass1

{

}

public void doSomething()

{

}

 

Create a class that implements it

class EAF_DIF_ServiceClass1 implements EAF_DIF_IServiceClass1

{

}

public void doSomething()

{

    info(funcName());

}

 

Create a class where I want to use my implemented interface

abstract class EAF_DIF_Trial1BaseClass

{

   EAF_DIF_IServiceClass1 service1;

}

public void getSomething()

{

   service1.doSomething();

}

 

class EAF_DIF_Trial1Class1 extends EAF_DIF_Trial1BaseClass

{

}

protected void new()

{

    service1 = new EAF_DIF_ServiceClass1();

}

public static EAF_DIF_Trial1Class1 construct()

{

    return new EAF_DIF_Trial1Class1();

}

public static void main(Args _args)

{

   EAF_DIF_Trial1BaseClass cl; //as a sidenote, notice here we respect the Liskov sobstitution principle

   System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();

    System.TimeSpan timeSpan;

    RealBase elapsed;

 

   stopwatch.Start();

 

    cl = EAF_DIF_Trial1Class1::construct();

 

   stopwatch.Stop();

    timeSpan = stopwatch.get_Elapsed();

    elapsed = timeSpan.get_Ticks();

    info(strFmt("Time elapsed: %1", elapsed));

 

   cl.getSomething();

}

 

If I run the class this is what I get:

clip_image001

(I added a stop watch to measure how long it takes to instantiate (here should be the fastest possible as I use the standard new keywork to instantiate my class.)

What did we accomplish so far: not much actually because I didn’t decouple the two classes, as you see I new up the dependent class: service1 = new EAF_DIF_ServiceClass1();

 

Basic dependency injection pattern

Here my first attempt of using dependency injection. I refactored the class to follow the constructor encapsulation pattern to do property injection like this:

class EAF_DIF_Trial1Class2 extends EAF_DIF_Trial1BaseClass

{

    ClassName serviceClassName;

}

protected void new()

{

}

protected static EAF_DIF_Trial1Class2 construct()

{

    return new EAF_DIF_Trial1Class2();

}

protected ClassName parmServiceClassName(ClassName _serviceClassName = serviceClassName)

{

   serviceClassName = _serviceClassName;

 

    return serviceClassName;

}

private void initialize()

{

    service1 = classFactory.createClass(className2Id(serviceClassName));

}

protected static EAF_DIF_Trial1Class2 newUsingServiceClass1(ClassName _serviceClassName)

{

   EAF_DIF_Trial1Class2 cl = EAF_DIF_Trial1Class2::construct();

   cl.parmServiceClassName(_serviceClassName);

   cl.initialize();

   

    return cl;

}

public static void main(Args _args)

{

   EAF_DIF_Trial1BaseClass cl;

   System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();

    System.TimeSpan timeSpan;

    RealBase elapsed;

 

   stopwatch.Start();

 

    cl = EAF_DIF_Trial1Class2::newUsingServiceClass1(classStr(EAF_DIF_ServiceClass1));

 

   stopwatch.Stop();

    timeSpan = stopwatch.get_Elapsed();

    elapsed = timeSpan.get_Ticks();

    info(strFmt("Time elapsed: %1", elapsed));

 

   cl.getSomething();

}

 

Notice the initialize method where I used the classFactory to initialize the class based on the name I pass as parameter on start up.

What did we achieve so far: we effectively decoupled the two classes, but with two major issues.

1)     Where do we pass the class name as parameter? At some point I have to pass the class name. I just deferred the problem to another place. At least now my class is testable.

2)     I pay a performance price. If you look inside the classFactory.createClass method it is using reflection to instantiate our class.

clip_image002

As you see the instantiation took almost double the time it takes when you new up the class regularly.

The difference is quite irrelevant in most cases. Don’t be fooled by the number, these are nanosecond. I had to use the c# stopwatch object otherwise looking at milliseconds only it was showing always only 1 millisecond!

Inversion of control (Ioc) container

I decided to move a step forward and try to create a custom dependency injection container.

Let me show you the end result in this newly refactored class using the IoC container:

class EAF_DIF_Trial1Class3 extends EAF_DIF_Trial1BaseClass

{

}

protected void new()

{

    service1 = EAF_AppClassFactory::getClassInstanceForInterface(classStr(EAF_DIF_IServiceClass1));

}

public static EAF_DIF_Trial1Class3 construct()

{

    return new EAF_DIF_Trial1Class3();

}

public static void main(Args _args)

{

   EAF_DIF_Trial1BaseClass cl;

   System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();

    System.TimeSpan timeSpan;

    RealBase elapsed;

 

   stopwatch.Start();

 

    cl = EAF_DIF_Trial1Class3::construct();

 

   stopwatch.Stop();

    timeSpan = stopwatch.get_Elapsed();

    elapsed = timeSpan.get_Ticks();

    info(strFmt("Time elapsed: %1", elapsed));

 

   cl.getSomething();

}

 

Mission accomplished! We truly decoupled the two classes and we delegate now to our AppClassFactory to figure out which implementation of our interface it should use.

Second accomplishment look the performance:

clip_image003

Is still slower than newing up the object, but I believe that 100 nanoseconds is a price we can pay!

(you can still claim we have a dependency to our AppClassFactory, true… but I consider it acceptable)

 

Ioc Container: AppClassFactory

 

First, I created a table where I set the relation (a bit like \Data Dictionary\Tables\DMFEntity)

I made 3 fields: Container name (so we can have different implementation for the same interface), Interface name, Bind to class name.

clip_image004

I have a primary index on container name and interface name.

clip_image005

public static EAF_IoCcontainer findByInterfaceContainer(

EAF_IoCcontainerInterfaceName    _interfaceName,

EAF_IoCcontainerName _containerName,

boolean                 _forUpdate = false,

ConcurrencyModel        _concurrencyModel = ConcurrencyModel::Auto)

{

   EAF_IoCcontainer IoCcontainer;

 

   IoCcontainer.selectForUpdate(_forUpdate);

    if (_forUpdate && _concurrencyModel != ConcurrencyModel::Auto)

    {

        IoCcontainer.concurrencyModel(_concurrencyModel);

    }

 

    select firstonly IoCcontainer

        where IoCcontainer.InterfaceName == _interfaceName

        &&IoCcontainer.ContainerName == _containerName;

 

    return IoCcontainer;

}

 

The other methods are standard best practice you find in every table.

Here what the table looks like:

clip_image006

(when I have some time I’ll make a form too)

 

Let me show what is inside the EAF_AppClassFactory.

(You will notice that I followed the example in the extension framework \Classes\SysExtensionAppClassFactory\getClassFromSysExtAttribute)

class EAF_AppClassFactory

{

}

protected void new()

{

}

protected static EAF_AppClassFactory construct()

{

    return new EAF_AppClassFactory();

}

public static Object getClassInstanceForInterface(

        EAF_IoCcontainerInterfaceName _interfaceName,

        EAF_IoCcontainerClassName _iocContainerName = ''

        )

{

    Object classInstance;

    System.Object sysObject;

    str cacheKey;

    SysGlobalCache globalCache = classFactory.globalCache();

    str myFuncName = funcName();

   EAF_IoCcontainerClassName className;

    ClassId interfaceId, classId;

    SysDictClass sysDictClass;

 

    cacheKey = myFuncName+_iocContainerName+_interfaceName;

 

    classInstance = globalCache.get(myFuncName, cacheKey, null);

 

    if (classInstance)

    {

        return classInstance;

    }

 

    if (!globalCache.isSet(myFuncName, cacheKey))

    {

        className = EAF_IoCcontainer::findByInterfaceContainer(_interfaceName, _iocContainerName).BindToClassName;

        classId = className2Id(className);

        interfaceId = className2Id(_interfaceName);

        sysDictClass = new SysDictClass(classId);

 

        if (!sysDictClass)

        {

            throw error(strFmt('Error ZAIFJ6BD88: could not find the class to bind to interface %1 in container "%2"', _interfaceName, _iocContainerName));

        }

        if (!sysDictClass.isImplementing(interfaceId))

        {

            throw error(strFmt('Error BD70ID4IHC: class %1 does not extend %2 (check in container "%3")', className, _interfaceName, _iocContainerName));

        }

 

        classInstance = classFactory.createClass(classId);

        globalCache.set(myFuncName, cacheKey, classInstance);

    }

 

    if (!classInstance)

        throw error(strFmt('Error VMRHB3Z2UO: could not implement the interface %1', _interfaceName));

 

    return classInstance;

}

 

The advantage of using a table to bind the interface to its implementation is that you can switch the implementation even in production without doing a deployment.

Alternatively you can use a singleton class, I will post an example in another post.

Any comments are welcome.

2 comments:

  1. Are you aware of SysExtension framework in AX 2012 and plugins in D365FO (which I mentioned in my blog post)?
    They don't come with a configuration table as your solution (they assume you'll use parameter tables of particular business areas), but that's just one extra layer on the top - everything else (e.g. a class factory) is already there and doesn't have to developed again.

    The usage is the same; whether I call EAF_AppClassFactory::getClassInstanceForInterface() or SysPluginFactory::instance() doesn't make much difference. I would argue that both are examples of dependency lookup and not dependency injection.

    The fact that everything must be set up up-front is exactly what I complained about in case of plugins and why I dreamed about an IoC containers. In my example, I didn't have to design the extensibility point in advance and write code calling a factory; I merely declared a property and used it. I didn't have to think about that somebody will want to extend my solution, which is preciselly what happened with all the legacy code in AX. That was the point of my blog post - although we have plugins and they help us with a better design of new code, they're still cumbersome and they can't help with legacy code. IoC containers would be a solution in many cases.

    ReplyDelete
    Replies
    1. Yes, I agree this is a dependency lookup, but still better than nothing.

      Regarding the complain that everything must be set up up-front, I’m not sure I understood it correctly.

      I could not try the plugins yet because I’m still on 2012, but they seem to work similarly:

      SysPluginFactory::Instance("Dynamics.AX.Application", classStr(GeneralLedgerIExtension), metadataCollection);

      But yes, you need to create the key value pair in the attribute class, so if you want to change implementation you need to redeploy, while the beauty of IoC is what you said “IoC containers even allow setting type mapping in configuration files, therefore you can easily change type to use without touching code at all.”

      if I understood it correctly, maybe my solution has the advantage that you can switch the implementation at any time from the configuration table without touching the code.

      Well I hope this discussion will inspire more developers and at some point we will have something efficient also in dynamics as we have in C#

      Delete