X++ Decorator pattern in Dynamics 365
A couple of times in my career I stumbled upon a development requirement to perform a set of loosely connected actions with a similar interface upon a certain business object depending on parameters or conditions:
- Sometimes do this
- Sometimes do that
- Sometimes do this and that
- In future, you may do something else in addition.
In Dynamics 365 for Finance and Operations, this usually results in a hierarchy of classes. The classes are typically bound to some parameter table containing checkboxes or – better – an enumeration field. This enumeration directs the SysExtension class factory which objects to instantiate.
A simple while_select loop traverses the parameter table and executes the instantiated objects one by one. However, if the classes implement more than one action i.e. method, this external loop may become repetitive. I ended up with a certain design pattern which turned out to have a name in object-oriented programming: a Decorator an internal iterator.
Errata: as readers pointed out, there is more of a Decorator pattern in this, than an Iterator pattern.
The constructor instantiates an object and gives it the previous object as a parameter, thus forming a linked chain of objects. The external actor only needs to take one parameter – the topmost object in the chain – and to call one operation .doThis() upon it. This .doThis() method is exclusively implemented in the parent class of the hierarchy, and the parent class knows how to iteratively call the objects.
Adding a new grid with a parameter table to an existing form is more difficult than adding new field(s) into an existing table via extensions. If the number of possible actions is countable and small, you may think of de-normalizing the parameter table and making an array field with possible options in one record. Instead of iterating records, the constructor will be iterating fields.
Below is an application of this pattern to a simple task: calculation of the nutrition value of one meal, where a meal may consist of an Entrée, an Entrée and a Main course, a Main course and a Dessert and so on:
class MealCourseAtrribute extends SysAttribute implements SysExtensionIAttribute
{
MealCourseEnum mealCourseEnum;
public void new(MealCourseEnum _mealCourseEnum)
{
super();
mealCourseEnum = _mealCourseEnum;
}
public str parmCacheKey()
{
return classStr(MealCourseAtrribute) + enum2Symbol(enumNum(MealCourseEnum), mealCourseEnum);
}
public boolean useSingleton()
{
return true;
}
}
abstract public class MealCourse
{
private MealCourse prevMealCourse;
abstract protected MealKcal kcal()
{
}
final public MealKcal kcalTotal()
{
MealKcal ret = this.kcal();
if (prevMealCourse)
{
ret += prevMealCourse.kcalTotal();
}
return ret;
}
private MealCourse prevMealCourse(MealCourse _prevMealCourse = prevMealCourse)
{
prevMealCourse = _prevMealCourse;
return prevMealCourse;
}
protected void new()
{
}
public static MealCourse construct(Meal _meal)
{
MealCourse mealCourse, prevMealCourse;
MealCourseAtrribute attr;
MealCourseEnum mealCourseEnum;
int i;
for (i = 1; i <= dimOf(_meal); i++)
{
mealCourseEnum = _meal[i];
attr = new MealCourseAtrribute(mealCourseEnum);
mealCourse = SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(MealCourse), attr);
if (prevMealCourse)
{
if (mealCourseEnum == MealCourseEnum::None)
{
continue;
}
mealCourse.prevMealCourse(prevMealCourse);
}
prevMealCourse = mealCourse;
}
return prevMealCourse;
}
public static void main(Args _args)
{
info(strFmt("Total calories in this meal is %1", MealParameters::find().meal().kcalTotal()));
}
}
[MealCourseAtrribute(MealCourseEnum::Entree)]
public class MealCourseEntree extends MealCourse
{
public MealKcal kcal()
{
return 140;
}
}
[MealCourseAtrribute(MealCourseEnum::Main)]
public class MealCourseMain extends MealCourse
{
public MealKcal kcal()
{
return 600;
}
}
[MealCourseAtrribute(MealCourseEnum::Dessert)]
public class MealCourseDessert extends MealCourse
{
public MealKcal kcal()
{
return 200;
}
}
Dynamics 365 FO may return no records for a certain table, but it cannot return an NULL value for a field; in addition, I would like to spare the caller an if (! parameter) {return}; validation. This is why the caller is always given at least one object of the MainCourseNone type which returns a zero and does nothing. The API then reduces to a one-liner:… = MealParameters::find().meal().kcalTotal();
here for a main course and a dessert:
Adding a new meal course type e.g. a soup is as simple as adding a new enumeration element to the MealCourseEnum, adding a new array element to the Meal extended data type (but only if the Soup may be ordered in addition to the other courses), and implementing a class tagged with this enumeration element as an attribute:
[MealCourseAtrribute(MealCourseEnum::Soup)]
public class MealCourseSoup extends MealCourse
{
public MealKcal kcal()
{
return 100;
}
}
Beware of the current limitations in the array fields support in D365FO: they cannot be exposed in Excel or imported properly in the Data management module.
You may download the source code here: TheMealProject
X++ programming blog series
Further reading:
Print a custom product label: a Template solution in Process Guide Framework, with detours
Extending SysOperation contracts with DataMemberAttribute
X++ Decorator pattern in Dynamics 365
Get a cost center in D365FO
Find and merge a DimensionAttributeValue
SysExtension framework pitfall: avoid new()
Input validation and messaging in the Process Guide Framework