Null Object Pattern in NAV and Business Central.
Jalmaraz
667
Context.
This pattern shows the application of a OOP pattern in NAV. This is a classic pattern, created by the gang of four: https://en.wikipedia.org/wiki/Null_object_pattern .
Before pattern explanation, is important to set some equivalences between the classic pattern and NAV programming. Many people have set these concordances between NAV and OOP objects before:
- Object=NAV Record.
- Null=Record with blank key value code and doesn´t exist in the table.
- Property=NAV table field.
This pattern is about of what to do when an object is not found: we create and empty(null) object and set a default value of his properties.
Most times could be so simple than this:
if not Customer.get(NoPassed) then
Customer.Init();
But other times is more complex and interesting:
- Currency table: In currency table, when a currency code is blank, the default values are not zero or blank: are the default values of G/L Setup rounding precision fields.
- Sales Header: When the shipping address code is blank, the values of "ship-to" fields are not blank: they are the customer card fields.
- Item table. "Prevent negative inventory" field. Another example of a field that his initial value is not blank: if its default vale is not filled, takes the value of "Prevent negative inventory" in "Inventory Setup" table.
- Language Windows ID in Language table, if settled to blank code must be GLOBALLENGAGE.
Problem.
When we handle with empty values in codes related to tables, we have two issues:
Force 1. Increase cyclomatic complexity
We can be forced to continuously check if the value is empty and do the subsequent actions. The code could become complex. When Robert Martin created this pattern, the goal was to avoid a permanent check of the null value, with conditional statements.
Try to imagine this: if currency shouldn´t give us an empty value for his precision fields we are forced to check in every amount rounding if currency is blank:
if Currency.Code = '' then begin
GLSetup.GET;
Amount := ROUND(Amount, GLSetup."Amount Rounding Precision");
end else begin
Amount := ROUND(Amount, Currency."Amount Rounding Precision");
end;
This is an unnecessary complexity, but in NAV you don´t have to do this. Currency set the default values of general setup when the code is blank.
Imagine this increase of conditional statements in tables as "Sales Header", "Sales Line" or Sales post Codeunit. The Code could be unreadable.
Let´s see another example: Sales Header. If Ship-to Code field was empty and Sales Header shouldn´t give us a default value we were forced to check the ship-to this way:
if SalesHeader."Ship-to Code" = '' then
FormatAddr.SalesHeaderSellTo(AddressArray, SalesHeader)
ELSE
FormatAddr.SalesHeaderShipTo(AddressArray,AddressArray2, SalesHeader);
Is another thing we don´t have to do because, when Ship code is empty, Sales Header move default address data of customer to the "Ship-to" fields.
Force 2. Object consumer decide null behavior.
Every time we found an empty value, we will take the decision of what to do with the related record.
We don´t have to leave people that use your object the decision of what to do with the blank code. If the empty state is more complex than a blank or zero, give this business logic in the table, because consumer could take the wrong decision.
Solution.
When we create a new table and its empty behavior is more complex than a “INIT” clause, we must do:
- Create a new global function Initialize(RecordIdentificationCode).
- When using this Object we use must use Initialize(Code) new function instead the GET(Code) statement. So, we don´t have to check the blank code anymore.
Usage. Currency Code.
NAV already makes an outstanding use of this pattern.
Example table 4 “Currency”.
Step 1. Create a new function Initialize(<RecordName>Code)
procedure Initialize(CurrencyCode : Code[20])
begin
IF CurrencyCode <> '' THEN
Get(CurrencyCode)
ELSE
InitRoundingPrecision;
end;
It´s very interesting what it´s doing “InitRoundingPrecision”:
procedure InitRoundingPrecision()
begin
GLSetup.GET;
IF GLSetup."Amount Rounding Precision" <> 0 THEN
"Amount Rounding Precision" := GLSetup."Amount Rounding Precision"
ELSE
"Amount Rounding Precision" := 0.01;
IF GLSetup."Unit-Amount Rounding Precision" <> 0 THEN
"Unit-Amount Rounding Precision" := GLSetup."Unit-Amount Rounding Precision"
ELSE
"Unit-Amount Rounding Precision" := 0.00001;
"Max. VAT Difference Allowed" := GLSetup."Max. VAT Difference Allowed";
"VAT Rounding Type" := GLSetup."VAT Rounding Type";
"Invoice Rounding Precision" := GLSetup."Inv. Rounding Precision (LCY)";
"Invoice Rounding Type" := GLSetup."Inv. Rounding Type (LCY)";
end;
"Invoice Rounding Type" := GLSetup."Inv. Rounding Type (LCY)";
Step 2. Consume this function.
Let´s see table 37, function GetSalesHeader:
procedure GetSalesHeader()
begin
TESTFIELD("Document No.");
IF ("Document Type" <> SalesHeader."Document Type") OR ("Document No." <> SalesHeader."No.") THEN BEGIN
SalesHeader.GET("Document Type","Document No.");
IF SalesHeader."Currency Code" = '' THEN
Currency.InitRoundingPrecision
ELSE BEGIN
SalesHeader.TESTFIELD("Currency Factor");
Currency.GET(SalesHeader."Currency Code");
Currency.TESTFIELD("Amount Rounding Precision");
END;
END;
end;
This function can´t use Currency.Intialize, but avoids unnecessary code using “Currency.InitRoundingPrecision”.
Usage. Language table.
The use in language table of this pattern it is a subtler usage than the currency initialize, but it´s very useful and powerful. In table 8 Language, it´s declared this function:
procedure GetLanguageID(LanguageCode : Code) : Integer
begin
CLEAR(Rec);
IF LanguageCode <> '' THEN
IF GET(LanguageCode) THEN
EXIT("Windows Language ID");
"Windows Language ID" := GLOBALLANGUAGE;
EXIT("Windows Language ID");
end;
The useful part is the use of this function in a report:
Sales Invoice Header - OnAfterGetRecord()
CurrReport.LANGUAGE := Language.GetLanguageID("Language Code");
It couldn´t be more simple and clean.
Consequences.
Think before use this pattern: sometimes the better action with a blank key code not is construct a null object, could be raise an error: "break it early".
Annex. Pattern format.
This post is written with the official NAV pattern format. Check this link for further pattern information: https://community.dynamics.com/nav/w/designpatterns. The meaning of pattern sections is:
Context=Introduction and general explanation about goal of the pattern.
Problem=Situation to solve with the pattern.
Forces=Problems you will get if you don´t use the pattern. This section must emphasize previous problem section, showing examples of code without the pattern application.
Solution=Short description of adopted solution, sometimes (but not mandatory) with an abstract template of code.
Usage=Examples or pattern use. Better if taken from NAV standard code.
Consequences=Warnings and possible adverse effects of pattern use.
*This post is locked for comments