In my last blogpost I wrote about how we can implement a Command Queue in AL. In the second part I will go a little bit in detail about the Solution. First of all you have to understand the differenences between valuetypes and referencetypes. If you already know you skip to the next paragraph.
TL;DR; Explanation of valuetypes and referencetypes
Business Central is build on top of .net so it also uses the same technologies. In .net there are mainly to types of datatypes: valuetypes and referencetypes.
Valuetypes are datatypes which directly store the value in the memory. One common used example is integer.
How those this look in the memory
procedure ValueTest()
var
a : int;
b : int;
c : int;
begin
a := 1;
b := 2;
c := a;
end;

As you can see the values of the variable is directly stored
When we look to reference types, the variable only holds an reference to the “value” (i will call it object later), and the “value” is stored on a different place. In AL most object types are reference types (i haven’t checked for all but IMHO all Objecttypes are reference types). In my example I take codeunits. A codeunit is instanciate when it is used for the first time.
So what happens in the storage?
procedure ReferenceTest()
var
a : Codeunit "My Codeunit";
b : Codeunit "My Codeunit";
c : Codeunit "My Codeunit";
begin
a.SetMessage('a');
b.SetMessage('b');
c := a;
c.SetMessage('c');
c.GetMessage(); // Resolves to "c"
end;

The “Queue” Object
A queue is a First in First out collection. To pop the first Item we need to now the beginning of the queue and to push new objects we need to now where the queue ends.
If we remember the first part of this post, the Queue has 2 global codeunit variables
var
first: Codeunit QueueEntry;
last: Codeunit QueueEntry;
So what we can store now is something like this

When we have a look to the queue Entry it has a global codeunit and a global interface (I will handle the Interface in the next paragraph
var
value: Interface ICommand;
NextEntry: Codeunit QueueEntry;
The NextEntry codeunit connects every qeueueentry to the next entry. Using this we can make a linked list of queue entries, where every member links the next one.



The “ICommand” Interface
Now we know how to get the next command of the command queue, but now we want to execute the next command.
Remember the last post the Interface “ICommand” is pretty simple it just has one known procedure
interface ICommand
{
procedure Execute();
}
When we now have a look how the memory of an interface variable simplified look it maybe gets a little bit clearlier. Interfaces are also reference types but the reference points to an instance of a codeunit. For my example I’m using the “MessageCommander” of the last post.
procedure TestInterface()
var
messageCommander : codeunit MessageCommander;
command : interface "ICommand";
begin
messageCommander.SetText('a');
command := messageCommander;
command.Execute(); // Shows message 'a'
end;

The interface is a loose coupling between to codeunits, so it is like a contract which codeunits must fullfil if they want to implement the Interface, because of this and the fact that the interface of the above example is referencing to the same object the “Execute” of the will call “Execute” in the codeunit

In Object Oriented Programming we call this coupling. This means we could use every codeunit which is implementing the ICommand, we can even mix it up in the same process
Another usage example
We now will have a second codeunit “SalesOrderPostCommander” which is looking like that
codeunit 50104 "SalesOrderPostCommander" implements ICommand
{
procedure SetSalesOrderNumber(value : Code[20])
begin
No := value;
end;
procedure Execute()
begin
// TODO Post Sales Header
end;
var
No : Code[20];
}
Now lets combine the to “ICommand” Codeunits
codeunit 50105 PatchPostQueue
{
procedure PatchPost()
begin
FilterSalesOrdersToPost();
if not SalesOrders.Findset(false) then
exit(); // Nothing to post
repeat
AddSalesOrderToQueue(SalesOrder."No.");
until SalesOrders.Next() = 0;
AddMessageToQueue('Posting Complete');
ExecuteQueue();
end;
local procedure ExecuteQueue()
var
object : interface "ICommand";
begin
repeat
object := queue.Pop();
object.Execute();
until queue.GetSize() = 0;
end;
local procedure FilterSalesOrdersToPost()
begin
// Filter Sales Orders here
end;
local procedure AddMessageToQueue(message : Text);
var
t: Codeunit MessageCommander;
object: Interface ICommand;
begin
t.SetText(message);
object := t;
queue.Push(object);
end;
local procedure AddSalesOrderToQueue(No : Text);
var
SaleOrderCommander: Codeunit SalesOrderPostCommander;
object: Interface ICommand;
begin
SaleOrderCommander.SetSalesOrderNumber(No);
object := SaleOrderCommander;
queue.Push(object);
end;
var
SalesOrders : Record "Sales Header";
queue: Codeunit Queue;
}
Nice pattern!
Just one comment: I think you don’t have to assign the commander object to interface ICommand before passing as a parameter to the Push function.
This should also work:
local procedure AddSalesOrderToQueue(No : Text);
var
SaleOrderCommander: Codeunit SalesOrderPostCommander;
begin
SaleOrderCommander.SetSalesOrderNumber(No);
queue.Push(SalesOrderCommander);
end;
I would have done it but, unfortunately the AL compiler does not allow this.
That’s because the parameter is passed by var. Then it *must* be the same type. Similar to passing a literal text to a var text parameter, that doesn’t work either.
But if you remove the var, then it works like a charm.
In general, the var should only be used in case you want to change the state and return it back to the caller.
My personal rule is to always use var for complex types (especially with record variables) unless you have a very good reason not to use it. Passing a codeunit to an interface variable is such an example (assuming the caller doesn’t read any state from the codeunit after the call).
Hi Patrick, I’m not sure if there was a more private channel but is “The interface is a lose coupling between to codeunits, ” supposed to be “… is a loose coupling …”?
Thank you, yes it is. I fixed it