Monday, August 19, 2019

Dynamics Ax SysExtension framework to decouple a display method and avoid if

A simple practical example how the SysExtension framework will rescue us to adhere to SOLID principles and make our code testable:

We have a table that has 4 fields:

clip_image001

1)     GrossValue (Real, extends RealBase)

2)     NetValue (Real, extends RealBase)

3)     Target (Real, extends RealBase)

4)     Type (Enum)

The enum  Type has two elements

clip_image002

Now the requirement is to create a display method, contractValue, that according to the type it does a different calculation:

So you could simply create your method like this:

[SysClientCacheDataMethodAttribute(true)]

public display Total contractValue()

{

    Total value;

 

    if (this.Type == EF_EnumsForSysExtFramworkDemo1::Type1)

        value = this.GrossValue * this.Target;

 

    if (this.Type == EF_EnumsForSysExtFramworkDemo1::Type2)

        value = this.NetValue * this.Target;

 

    return value;

}

 

Can you guess what is the problem with this method?

·        Brilliant!! Yes you are right, too many ‘If’ clauses are there and if we want to introduce another new type, then you need to write another ‘if’. In other words, it violates the Open closed principle (OCP): a class should be open for extension but closed for modification.

·        Second issue this method can’t be properly unit tested!

·        It violates the single responsibility principle

If you work on a small project you can simply ignore these points and claim and that you need a quick simple solution to finish your work quickly and go home early, but if you work on a large enterprise project you as developer have the responsibility to deliver good quality code and Dynamics is by definition an enterprise project. So, let’s see how we can improve this code and make our code SOLID and testable.

So how can you do it?

Use the sys Extension Framework!
there are other ways to achieve it (for example using the strategy pattern https://refactoring.guru/design-patterns/strategy/csharp/example) but I prefer to use ax built in tools as it also caches and is much faster (see this article to know more https://blogs.msdn.microsoft.com/mfp/2014/05/08/x-pedal-to-the-metal/).

Create an attribute class that extends SysAttribute

 

class EF_SysExtFramworkDemo1Attribute extends SysAttribute

{

   EF_EnumsForSysExtFramworkDemo1 types; //the enum for types

}

 

 

public void new(EF_EnumsForSysExtFramworkDemo1 _types)

{

    super();

    types = _types;

}

 

public EF_EnumsForSysExtFramworkDemo1 parmTypes(EF_EnumsForSysExtFramworkDemo1 _types = types)

{

    types = _types;

 

    return types;

}

 

Create an Abstract base class

 

abstract class EF_SysExtFramworkDemo1BaseClass

{

   EF_TableForSysExtFramworkDemo1 tableBuffer;

}

protected void initTableBuffer(EF_TableForSysExtFramworkDemo1 _tableBuffer)

{

    tableBuffer = _tableBuffer;

}

abstract public void getContractValue()

{

}

 

Create the derived classes for the different type options

 

[EF_SysExtFramworkDemo1Attribute(EF_EnumsForSysExtFramworkDemo1::Type1)]

class EF_SysExtFramworkDemo1Type1 extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return tableBuffer.GrossValue * tableBuffer.Target;

}

 

[EF_SysExtFramworkDemo1Attribute(EF_EnumsForSysExtFramworkDemo1::Type2)]

class EF_SysExtFramworkDemo1Type2 extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return tableBuffer.NetValue * tableBuffer.Target;

}

 

Create a factory class

 

class EF_SysExtFramworkDemo1Factory extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return 0;

}

public Total getContractValue()

{

    return 0;//default value

}

public static EF_SysExtFramworkDemo1Factory construct(EF_EnumsForSysExtFramworkDemo1 _type, Common _tableBuffer)

{

   EF_SysExtFramworkDemo1Attribute attr;

   EF_SysExtFramworkDemo1BaseClass baseCl;

    ClassName baseClName = classStr(EF_SysExtFramworkDemo1BaseClass);

 

    attr = new EF_SysExtFramworkDemo1Attribute(_type);

    baseCl = SysExtensionAppClassFactory::getClassFromSysAttribute(baseClName, attr);

 

    if (!baseCl)

    {

        baseCl = new EF_SysExtFramworkDemo1Factory(); //so it will use the default getContractValue in this class

 

        //OR just throw an error

        //throw error(Error::wrongUseOfFunction(funcName()));

    }

 

   baseCl.initTableBuffer(_tableBuffer);

 

    return baseCl;

}

 

Update the display method on the table

 

[SysClientCacheDataMethodAttribute(true)]

public display Total contractValue2()

{

    Total value;

 

   EF_SysExtFramworkDemo1Factory cl;

    cl = EF_SysExtFramworkDemo1Factory::construct(this.Type, this);

 

    value = cl.getContractValue();

 

    return value;

}

 

 

Now we have achieved two important goals

1)     If we add a new type that will require a different calculation, we don’t have to touch anywhere our code (closed for modification). We simply have to create a new derived class (open for extension)

[EF_SysExtFramworkDemo1Attribute(EF_EnumsForSysExtFramworkDemo1::Type3)]

class EF_SysExtFramworkDemo1Type3 extends EF_SysExtFramworkDemo1BaseClass

{

}

public Total getContractValue()

{

    return ….;

}

 

2)     We can now unit test

 

To complete the picture, we can create the Unit test:

class EF_SysExtFramworkDemo1Type1Test extends SysTestCase

{

}

[SysTestCheckInTestAttribute]

public void testType1()

{

    // Arrange

   EF_SysExtFramworkDemo1BaseClass cl;

   EF_TableForSysExtFramworkDemo1 table;

    Total actualTotal, expectedTotal;

 

    // Act

   table.GrossValue = 100;

    table.Target = 2;

   

    expectedTotal = 200;

   

    cl = EF_SysExtFramworkDemo1Factory::construct(EF_EnumsForSysExtFramworkDemo1::Type1, table);

    actualTotal = cl.getContractValue();

 

    // Assert

   this.assertEquals(expectedTotal, actualTotal);

}

[SysTestCheckInTestAttribute]

public void testType2()

{

    // Arrange

   EF_SysExtFramworkDemo1BaseClass cl;

   EF_TableForSysExtFramworkDemo1 table;

    Total actualTotal, expectedTotal;

 

    // Act

    table.NetValue = 100;

    table.Target = 2;

   

    expectedTotal = 200;

   

    cl = EF_SysExtFramworkDemo1Factory::construct(EF_EnumsForSysExtFramworkDemo1::Type2, table);

    actualTotal = cl.getContractValue();

 

    // Assert

   this.assertEquals(expectedTotal, actualTotal);

}

 

To run the test you can just now just right click on the class, AddIns -> Run Tests.

But I usually create also a main method to the test class so that I can just run the class and even add a menu item to run my test

public static void main(Args _args)

{

    ListEnumerator msgs;

    SysTestSuite suite = new SysTestSuite(classstr(EF_SysExtFramworkDemo1Type1Test));

    //use this to run in transaction isolation

    //SysTestSuite suite = new SysTestSuiteTTS(classstr(EF_SysExtFramworkDemo1Type1Test));

 

    SysTestResult result = new SysTestResult();

 

   result.addListener(new SysTestListenerPrint());

   result.addListener(new SysTestListenerDB());

   result.addListener(new SysTestListenerInfolog());

   result.parmCodeCoverageEnabled(true);

 

    suite.run(result);

 

   info(result.getSummary());

 

    msgs = result.getMessages().getEnumerator();

    while (msgs.moveNext())

    {

        error(strFmt('%1', msgs.current()));

    }

}

 

clip_image003

 

One side note, The extension framework cache so every time you do some changes during development cleae the cache :

SysExtensionCache::clearAllScopes();

Project

 

Here is how your project might look like:

clip_image004

 

I know what you think, 6 classes, lot of complexity to replace 4 lines of code!!

Yes, at first glance seems a non-sense, but believe me (actually not me but all the community), writing solid extensible, testable code is the only way to make withstand the changes and the new requirements which always come sooner or later. And the ability to just extend and simply add new classes without touching the existing code is so much powerful that worth all the effort.

Once you get used is not so much a time to write your code properly and you will start to realize that a code that is not backed by unit tests is simply Bad Code.

No comments:

Post a Comment