8.3 Interfaces, Polymorphism & Dynamic Codeunit Invocation
Key Takeaways
- Interfaces in AL define pure syntactic contracts of procedure signatures, enabling object-oriented polymorphism and adherence to the Open-Closed Principle in Business Central.
- Codeunits implement interfaces using the 'implements' keyword, requiring exact compile-time signature conformity for parameter types, reference directions (var), and return types.
- Extensible enums link interface definitions to concrete codeunits via the 'Implementation' property, enabling dynamic runtime instantiation without hardcoded CASE branching.
- The DefaultImplementation and UnknownValueImplementation enum properties provide robust fallback mechanisms for uninitialized or unmapped ordinal values.
- A single codeunit can implement multiple interfaces simultaneously, and interfaces facilitate isolated unit testing by enabling mock implementations without external network dependencies.
8.3 Interfaces, Polymorphism & Dynamic Codeunit Invocation
Prior to the introduction of interfaces in AL, writing extensible business logic with multiple interchangeable implementations required extensive CASE statements or complex hook events. If a third-party extension wanted to add a new calculation method, shipping carrier, or payment gateway, it had to subscribe to hook events and handle custom branching manually. AL interfaces enable object-oriented polymorphism by decoupling the definition of capabilities from their concrete execution, allowing dynamic runtime dispatch governed by extensible enums.
1. Interface Declaration & Syntax
An interface object in AL defines a contractual specification composed of procedure signatures. It contains no implementation code, no local or global variable declarations, and no triggers. It enforces a strict compile-time contract: any codeunit declaring that it implements the interface must provide concrete implementations for every procedure with identical parameter names, data types, parameter passing modes (var vs. value), and return types.
// 1. Interface Declaration
interface "IShippingCarrier"
{
procedure CalculateShippingCost(PackageWeight: Decimal; DestinationPostalCode: Code[20]): Decimal;
procedure GenerateTrackingNumber(ShipmentHeaderNo: Code[20]): Text[50];
procedure ValidateAddress(AddressLine: Text[100]; PostalCode: Code[20]; CountryCode: Code[10]): Boolean;
}
Rules for Interface Declarations
- Procedure Signatures Only: Each declaration consists of the procedure name, input parameters, and optional return value. No
begin...endblocks or variable blocks are permitted. - Access Modifiers: Procedures declared within an interface are implicitly public contracts.
- Versioning Stability: Once an interface is published in an AppSource app or shared library, altering existing procedure signatures breaks all dependent codeunits. New capabilities should be introduced via distinct interface extensions or new interface objects.
2. Implementing Interfaces in Codeunits
Codeunits implement interfaces by specifying the implements keyword followed by the quoted interface name. Multiple distinct codeunits can implement the same interface, each encapsulating provider-specific business logic.
// 2. Concrete Implementation: FedEx Carrier
codeunit 50120 "FedEx Shipping Provider" implements "IShippingCarrier"
{
procedure CalculateShippingCost(PackageWeight: Decimal; DestinationPostalCode: Code[20]): Decimal
begin
// FedEx rate calculation algorithm / API call
exit(15.0 + (PackageWeight * 1.75));
end;
procedure GenerateTrackingNumber(ShipmentHeaderNo: Code[20]): Text[50]
begin
exit('FDX-' + Format(CurrentDateTime, 0, '<Year4><Month,2><Day,2><Hours24><Minutes,2><Seconds,2>'));
end;
procedure ValidateAddress(AddressLine: Text[100]; PostalCode: Code[20]; CountryCode: Code[10]): Boolean
begin
// FedEx address validation service
exit((PostalCode <> '') and (CountryCode <> ''));
end;
}
// 3. Concrete Implementation: UPS Carrier
codeunit 50121 "UPS Shipping Provider" implements "IShippingCarrier"
{
procedure CalculateShippingCost(PackageWeight: Decimal; DestinationPostalCode: Code[20]): Decimal
begin
// UPS zone-based calculation algorithm
exit(12.5 + (PackageWeight * 2.10));
end;
procedure GenerateTrackingNumber(ShipmentHeaderNo: Code[20]): Text[50]
begin
exit('1Z999999' + ShipmentHeaderNo);
end;
procedure ValidateAddress(AddressLine: Text[100]; PostalCode: Code[20]; CountryCode: Code[10]): Boolean
begin
// UPS address validation logic
exit(StrLen(PostalCode) >= 5);
end;
}
Multi-Interface Implementation on a Single Codeunit
AL supports implementing multiple interfaces on a single codeunit. This allows a unified service codeunit to satisfy several distinct operational contracts across the system.
interface "ILabelPrinter"
{
procedure PrintShippingLabel(TrackingNo: Text[50]; LabelFormat: Code[10]): Boolean;
}
// Codeunit implementing both IShippingCarrier and ILabelPrinter
codeunit 50122 "DHL Express Provider" implements "IShippingCarrier", "ILabelPrinter"
{
// IShippingCarrier implementation
procedure CalculateShippingCost(PackageWeight: Decimal; DestinationPostalCode: Code[20]): Decimal
begin
exit(18.0 + (PackageWeight * 1.50));
end;
procedure GenerateTrackingNumber(ShipmentHeaderNo: Code[20]): Text[50]
begin
exit('DHL-' + ShipmentHeaderNo);
end;
procedure ValidateAddress(AddressLine: Text[100]; PostalCode: Code[20]; CountryCode: Code[10]): Boolean
begin
exit(PostalCode <> '');
end;
// ILabelPrinter implementation
procedure PrintShippingLabel(TrackingNo: Text[50]; LabelFormat: Code[10]): Boolean
begin
// Direct thermal / PDF label generation logic
exit(true);
end;
}
3. Dynamic Runtime Instantiation via Extensible Enums
The full architectural power of AL interfaces is realized when coupled with extensible enums. By mapping enum values directly to implementing codeunits, the AL runtime performs dynamic polymorphic instantiation without requiring brittle branching logic.
enum 50120 "Shipping Carrier Type" implements "IShippingCarrier"
{
Extensible = true;
DefaultImplementation = "IShippingCarrier" = "Manual Carrier Provider";
UnknownValueImplementation = "IShippingCarrier" = "Manual Carrier Provider";
value(0; Manual)
{
Caption = 'Manual / Local Delivery';
Implementation = "IShippingCarrier" = "Manual Carrier Provider";
}
value(1; FedEx)
{
Caption = 'FedEx Express';
Implementation = "IShippingCarrier" = "FedEx Shipping Provider";
}
value(2; UPS)
{
Caption = 'UPS Ground';
Implementation = "IShippingCarrier" = "UPS Shipping Provider";
}
value(3; DHL)
{
Caption = 'DHL International';
Implementation = "IShippingCarrier" = "DHL Express Provider";
}
}
Eliminating Fragile CASE Statements
In traditional AL programming, selecting an algorithm based on an enum or option field required a CASE statement. If a third-party extension added a new option, the base CASE statement failed or ignored the new value. With interfaces, developers cast the enum variable directly to the interface variable:
codeunit 50130 "Shipment Execution Engine"
{
procedure ProcessShipmentFreight(CarrierType: Enum "Shipping Carrier Type"; Weight: Decimal; PostalCode: Code[20]): Decimal
var
Carrier: Interface "IShippingCarrier";
FreightAmount: Decimal;
begin
// Dynamic runtime instantiation: The runtime creates the matching codeunit automatically!
Carrier := CarrierType;
FreightAmount := Carrier.CalculateShippingCost(Weight, PostalCode);
exit(FreightAmount);
end;
}
Adding New Providers in Independent Extensions
A separate ISV extension can add support for a new carrier (e.g., Royal Mail) without modifying the base app:
- Declare a codeunit
codeunit 50200 "Royal Mail Provider" implements "IShippingCarrier". - Extend the enum:
enumextension 50200 "Royal Mail Ext" extends "Shipping Carrier Type". - Add value
value(50200; RoyalMail) { Implementation = "IShippingCarrier" = "Royal Mail Provider"; }. - When the user selects
RoyalMailon a shipping method,Carrier := CarrierTypeautomatically resolves toRoyal Mail Providerat runtime.
4. Fallback Implementations & Test Mocking
DefaultImplementation & UnknownValueImplementation
To prevent unhandled runtime exceptions when data is incomplete or corrupted, AL enums support two critical fallback properties:
DefaultImplementation: Specifies the fallback codeunit to instantiate when an uninitialized enum variable (ordinal 0 or unassigned) is cast to an interface.UnknownValueImplementation: Specifies the fallback codeunit when an unrecognized or unmapped ordinal value is encountered (for example, if a third-party extension that added an enum value is uninstalled, but existing records still contain that integer value in the database).
enum 50125 "Tax Engine Type" implements "ITaxCalculator"
{
Extensible = true;
DefaultImplementation = "ITaxCalculator" = "Standard Tax Engine";
UnknownValueImplementation = "ITaxCalculator" = "Standard Tax Engine";
value(0; Standard)
{
Implementation = "ITaxCalculator" = "Standard Tax Engine";
}
value(1; Avalara)
{
Implementation = "ITaxCalculator" = "Avalara Tax Engine";
}
}
Testing with Mock Interface Implementations
Interfaces are invaluable for automated unit testing in Business Central. By implementing a mock codeunit that satisfies an interface contract, developers can test complex business logic without connecting to live external web services or incurring third-party API costs.
codeunit 50140 "Mock Carrier Provider" implements "IShippingCarrier"
{
procedure CalculateShippingCost(PackageWeight: Decimal; DestinationPostalCode: Code[20]): Decimal
begin
// Return deterministic test data
if DestinationPostalCode = '99999' then
exit(0.0); // Free shipping test scenario
exit(25.0);
end;
procedure GenerateTrackingNumber(ShipmentHeaderNo: Code[20]): Text[50]
begin
exit('MOCK-TRACK-12345');
end;
procedure ValidateAddress(AddressLine: Text[100]; PostalCode: Code[20]; CountryCode: Code[10]): Boolean
begin
exit(true);
end;
}
Architectural Comparison: Interfaces vs. Events vs. CASE Statements
| Feature | AL Interfaces & Enums | Event-Driven Hooks | Legacy CASE Statements |
|---|---|---|---|
| Extensibility Model | Open-Closed Principle (additive enum extensions). | Decoupled subscribers listening to publishers. | Closed monolithic branching. |
| Contract Enforcement | Strict compile-time signature verification. | Weak (parameter mismatches cause runtime issues). | None (hardcoded branches). |
| Execution Pattern | Exactly one concrete implementation per enum value. | Multiple subscribers execute in non-deterministic order. | Single branch matches ordinal. |
| Fallback Handling | Native DefaultImplementation & UnknownValue. | Manual handling via if not IsHandled then. | else branch required. |
| Unit Testability | High (swap enum with mock implementation). | Moderate (manual binding required). | Poor (requires mock database data). |
A developer writes: 'codeunit 50100 MyCarrier implements ICarrier, ILabelPrinter'. What does the AL compiler require of this codeunit?
In an extensible Enum that implements an Interface, what is the primary purpose of declaring the 'UnknownValueImplementation' property?
A developer needs to create an extensible calculation engine where different ISV apps can provide their own calculation algorithms. Which AL language constructs must be combined to achieve compile-time contract enforcement and dynamic runtime execution without CASE statements?
How do AL interfaces facilitate automated unit testing when developing integrations with third-party payment gateways?