Statements are grouped into
methods, and methods into classes, tables, and other model element
types. These structures enable you to inspect X++ code on higher levels
of abstractions. The following example shows how an assignment operation
can be encapsulated into a method to raise the level of abstraction.
is on a higher level of abstraction than
Patterns enable
developers to communicate their solutions more effectively and to reuse
proven solutions to common problems. Using patterns offers readers of
the source code the benefit of quickly understanding the purpose of a
particular implementation. You should always bear in mind that even as a code author you spend more time reading source code than writing it.
Implementations
of patterns are typically recognizable by the names used for classes,
methods, parameters, and variables. Arguably, naming these elements so
that they effectively convey the intention of the code is the
developer’s most difficult task. A lot of the information in the
existing literature on design patterns pertains to object-oriented
languages, and you can benefit from exploring that information to find
patterns and techniques you can apply when you’re writing X++ code.
Design patterns express relationships or interactions between several
classes or objects. They don’t prescribe a specific implementation, but
they do offer a template solution for a typical design problem. In
contrast, implementation patterns are implementation specific and can
have a scope that spans only a single statement.
This section
highlights some of the most frequently used patterns specific to X++.
More descriptions are available in the Microsoft Dynamics AX SDK on
MSDN.
Class-Level Patterns
These patterns apply to classes in X++.
Parameter Methods
To set and get a class
field from outside the class, you should implement a parameter method.
The parameter method should have the same name as the field and be
prefixed with parm. Parameter methods come in two flavors: get-only and get/set.
public class Employee { EmployeeName name;
public EmployeeName parmName(EmployeeName _name = name) { name = _name; return name; } }
|
Constructor Encapsulation
The purpose of the constructor encapsulation pattern is to enable Liskov’s class substitution principle.
In other words, constructor encapsulation lets you replace an existing
class with a customized class without using the layering system. Just as
in the layering system, this pattern enables changing the logic in a
class without having to update any references to the class. Be careful to avoid overlayering because it often causes upgrade conflicts.
Classes that have a static construct method are following the constructor encapsulation pattern. The construct method should instantiate the class and immediately return the instance. The construct method must be static and shouldn’t take any parameters.
When parameters are required, you should implement the static new methods. These methods call the construct method to instantiate the class and then call the parameter methods to set the parameters; in this case, the construct method should be private.
public class Employee { ... protected void new() { }
protected static Employee construct() { return new Employee(); }
public static Employee newName(EmployeeName name) { Employee employee = Employee::construct(); ; employee.parmName(name); return employee; } }
|
Serialization with the pack and unpack Methods
Many classes
require the ability to serialize and deserialize themselves.
Serialization is an operation that extracts an object’s state into
value-type data; deserialization creates an instance from that data.
X++ classes implementing the Packable interface support serialization. The Packable interface contains two methods: pack and unpack. The pack method returns a container with the object’s state; the Unpack
method takes a container as a parameter and sets the object’s state
accordingly. You should include a versioning number as the first entry
in the container to make the code resilient to old packed data stored in
the database when the implementation changes.
public class Employee implements SysPackable { EmployeeName name; #define.currentVersion(1) #localmacro.CurrentList name #endmacro ...
public container pack() { return [#currentVersion, #currentList); }
public boolean unpack(container packedClass) { Version version = runbase::getVersion(packedClass);
switch (version) { case #CurrentVersion: [version, #CurrentList] = packedClass; break; default: //The version number is unsupported return false; } return true; } }
|
Observers and Listeners
The Observer/Listener
design pattern is used in many languages. It is particularly useful in
X++ because X++ doesn’t support an event concept. This pattern enables a
class to subscribe to an event and to be invoked when the event occurs.
Classes and interfaces that have “Observer” or “Listener” as part of
their names are implementations of the Observer/Listener pattern.
Here is an implementation of the Observer pattern. In this example, an Observer class is assumed to exist and have a public invoke method.
List observers = new List(Types::Class);
public void registerObserver(object observer) { observers.addEnd(observer); } private void invokeObservers() { ListEnumerator listEnum = observers.getEnumerator(); Object observer;
while (listEnum.moveNext()) { observer = listEnum.current(); //Call invoke() on all observers observer.invoke(); } }
|