Microsoft Dynamics 365 Business Central 2021 Release Wave 1 is out (whoa, that was a mouthful) with some new perks for developers. Today, I had another live session at http://vjeko.live, and I made it both the first one in the series of What’s New for the latest release, as well as the episode four of Fun with Interfaces.
Interfaces are such an amazing feature in AL language, that was long missed, and that’s now saving my day nearly every time I do something with AL. The latest AL compiler (runtime “7.0”) comes with these new interesting features about interfaces:
- You can mark interfaces for obsoletion.
- You can mark individual interface functions for obsoletion.
- You can return interfaces as return type from functions.
- You can define
UnknownValueImplementation
for enums to specify the interface that represents any unknown enum values.
Let’s take a look at each one of those with examples.
Obsolete interfaces
Imagine you have an app that provides some cool functionality, and other partners and customers have built their own apps on top of it. You declare an interface that other partners have implemented in their own custom codeunits.
For example, this is your interface:
interface "ILoyaltyLevel"
{
procedure GetTreatment(): Text;
}
Then, you implemented it in several codeunits, for example this one:
codeunit 50102 Gold implements ILoyaltyLevel
{
procedure GetTreatment(): Text;
begin
exit('Be extremly friendly, offer a discount and a free gift.');
end;
}
And then, there is a partner that built their own app, declared your app as a dependency, and then implemented the same interface in a codeunit of their own:
codeunit 50120 Platinum implements ILoyaltyLevel
{
procedure GetTreatment(): Text;
begin
exit('This customer is our top customer. Treat them with utmost respect. Maximum discount + two free gifts.');
end;
}
So far so good.
Now, imagine you realize that this interface is not needed for your app anymore, and you want to remove it. Instead of removing it in one go, and thus break all of the code for your partners, you decide to do this step-by-step, over two future releases: in the first release you mark it for obsoletion, and then in the second step you actually remove it.
This is what you can now do:
interface "ILoyaltyLevel"
{
ObsoleteState = Pending;
ObsoleteReason = 'This interface is no longer used, and is being replaced by ILoyaltyFeatures interface.';
procedure GetTreatment(): Text;
}
Once you do that, any references to that interface from anywhere inside your app will now start showing warnings, like this:
So, at this point when your partners get the new version of your symbols, they’ll start getting compiler warnings, and will have time to migrate to the new feature, without them having to do it all at once. Pretty smooth.
In my CI/CD from AL Developer’s Shoes webinar I’ll give some real-life examples of when and why this is so useful, and how we can now use this feature to improve our continuous integration and continuous deployment techniques.
Obsolete interface functions
Now, imagine that instead of removing the entire interface, you decide that the way this interface works is too vague. You need this interface, but you want to change its functionality. In my earlier example, I want to make it more business-logic-like, and instead of simply giving out a vague text message on screen, I want to define an actual discount percentage and perks you give to customers at different loyalty levels that I can then use from different places of business logic (e.g. to automatically assign discounts and bonus items when creating sales orders).
Let’s imagine we want this interface to look like this:
interface "ILoyaltyLevel"
{
procedure GetDiscountPct(): Decimal;
procedure GetPerks(): Codeunit Perks;
}
Of course, this immediately breaks your code, which is easy to fix because you own it. But it also breaks all of your existing partners code, and you may not really want that.
So, instead of forcing a change like that, you may want to start obsoleting the existing function, again over two releases. The first step is to mark your method as obsolete using the new Obsolete
attribute.
interface "ILoyaltyLevel"
{
[Obsolete('Do not use GetTreatment. We are moving this to GetDiscoutPct and GetPerks')]
procedure GetTreatment(): Text;
procedure GetDiscountPct(): Decimal;
procedure GetPerks(): Text;
}
This now marks your function (rather than your interface) as obsolete. Check this out.
Here, it’s no longer the Loyalty
variable declaration that shows warning, it’s the invocation of the GetTreatment
method.
It’s not just that, IntelliSense has also nice new support for obsolete functions, you see them like this:
So, the old functionality stays in place, it may even be still used by your app’s code, but you are clearly making it known to everyone that you are about to remove that function and that they should start using different functions now.
Interfaces as return values
Now this is really cool. It’s not just interfaces – you can now return all most complex types directly from functions. So, instead of this:
procedure GetInterface(var Loyalty: Interface ILoyaltyLevel);
begin
// Here be dragons
end;
procedure Consumer();
var
Loyalty: Interface ILoyaltyLevel;
begin
GetInterface(Loyalty);
Loyalty.GetTreatment();
end;
… you can now write this:
procedure GetInterface() Loyalty: Interface ILoyaltyLevel;
begin
// Here be dragons
end;
procedure Consumer();
var
Loyalty: Interface ILoyaltyLevel;
begin
Loyalty := GetInterface();
Loyalty.GetTreatment();
end;
It may sound like an irrelevant change, but there is a universe of difference between GetInterface(Loyalty)
and Loyalty := GetInterface()
. Looking at the former, you don’t really see from code what’s going on – it’s not directly obvious what exactly the GetInterface
method does. Looking at the latter, it’s entirely obvious that your intent is to assign a value. Things like this make development (and code maintenance!) a lot nicer an experience.
Needless to say, you can method-chain now (saving you unnecessary variable declarations), like this:
procedure Consumer();
begin
GetInterface().GetTreatment();
end;
I am not saying that I am recommending this as a general best practice (sometimes this may make your code less readable), but this is definitely something you can take advantage of if all you want to do is this:
trigger OnAction();
var
Loyalty: Interface ILoyaltyLevel;
begin
Loyalty := GetInterface();
Message(Loyalty.GetTreatment());
end;
Because now you can make it much more concise:
trigger OnAction();
begin
Message(GetInterface().GetTreatment());
end;
This is cool. Too bad we had to wait for this long to get things like that.
Unknown enum values
One of the problems with previous versions of interfaces was that you could bind interfaces to enums, and other people can then extend those enums, and other people may later remove their apps from your tenant, and then you end up having unknown enum values in the database.
Imagine you had this enum:
enum 50100 "LoyaltyLevel" implements ILoyaltyLevel
{
Caption = 'Loyalty Level';
Extensible = true;
DefaultImplementation = ILoyaltyLevel = Bronze;
value(0; Bronze)
{
Caption = 'Bronze';
}
value(1; Silver)
{
Caption = 'Silver';
Implementation = ILoyaltyLevel = Silver;
}
value(2; Gold)
{
Caption = 'Gold';
Implementation = ILoyaltyLevel = Gold;
}
}
And then you had this Customer extension:
tableextension 50100 "Customer Extension" extends Customer
{
fields
{
field(50100; "Loyalty Level"; Enum LoyaltyLevel)
{
Caption = 'Loyalty Level';
}
}
}
This allows your users to assign different loyalty levels to customers in the database. It also allows those levels to have specific business logic (implemented in separate codeunits) be easily invoked in a polymorfic manner, like this:
trigger OnAction();
var
Loyalty: Interface ILoyaltyLevel;
begin
Loyalty := Rec."Loyalty Level";
Message(Loyalty.GetTreatment());
end;
Just as a refresher on what’s going on in here. Rec."Loyalty Level"
contains the actual selection for loyalty level for a customer. Since it’s an enum, and since that enum implements an interface, you can invoke an interface method without having to worry about what’s the actual value. Instead of writing an endless (and unmaintainable, not to mention unextensible) case Rec."Loyalty Level"
statement you easily defer the actual implementation to enum, while keeping your code abstract. Nice.
Now, you select Gold loyalty level for a customer, this code will run the Gold
codeunit. If you assign Silver, it will run the Silver
codeunit.
But then a problem happens. Another partner extends your enum in their own extension, like this:
enumextension 50120 "Loyalty Level Extension" extends LoyaltyLevel
{
value(10; Platinum)
{
Caption = 'Platinum';
Implementation = ILoyaltyLevel = Platinum;
}
}
… and they provide an implementation:
codeunit 50120 Platinum implements ILoyaltyLevel
{
procedure GetTreatment(): Text;
begin
exit('This customer is our top customer. Treat them with utmost respect. Maximum discount + two free gifts.');
end;
}
Now, you can assign Platinum levels to customers, and for them, the Platinum
codeunit will execute.
That’s all fine and hunky-dory, but what if you then uninstall that other app that extended the enum? What’s the big deal, you may ask. But it is a big deal: your database now contains customer records with value 10 inside the Loyalty Level field, and that 10 doesn’t map to any known enum. It’s an unknown enum value.
Again, what’s the big deal, you may ask, because we have this setting in our enum:
DefaultImplementation = ILoyaltyLevel = Bronze;
Shouldn’t then the Bronze
codeunit take over when we attempt to execute business logic over the ILoyaltyLevel
interface? No. And that’s the point. The DefaultImplementation
setting only applies to those enum values that are defined like this:
value(0; Bronze)
{
Caption = 'Bronze';
}
… as opposed to those that define an implementation explicitly, like this:
value(1; Silver)
{
Caption = 'Silver';
Implementation = ILoyaltyLevel = Silver;
}
It certainly does not (and must not!) apply to situations when there is an unknown enum value. Applying the default implementation to unknown enum values could result in bad data, and that’s something Microsoft does not want to allow.
So, instead of the default implementation running when you attempt to access the interface of an unknown enum value, you get a runtime error:
Now, with 2021 wave 1, we can define an unknown value implementation:
DefaultImplementation = ILoyaltyLevel = Bronze;
UnknownValueImplementation = ILoyaltyLevel = Unknown;
… and then provide that implementation:
codeunit 50103 Unknown implements ILoyaltyLevel
{
procedure GetTreatment(): Text;
begin
exit('This customer uses an unknown loyalty level. Please check and then assign the best one.');
end;
}
When you attempt to access an unknown enum as an interface, this codeunit will kick in:
This safeguards against corrupt data, and allows you flexibility to jump in and gracefully handle these situations in a more controlled way than a simple runtime error allows.
… and that’s it
That’s all folks. Thanks for bearing with me until the end, and if you haven’t already done so – why don’t you head over to YouTube and watch the session?
Pingback: What’s new about interfaces in 2021 Wave 1 | Pardaan.com
Hi Vjeko is it possible to overload interface functions in the codeunits that implement the interface?
For example I may have an IShape interface with two functions. CalcArea and CalcPerimiter.
Lets say I have a codeunit Square which implements IShape and the CalcArea function takes two arguments, length & breadth. On the other hand the Circle codeunit CalcArea function only takes one argument, radius. Is this possible?
No, unfortunately you can’t do this. The cleanest way for this would be to have two different interfaces: IShape and IRadialShape or something like that. I know you don’t really ask about IShape and squares and circles, so I guess you’ll figure out from my reply how to apply this to your problem. The solution is multiple interfaces.