Skip to content

Commit 757e320

Browse files
committed
Added car & engine example
0 parents  commit 757e320

14 files changed

+525
-0
lines changed

.gitignore

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## --- AL gitignore ---
2+
## Ignores generated binary files and
3+
## temporary files from Visual Studio and Visual Studio code
4+
5+
# Binary files
6+
.alpackages/
7+
*.app
8+
9+
# Temp files
10+
.vs/
11+
.vscode/
12+
13+
# Others
14+
~$*
15+
*~

README.MD

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Object Interface Pattern
2+
This is a design pattern for AL and C/AL.
3+
It uses manually bound event subscribers at both the interface layer and the implementation layer to allow consumers of the interface to work with none of the drawbacks or boilerplate commonly seen in other NAV/BC design patterns that address loose coupling and polymorphism. Usually those drawbacks are one or more of the following:
4+
- Parameter records that only supports simple types (or binary..).
5+
- Single instance codeunits at parts of the pattern requiring either singleton design, no global state or manual destruction of global state instead of relying on normal variable scoping to clean up.
6+
- Stateless implementation (this one usually comes with initializing a new object every time the implementation is invoked which impacts performance - See https://youtu.be/jGdkpTGmCgs?t=3995 for more info on that, that talk was also the whole inspiration for this pattern..).
7+
8+
9+
This pattern has none of these drawbacks, but it's still not perfect. It'll probably make debugging harder if taken to extremes since the language doesn't help you with any of this and if you have a lot of instantiated objects at the same time via the same interface there will be overhead when invoking one of them since manually bound subscribers still listen to ALL instantiated objects containing their corresponding publisher..
10+
11+
Microsoft should add a proper solution to this design problem in the language/runtime but until they do, this is the best pattern I can imagine when you need to build a generic framework and your daily work revolves around NAV/BC.
12+
All of this should work in NAV2016 and newer, including AppSource extensions, since it only relies on manually bound events and CODEUNIT.RUN
13+
14+
Check out CarInterfaceConsumer.al first if you want to understand why this pattern can be useful.
15+
The TestPage.al has an action that will run the consumer so you can see it in action.

app.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"id": "bfab16a2-7e8e-4f93-9e21-343d9657b76b",
3+
"name": "Object Interface Pattern",
4+
"publisher": "Mikkel Mansa Vilhelmsen",
5+
"brief": "",
6+
"description": "Object oriented AL proof of concept",
7+
"version": "1.0.0.0",
8+
"privacyStatement": "",
9+
"EULA": "",
10+
"help": "",
11+
"url": "",
12+
"logo": "",
13+
"capabilities": [],
14+
"dependencies": [],
15+
"screenshots": [],
16+
"platform": "13.0.0.0",
17+
"application": "13.0.0.0",
18+
"idRange": {
19+
"from": 50100,
20+
"to": 50149
21+
},
22+
"target": "Extension",
23+
"showMyCode": true
24+
}

src/Car/ICar.al

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
codeunit 50100 ICar
2+
{
3+
var
4+
_binder: Codeunit ICarBinder;
5+
6+
procedure Construct(codeunitID: Integer)
7+
begin
8+
_binder.Bind(codeunitID, _binder);
9+
end;
10+
11+
procedure GetImplementationType(): Integer
12+
begin
13+
exit(_binder.GetBoundCodeunitID());
14+
end;
15+
16+
procedure GetTopSpeed(): Decimal
17+
var
18+
topSpeed: decimal;
19+
begin
20+
_binder.OnGetTopSpeed(topSpeed, _binder.GetBindingID());
21+
exit(topSpeed);
22+
end;
23+
24+
procedure GetEngine(var engineOut: Codeunit IEngine)
25+
begin
26+
_binder.OnGetEngine(engineOut, _binder.GetBindingID());
27+
end;
28+
}

src/Car/ICarBinder.al

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
codeunit 50101 ICarBinder
2+
{
3+
EventSubscriberInstance = Manual;
4+
5+
var
6+
_boundCodeunitReference: Variant;
7+
_boundCodeunitID: Integer;
8+
_this: Codeunit ICarBinder;
9+
_readyToBind: Boolean;
10+
_bindingID: Guid;
11+
_bindingErr: Label 'Binding with implementation codeunit %1 failed';
12+
13+
procedure Bind(codeunitID: Integer; this: Codeunit ICarBinder)
14+
begin
15+
if (_readyToBind) then
16+
Destruct();
17+
18+
_this := this;
19+
20+
_readyToBind := BindSubscription(_this);
21+
if (not _readyToBind) then
22+
Error(_bindingErr, codeunitID);
23+
24+
if (not Codeunit.Run(codeunitID)) or (not _boundCodeunitReference.IsCodeunit()) then begin
25+
Destruct();
26+
Error(_bindingErr, codeunitID);
27+
end;
28+
29+
_readyToBind := not UnbindSubscription(_this);
30+
if (_readyToBind) then begin
31+
Destruct();
32+
Error(_bindingErr, codeunitID);
33+
end;
34+
35+
_boundCodeunitID := codeunitID;
36+
end;
37+
38+
procedure GetBindingID(): Guid
39+
begin
40+
exit(_bindingID);
41+
end;
42+
43+
procedure GetBoundCodeunitID(): Integer
44+
begin
45+
exit(_boundCodeunitID);
46+
end;
47+
48+
local procedure Destruct()
49+
begin
50+
CLEAR(_boundCodeunitReference); //Dereference the bound implementation codeunit
51+
if (_readyToBind) then
52+
_readyToBind := not UnbindSubscription(_this);
53+
CLEARALL;
54+
end;
55+
56+
[EventSubscriber(ObjectType::Codeunit, Codeunit::ICarBinder, 'OnBindInterfaceToImplementation', '', true, true)]
57+
local procedure OnSubscribeBindInterfaceToImplementation(implementationCodeunit: Variant; var bindingIDOut: Guid)
58+
begin
59+
_boundCodeunitReference := implementationCodeunit;
60+
_bindingID := CreateGuid();
61+
bindingIDOut := _bindingID;
62+
end;
63+
64+
[IntegrationEvent(false, false)]
65+
procedure OnBindInterfaceToImplementation(implementationCodeunit: Variant; var bindingIDOut: Guid)
66+
begin
67+
end;
68+
69+
[IntegrationEvent(false, false)]
70+
procedure OnGetTopSpeed(var topSpeed: Decimal; bindingID: Guid)
71+
begin
72+
end;
73+
74+
[IntegrationEvent(false, false)]
75+
procedure OnGetEngine(var engine: Codeunit IEngine; bindingID: Guid)
76+
begin
77+
end;
78+
}

src/Car/Skoda.al

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
codeunit 50102 Skoda
2+
{
3+
EventSubscriberInstance = Manual;
4+
5+
var
6+
_bindingID: Guid;
7+
_engine: Codeunit IEngine;
8+
9+
trigger OnRun()
10+
var
11+
this: Codeunit Skoda;
12+
begin
13+
BindSubscription(this);
14+
this.Construct(this);
15+
end;
16+
17+
procedure Construct(this: Codeunit Skoda)
18+
var
19+
CodeunitVariant: Variant;
20+
ICarBinder: Codeunit ICarBinder;
21+
begin
22+
CodeunitVariant := this;
23+
ICarBinder.OnBindInterfaceToImplementation(CodeunitVariant, _bindingID);
24+
25+
//Default object state
26+
_engine.Construct(Codeunit::GasolineEngine);
27+
end;
28+
29+
[EventSubscriber(ObjectType::Codeunit, Codeunit::ICarBinder, 'OnGetTopSpeed', '', true, true)]
30+
local procedure OnGetTopSpeed(var topSpeed: Decimal; bindingID: Guid)
31+
begin
32+
if (bindingID <> _bindingID) then
33+
exit;
34+
35+
topSpeed := 200.00;
36+
end;
37+
38+
[EventSubscriber(ObjectType::Codeunit, Codeunit::ICarBinder, 'OnGetEngine', '', true, true)]
39+
local procedure OnGetEngine(var engine: Codeunit IEngine; bindingID: Guid)
40+
begin
41+
if (bindingID <> _bindingID) then
42+
exit;
43+
44+
engine := _engine;
45+
end;
46+
}

src/Car/Tesla.al

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
codeunit 50103 Tesla
2+
{
3+
EventSubscriberInstance = Manual;
4+
5+
var
6+
_bindingID: Guid;
7+
_engine: Codeunit IEngine;
8+
9+
trigger OnRun()
10+
var
11+
this: Codeunit Tesla;
12+
begin
13+
BindSubscription(this);
14+
this.Construct(this);
15+
end;
16+
17+
procedure Construct(this: Codeunit Tesla)
18+
var
19+
codeunitVariant: Variant;
20+
ICarBinder: Codeunit ICarBinder;
21+
begin
22+
codeunitVariant := this;
23+
ICarBinder.OnBindInterfaceToImplementation(codeunitVariant, _bindingID);
24+
25+
//Default object state
26+
_engine.Construct(Codeunit::ElectricEngine);
27+
end;
28+
29+
[EventSubscriber(ObjectType::Codeunit, Codeunit::ICarBinder, 'OnGetTopSpeed', '', true, true)]
30+
local procedure OnGetTopSpeed(var topSpeed: Decimal; bindingID: Guid)
31+
begin
32+
if (bindingID <> _bindingID) then
33+
exit;
34+
35+
topSpeed := 400.00;
36+
end;
37+
38+
[EventSubscriber(ObjectType::Codeunit, Codeunit::ICarBinder, 'OnGetEngine', '', true, true)]
39+
local procedure OnGetEngine(var engine: Codeunit IEngine; bindingID: Guid)
40+
begin
41+
if (bindingID <> _bindingID) then
42+
exit;
43+
44+
engine := _engine;
45+
end;
46+
}

src/CarComparison.al

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
codeunit 50108 CarComparison
2+
{
3+
procedure CompareTopSpeed(car1: Codeunit ICar; car2: Codeunit ICar)
4+
var
5+
car1TopSpeed: Decimal;
6+
car2TopSpeed: Decimal;
7+
allObj1: Record AllObjWithCaption;
8+
allObj2: Record AllObjWithCaption;
9+
begin
10+
car1TopSpeed := car1.GetTopSpeed();
11+
car2TopSpeed := car2.GetTopSpeed();
12+
13+
//We can get the specific type of the implementation codeunit if we want..
14+
allObj1.Get(allObj1."Object Type"::Codeunit, car1.GetImplementationType());
15+
allObj2.Get(allObj1."Object Type"::Codeunit, car2.GetImplementationType());
16+
17+
case true of
18+
car1TopSpeed > car2TopSpeed:
19+
Message('The %1 (%2 km/h) is faster than the %3 (%4 km/h)!', allObj1."Object Caption", car1TopSpeed, allObj2."Object Caption", car2TopSpeed);
20+
car1TopSpeed < car2TopSpeed:
21+
Message('The %3 (%4 km/h) is faster than the %1 (%2 km/h)!', allObj1."Object Caption", car1TopSpeed, allObj2."Object Caption", car2TopSpeed);
22+
else
23+
Message('The cars are equally fast, or slow (%1 km/h)!', car1TopSpeed);
24+
end;
25+
end;
26+
27+
procedure CompareEngineHorsePower(car1: Codeunit ICar; car2: Codeunit ICar)
28+
var
29+
engine1: Codeunit IEngine;
30+
engine2: Codeunit IEngine;
31+
engine1HorsePower: Decimal;
32+
engine2HorsePower: Decimal;
33+
begin
34+
car1.GetEngine(engine1);
35+
car2.GetEngine(engine2);
36+
37+
engine1HorsePower := engine1.GetHorsePower();
38+
engine2HorsePower := engine2.GetHorsePower();
39+
40+
case true of
41+
engine1HorsePower > engine2HorsePower:
42+
Message('The first car (%1) has more horsepower than the second (%2)!', engine1HorsePower, engine2HorsePower);
43+
engine1HorsePower < engine2HorsePower:
44+
Message('The second car (%2) has more horsepower than the first (%1)!', engine1HorsePower, engine2HorsePower);
45+
else
46+
Message('The engines have the same horsepower (%1)!', engine1HorsePower);
47+
end;
48+
end;
49+
}

src/CarInterfaceConsumer.al

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
codeunit 50109 CarInterfaceConsumer
2+
{
3+
trigger OnRun()
4+
begin
5+
CompareCars();
6+
end;
7+
8+
local procedure CompareCars()
9+
var
10+
car1: Codeunit ICar;
11+
car2: Codeunit ICar;
12+
carComparison: Codeunit CarComparison;
13+
begin
14+
//The codeunit IDs could come from persistent setup, be decided runtime or be send to NAV as part of a request from an external system.
15+
car1.Construct(Codeunit::Skoda);
16+
car2.Construct(Codeunit::Tesla);
17+
18+
//The object reference chains will be clean & independent after the object is constructed:
19+
//Car1 => ICar => ICarBinder => Skoda
20+
//Car2 => ICar => ICarBinder => Tesla
21+
22+
//Notice that ICar has the consumer facing API, while ICarBinder has the implementation facing API.
23+
//From the consumers point of view, there is no magic or boilerplate or things to consider/keep in mind.
24+
25+
//Since nothing in the pattern is single instance the objects can live side by side, and go out of scope independently of each other. That also means no manual destructor is necessary.
26+
//Compared to a discovery event pattern, constructing & using an object through an interface will not grow slower as the number of different possible implementations (car models) grow,
27+
//since we don't need to broadcast to all possible implementations to find the right one.
28+
29+
//The only remaining overhead is: since every manually bound subscriber listens to ALL instances of publisher objects rather than one specific instance, performance will
30+
//be impacted by the number of living implementation object at any given time. Ie. if I instantiate 100 cars and invoke 1 of them, 99 will have to exit out.
31+
//But since we assume that the 99 other objects will at least be useful, otherwise why were they constructed in the first place, this hopefully has minimal impact compared to
32+
//other drawbacks.
33+
34+
//We can pass the constructed objects to other codeunits that are written for the interface rather than for hardcoded references:
35+
carComparison.CompareTopSpeed(car1, car2);
36+
37+
//The horse power comparison is using a nested object Engine within Car, implemented via the same pattern for illustration purposes.
38+
carComparison.CompareEngineHorsePower(car1, car2);
39+
end;
40+
}

src/Engine/ElectricEngine.al

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
codeunit 50106 ElectricEngine
2+
{
3+
EventSubscriberInstance = Manual;
4+
5+
var
6+
_bindingID: Guid;
7+
8+
trigger OnRun()
9+
var
10+
this: Codeunit ElectricEngine;
11+
begin
12+
BindSubscription(this);
13+
this.Construct(this);
14+
end;
15+
16+
procedure Construct(this: Codeunit ElectricEngine)
17+
var
18+
codeunitVariant: Variant;
19+
IEngineBinder: Codeunit IEngineBinder;
20+
begin
21+
codeunitVariant := this;
22+
IEngineBinder.OnBindInterfaceToImplementation(codeunitVariant, _bindingID);
23+
end;
24+
25+
[EventSubscriber(ObjectType::Codeunit, Codeunit::IEngineBinder, 'OnGetHorsePower', '', true, true)]
26+
local procedure OnGetTopSpeed(var horsePower: Decimal; bindingID: Guid)
27+
begin
28+
if (bindingID <> _bindingID) then
29+
exit;
30+
31+
horsePower := 1000;
32+
end;
33+
}

0 commit comments

Comments
 (0)