Background
At the Dynamics 365 for Operations Technical Conference earlier this week, Microsoft announced its plans around overlayering going forward. If you have not heard it yet, here's the tweet I posted on it:The direct impact of this change is that we should stop using certain patterns when writing new X++ code.#Dyn365Tech AppSuite will be "soft sealed" in Fall 2017 release and "hard sealed" in Spring 2018 - move to use #Extensions in your solutions— Ivan Kashperuk (@IvanKashperuk) March 14, 2017
Pattern to avoid
One of these patterns is the implementation of factory methods through a switch block, where depending on an enumeration value (another typical example is table ID) the corresponding sub-class is returned.First off, it's coupling the base class too tightly with the sub-classes, which it should not be aware of at all.
Secondly, because the application model where the base class is declared might be sealed (e.g, foundation models are already sealed), you would not be able to add additional cases to the switch block, basically locking the application up for any extension scenarios.
So, that's all good and fine, but what can and should we do instead?
Pattern to use
The SysExtension framework is one of the ways of solving this erroneous factory method pattern implementation.
This has been described in a number of posts already, so I won't repeat here. Please read the below posts instead, if you are unfamiliar with how SysExtension can be used:
- https://blogs.msdn.microsoft.com/mfp/2013/06/12/sysextension-framework-to-the-rescue/
- https://blogs.msdn.microsoft.com/ax_gfm_framework_team_blog/2012/11/13/the-microsoft-dynamics-ax-2012-extension-framework-part-1/
Problem statement
In many cases in existing X++ code, the constructor of the class (the new() method) takes one or more arguments. In this case you cannot simply use the SysExtension framework methods described above.
Here's an artificial example I created to demonstrate this:
- A base class that takes one string argument in the constructor. This could also be abstract in many cases.
- Two derived classes that need to be instantiated depending on the value of a NoYesUnchanged enum passed into the construct() method.
public class BaseClassWithArgInConstructor { public str argument; public void new(str _argument) { argument = _argument; } public static BaseClassWithArgInConstructor construct(NoYesUnchanged _factoryType, str _argument) { // Typical implementation of a construct method switch (_factoryType) { case NoYesUnchanged::No: return new DerivedClassWithArgInConstructor_No(_argument); case NoYesUnchanged::Yes: return new DerivedClassWithArgInConstructor_Yes(_argument); } return new BaseClassWithArgInConstructor(_argument); } }
public class DerivedClassWithArgInConstructor_No extends BaseClassWithArgInConstructor { }
public class DerivedClassWithArgInConstructor_Yes extends BaseClassWithArgInConstructor { }
And here's a Runnable class we will use to test our factory method:
class TestInstantiateClassWithArgInConstructor { public static void main(Args _args) { BaseClassWithArgInConstructor instance = BaseClassWithArgInConstructor::construct(NoYesUnchanged::Yes, "someValue"); setPrefix("Basic implementation with switch block"); info(classId2Name(classIdGet(instance))); info(instance.argument); } }
Running this now would produce the following result:
OK, so to decouple the classes declared above, I created a "factory" attribute, which takes a NoYesUnchanged enum value as input.
The right derived class with the correct argument value returned |
public class NoYesUnchangedFactoryAttribute extends SysAttribute { NoYesUnchanged noYesUnchangedValue; public void new(NoYesUnchanged _noYesUnchangedValue) { noYesUnchangedValue = _noYesUnchangedValue; } public NoYesUnchanged parmNoYesUnchangedValue() { return noYesUnchangedValue; } }
Let's now decorate the two derived classes and modify the construct() on the base class to be based on the SysExtension framework instead of the switch block:
[NoYesUnchangedFactoryAttribute(NoYesUnchanged::No)] public class DerivedClassWithArgInConstructor_No extends BaseClassWithArgInConstructor { }
[NoYesUnchangedFactoryAttribute(NoYesUnchanged::Yes)] public class DerivedClassWithArgInConstructor_Yes extends BaseClassWithArgInConstructor { }
public class BaseClassWithArgInConstructor { // ... public static BaseClassWithArgInConstructor construct(NoYesUnchanged _factoryType, str _argument) { NoYesUnchangedFactoryAttribute attr = new NoYesUnchangedFactoryAttribute(_factoryType); BaseClassWithArgInConstructor instance = SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(BaseClassWithArgInConstructor), attr); return instance; } }
Running the test now will however not produce the expected result:
The right derived class is returned but argument is missing |
Solution
In order to account for the constructor arguments we need to use an Instantiation strategy, which can then be passed in as the 3rd argument when calling SysExtensionAppClassFactory.
Let's define that strategy class:
public class InstantiationStrategyForClassWithArg extends SysExtAppClassDefaultInstantiation { str arg; public anytype instantiate(SysExtModelElement _element) { SysExtModelElementApp appElement = _element as SysExtModelElementApp; Object instance; if (appElement) { SysDictClass dictClass = SysDictClass::newName(appElement.parmAppName()); if (dictClass) { instance = dictClass.makeObject(arg); } } return instance; } protected void new(str _arg) { this.arg = _arg; } public static InstantiationStrategyForClassWithArg construct(str _arg) { return new InstantiationStrategyForClassWithArg(_arg); } }
As you can see above, we had to
- Define a class extending from SysExtAppClassDefaultInstantiation (it's unfortunate that it's not an interface instead).
- Declare all of the arguments needed by the corresponding class we plan to construct.
- Override the instantiate() method, which is being invoked by the SysExtension framework when the times comes
- In there we create the new object instance of the appElement and, if necessary, pass in any additional arguments, in our case, arg.
Let's now use that in our construct() method:
public class BaseClassWithArgInConstructor { //... public static BaseClassWithArgInConstructor construct(NoYesUnchanged _factoryType, str _argument) { NoYesUnchangedFactoryAttribute attr = new NoYesUnchangedFactoryAttribute(_factoryType); BaseClassWithArgInConstructor instance = SysExtensionAppClassFactory::getClassFromSysAttribute( classStr(BaseClassWithArgInConstructor), attr, InstantiationStrategyForClassWithArg::construct(_argument)); return instance; } }
If you modify the attributes/hierarchy after the initial implementation, you might need to clear the cache, and restarting IIS is not enough, since the cache is also persisted to the DB. You can do that by invoking the below static method:
SysExtensionCache::clearAllScopes();
Parting note
There is a problem with the solution described above. The problem is performance.
I will walk you through it, as well as the solution, in my next post.
No comments:
Post a Comment
Please don't forget to leave your contact details if you expect a reply from me. Thank you