Moving code by using an interface

If you didn’t work with interfaces before, then the learning path to actually using them can seem difficult. Maybe a simple pattern to clean code gets you started?

  1. On my road to clean code
  2. Identifying area specific code
  3. Collecting all interface implementations
  4. Implementation calls all other implementations
  5. Summary

On my road to clean code

If you’ve already switched your repositories from folder per object type to folder per functional area, then you can safely skip this section.

My app once was a result of a direct Text2Al (C/AL to AL) conversion years ago: There was only one folder per object type, filled with an abundance of objects of different functional area and purposes. Furthermore, there was a single but extra-long subscriber codeunit. It was impossible to determine all code that integrated into specific standard areas. It was a nightmare to add a new area to integrate into. Hence, my goal was to keep all area specific code together in one subfolder.

As a first step, I created another main folder with one subfolder per standard functional area, such “Sales”, “Purchases”, “Manufacturing”. Then, I’ve moved all extension objects into the corresponding subfolder. Finally, I’ve split the single subscriber codeunit into many, one per area. Each area folder now consisted of its own management codeunit, a subscriber codeunit calling procedures in the management codeunit, and objects extending the standard. Good, but what about my other objects that still reference standard areas?

Identifying area specific code

Let’s look specifically at two typical but simplified procedures about to convert.

The first procedure is public. I call it twice in my own code, and other apps might use it as well. It is located in a codeunit called “Drawing Replace Mgt.”. This information will be important later.

procedure GetPostedTableNos(var TableNoList: List of [Integer])
begin
    TableNoList.Add(0);
    // Sales
    TableNoList.Add(Database::SalesTable1);
    TableNoList.Add(Database::SalesTable2);
    ...
    // Purchases
    TableNoList.Add(Database::PurchaseTable1);
    ...
    // Service
    ...
 
    // optional, in fact not in my code here:
    OnAfterPostedTableNos(RecRef, TableNoList);
end;

Especially the second procedure type tends to get really long:

internal procedure AddRecRefToFoundRecordsBuffer(RecRef: RecordRef; var TempFoundRecordsBuffer: Record "My Found Records Buffer" temporary)
var
    SalesLine: Record "Sales Line";
    PurchaseLine: Record "Purchase Line";
    ...
begin
    case RecRef.Number of

        // Sales
        Database::"Sales Line":
            begin
                RecRef.SetTable(SalesLine);
                InsertIntoFoundRecordsBuffer(
                    TempFoundRecordsBuffer, RecRef, SalesLine."No.", SalesLine."Variant Code");
            end;
        // Purchases
        Database::"Purchase Line":
            begin
                RecRef.SetTable(PurchaseLine);
                InsertIntoFoundRecordsBuffer(
                    TempFoundRecordsBuffer, RecRef, PurchaseLine."No.", PurchaseLine."Variant Code");
            end;
        // Service
        ...
        else
            OnAddRecRefToFoundRecordsBuffer_OnNewTable(RecRef, TempFoundRecordsBuffer);
    end;
end;   

Both procedures consist of areas that I’ve created a dedicated folder structure for, and they end with an event to handle custom tables.

Collecting all interface implementations

I can create and implement interfaces to replace events. But can I use interfaces even when I need to collect all possible interface implementations at once, even those from other apps? And will I be able to collect all their returned data or variables, without cluttering my code? With that question in mind, I’ve asked my colleague Patrick Schiefer, and he came with a solution that you can read about in this blog post: Exploring Interface Implementation: A Journey from Events to Enums. Please refer to his article for code snippets, as I won’t repeat them here.

As a result, I’ve created

  1. an interface called DrawingReplace with the two procedures mentioned above (GetPostedTableNos, AddRecRefToFoundRecordsBuffer),
  2. an extensible enum called FunctionalArea (consisting of values DefaultArea, MyAppArea2, …, Sales, Purchases, Sevice etc.).

Each enum value was implemented within a new codeunit, called SalesDrawingReplace, PurchaseDrawingReplace, etc.:

Did I just say “each enum value… within a new codeunit”? Well, not quite.
Do you remember codeunit “Drawing Replace Mgt.” with its procedure GetPostedTableNos? This one now implements my interface as the default implementation, enum value 0 (DefaultArea): the codeunit name fits its purpose perfectly, so no need for another codeunit. (One could argue that implementations should not mix with other procedures, but that’s out of scope here.)

Implementation calls all other implementations

Even with the codeunit being part of an interface implementation, we can still call DrawingReplaceMgt.GetPostedTableNos() directly in our code. In such cases, we circumvent the interface and just execute what’s inside the procedure – nothing new at all.

Whenever this procedure is called directly, I want to return the same stuff that was inside it before I moved parts of the procedure code to other codeunits. In order to achieve this, my procedure now looks like this:

    procedure GetPostedTableNos(TableNoList: List of [Integer])
    var
        DrawingReplace: Interface DrawingReplace;
        Ordinal: Integer;
    begin
        TableNoList.Add(0); // implementation of DefaultArea

        // Next, loop all other implementations of the interface
        foreach Ordinal in Enum::FunctionalArea.Ordinals() do
            if Ordinal <> Enum::FunctionalArea::DefaultArea.AsInteger() then begin
                DrawingReplace := Enum::FunctionalArea.FromInteger(Ordinal);
                DrawingReplace.GetPostedTableNos(TableNoList);
            end;
    end;

Line 10 ensures that DrawingReplaceMgt.GetPostedTableNos() does not call itself.
Line 12 calls the same procedure in all other implementation codeunits.
If the original procedure contained an event, I would have obsoleted it.
The implementation procedures in other codeunits don’t call other implementations.

Summary

For AL-only developers, interfades are quite difficult to grasp, but yet this journey taught me that they are really not that complicated. They help us to structure our code better and ensure that other apps integrate completely with whatever we put into an interface.

As a result, my code is now much cleaner. If I decide later to integrate into another standard area, I can use any area folder (e.g. Sales) as a copy template, without fearing to forget something important.

Leave a comment

Blog at WordPress.com.

Up ↑

Design a site like this with WordPress.com
Get started