Business Central development focuses a lot on the Table/Page/Report structures, and for many customizations this is all that is required. As things get more complex, we start incorporating Code Units to compartmentalize the data processing. Business Central Interfaces provide a means to better define and work with Code Units.
An interface is a kind of agreement on how to interact with a Code Unit. In this case, the Code Unit takes the role of what would be considered a Class in a more traditional Object-Oriented Programming Language. The great thing about an interface is that it guarantees what must be included in the Code Unit but doesn’t restrict what can be included.
I’m currently working on a project where I want to ask several different developers to provide me with Code Units to solve problems. I don’t want to dictate what goes into those Code Units, but I also want a uniform way to handle them. In this case I provide the Interface I want the Code Units to implement and then add the provided Code Units to the project and my “factory”.
Let’s take a step back and look at the challenge I’m solving and how Interfaces are going to help me solve it.
The Advent of Code is a 25-day programming challenge. Each day provides a new two-part software challenge that takes in a text block and spits out an integer result. I’m planning to work with a group of AL Developers to try and solve each of the problems, at least part 1 of each, in Business Central AL. I’ll be providing a solution that has some supporting tables, pages, and processors based on previous years challenges.
The goal is to allow participants to download the code from GitHub and add their Code Unit to the set of solutions. Each Code Unit needs to have a minimum set of procedures for it to play well with the framework I’ve created. Here is where an interface is handy.
Let’s take a look at the interface code I’ve created.
interface ARD_AdventOfCodeProcessor
{
procedure CalculateResult1(Rec: record ARD_AOCChallenge; RunExample: Boolean): Integer;
procedure CalculateResult2(Rec: record ARD_AOCChallenge; RunExample: Boolean): Integer;
}
An interface is simply the signatures of the procedures I expect to find inside the Code Unit. In this case I need two procedures, one that solves challenge part 1, CalculateResult1, and another that solves challenge part 2, CalculateResult2.
When I create a Code Unit for a solution, in this case Advent Of Code 2023 Day 1, I create a Code Unit that looks like this. Note I didn’t solve part 2, so it returns 0.
codeunit 82024 ARD_AOC202301 implements ARD_AdventOfCodeProcessor
{
Procedure CalculateResult1(Rec: record ARD_AOCChallenge; RunExample: Boolean): Integer
var
Results: Integer;
begin
if RunExample then
Results := Calc2023011(Rec.RetrieveChallengeExample())
else
Results := Calc2023011(Rec.RetrieveChallengeData());
exit(Results);
end;
Procedure CalculateResult2(Rec: record ARD_AOCChallenge; RunExample: Boolean): Integer
begin
exit(0);
end;
//AOC 2023 Day 1
local Procedure Calc2023011(Input: Text): Integer
var
AOCSupport: Codeunit ARD_AOCSupport;
Inputs: list of [Text];
TestValue: Text;
Testcharacter: integer;
Result: Integer;
FirstNumber: Integer;
LastNumber: Integer;
LineValue: Integer;
begin
Result := 0;
Inputs := AOCSupport.SplitLines(Input);
foreach TestValue in Inputs do begin
FirstNumber := -1;
LastNumber := -1;
LineValue := 0;
foreach Testcharacter in TestValue do
if (TestCharacter > 47) AND (Testcharacter < 58) then begin
if FirstNumber < 0 then
FirstNumber := TestCharacter - 48;
LastNumber := Testcharacter - 48;
end;
LineValue := (FirstNumber * 10) + LastNumber;
Result := Result + LineValue;
end;
exit(Result);
end;
}
You can see I have the two procedures as required by the Interface, as well as an additional procedure that performs the calculations. This one is local, but that isn’t a requirement. The Interface only sets a minimum for what the Code Unit exposes, not a restriction.
Now that we can create Code Units that have a specific Interface, how do we use them? A common method is the “Factory”. Here we create a procedure where we ask for a Code Unit, and it selects one from a list and returns it.
codeunit 82027 ARD_AOCProcessorFactory
{
procedure GetProcessor(Day: Integer): interface ARD_AdventOfCodeProcessor
var
Day1: CodeUnit ARD_AOC202301;
Day2: CodeUnit ARD_AOC202302;
begin
case Day of
1:
exit(Day1);
2:
exit(Day2);
end;
end;
}
In this example we pass in which Day we want, and the factory returns any one of the Code Units. It returns it as the interface, so the calling routine can expect the procedures from within that interface. The fact that Day1 and Day2 are completely different Code Units, they both implement the ARD_AdventOfCodeProcessor Interface so we can return them as that interface type.
Let’s take a look at the system that uses the Factory. The Results Calculator receives a record that contains the day number and test data. We call the ARD_AOCProcessorFactor with that day number, and it returns the appropriate Code Unit as the ARD_AdventOfCodeProcessor Interface, and we can call the CalculateResult1 or CalculateResult2 procedure.
codeunit 82025 ARD_ResultsCalculator
{
Procedure CalculateResult1(Rec: record ARD_AOCChallenge; RunExample: Boolean): Integer
var
AOCFactory: Codeunit ARD_AOCProcessorFactory;
Results: Integer;
begin
Results := 0;
Results := AOCFactory.GetProcessor(Rec.ARD_Day).CalculateResult1(Rec, RunExample);
exit(Results);
end;
Procedure CalculateResult2(Rec: record ARD_AOCChallenge; RunExample: Boolean): Integer
var
AOCFactory: Codeunit ARD_AOCProcessorFactory;
Results: Integer;
begin
Results := 0;
Results := AOCFactory.GetProcessor(Rec.ARD_Day).CalculateResult2(Rec, RunExample);
exit(Results);
end;
}
The really interesting thing here is that I don’t know which Code Unit the Factory returned. I do know that it meets the interface requirements, so I can call the procedures I need.
Things get simpler for the Card where I only need 2 Actions, one to Calculate Result 1 and another to Calculate Result 2. The Processor and Factory go about the hard work of finding the correct processor Code Unit and returning an answer.
There are other features of Interfaces that can be very helpful. If you are ever faced with a Variant data type, the “is” and “as” operators are very helpful.
In this example, we can pretend that we don’t know if the value we are getting from the Factory is actually an ARD_AdventOfCodeProcessor. In this case we can test if it “is” an ARD_AdventOfCodeProcessor then cast it “as” and ARD_AdventOfCodeProcessor.
Procedure CalculateResult1(Rec: record ARD_AOCChallenge; RunExample: Boolean): Integer
var
AOCFactory: Codeunit ARD_AOCProcessorFactory;
AOCProcessor: Interface ARD_AdventOfCodeProcessor;
Processor: Variant;
Results: Integer;
begin
Results := 0;
Processor := AOCFactory.GetProcessor(Rec.ARD_Day);
if Processor is ARD_AdventOfCodeProcessor then
AOCProcessor := Processor as ARD_AdventOfCodeProcessor
else
Error('Not an AOC Processor');
Results := AOCProcessor.CalculateResult1(Rec, RunExample);
exit(Results);
end;
More details can be found here: Type testing and casting operators for interfaces | Microsoft Learn
Here is the GitHub to the source: Advent Of Code AL
I hope this has help you see the value and power of Interfaces. If you are interested in participating in the Advent of Code with me this year, please let me know in the comments below. Also keep an eye out in December as a group of talented developers and I attempt to tackle that Advent of Code in Business Central AL.





Leave a comment