mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-07 18:06:53 +01:00
Add CMFRT coding standards documentation and examples
- Introduced guidelines for "one codeunit one global function" architecture to enforce single responsibility in AL code. - Added best practices and anti-patterns for adding parameters via overloads to maintain backward compatibility. - Documented the importance of never deleting members in AL and always marking them as obsolete. - Established the requirement for OnBefore and OnAfter integration events for global procedures to enhance extensibility. - Defined naming conventions for CMFRT objects, including prefixes and object ID ranges to avoid conflicts. - Implemented patterns for case statements to ensure all cases are handled, including the necessity of an else clause. - Introduced the interface injection pattern to allow pluggable operations in table-level code. - Recommended using Confirm Management for user confirmations to improve testability. - Established a three-permission set pattern for security to ensure proper access control. - Created a review skill for CMFRT AL standards to automate compliance checks against established guidelines.
This commit is contained in:
parent
4119417ce4
commit
c72c0ad685
35 changed files with 866 additions and 0 deletions
|
|
@ -0,0 +1,14 @@
|
|||
// Subscriber calls the implementation codeunit DIRECTLY — bypasses the base
|
||||
// table's entry point and all OnBefore/OnAfter integration events.
|
||||
codeunit 2045720 "CMFRT BA Sales Subscribers"
|
||||
{
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
|
||||
local procedure CMFRTBAOnAfterPostSalesDoc(var SalesHeader: Record "Sales Header")
|
||||
var
|
||||
SyncImpl: Codeunit "CMFRT BA SyncPostedDoc Impl";
|
||||
begin
|
||||
// Direct call to implementation codeunit — OnBefore/OnAfter events on the
|
||||
// base table never fire, dependent extensions cannot intercept this operation.
|
||||
SyncImpl.CMFRTBASyncPostedSalesDoc(SalesHeader."No.");
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Subscriber codeunit calls the BASE TABLE procedure — not the implementation codeunit.
|
||||
codeunit 2045720 "CMFRT BA Sales Subscribers"
|
||||
{
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterPostSalesDoc', '', false, false)]
|
||||
local procedure CMFRTBAOnAfterPostSalesDoc(var SalesHeader: Record "Sales Header")
|
||||
var
|
||||
CMFRTBAItem: Record "CMFRT BA Item";
|
||||
begin
|
||||
// Route through the base table — integration events fire normally.
|
||||
CMFRTBAItem.CMFRTBASyncPostedSalesDoc(SalesHeader."No.");
|
||||
end;
|
||||
}
|
||||
|
||||
// Base table owns all entry points to implementation codeunits.
|
||||
table 2045095 "CMFRT BA Item"
|
||||
{
|
||||
procedure CMFRTBASyncPostedSalesDoc(SalesDocNo: Code[20])
|
||||
var
|
||||
SyncImpl: Codeunit "CMFRT BA SyncPostedDoc Impl";
|
||||
Handled: Boolean;
|
||||
begin
|
||||
OnBeforeCMFRTBASyncPostedSalesDoc(SalesDocNo, Handled);
|
||||
if Handled then
|
||||
exit;
|
||||
CMFRTBASyncPostedSalesDoc(SyncImpl);
|
||||
end;
|
||||
|
||||
procedure CMFRTBASyncPostedSalesDoc(SyncImpl: Interface "CMFRT BA ISyncPostedDoc")
|
||||
begin
|
||||
SyncImpl.CMFRTBASyncPostedSalesDoc(Rec);
|
||||
end;
|
||||
|
||||
[IntegrationEvent(false, false)]
|
||||
local procedure OnBeforeCMFRTBASyncPostedSalesDoc(SalesDocNo: Code[20]; var Handled: Boolean)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [codeunit, base-table, entry-point, coupling, architecture, cross-codeunit, subscriber]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT calls to implementation codeunits from base table only
|
||||
|
||||
## Description
|
||||
|
||||
In the CMFRT extension architecture, all calls to implementation codeunits must originate from the base-table object. No AL object other than the base table may call a CMFRT implementation codeunit directly. Event subscriber codeunits handle platform or application events and call the base table's entry-point procedures — not the implementation codeunit directly. Pages and reports call base-table procedures. This keeps the base table as the single integration point for all business-logic calls and ensures that `OnBefore`/`OnAfter` integration events fire consistently regardless of where a workflow begins.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When a subscriber codeunit handles an event and needs to trigger a CMFRT operation, it calls the relevant base-table procedure. When a page action initiates business logic, it calls the base-table procedure. The implementation codeunit is an internal detail of the base table and should never appear in `using` clauses or variable declarations of pages, reports, or subscriber codeunits.
|
||||
|
||||
See sample: `cmfrt-calls-from-base-table-only.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Calling an implementation codeunit directly from a page, report, subscriber codeunit, or any object other than the base table. Direct calls bypass the base table's integration events, making the operation invisible to dependent extensions that subscribed to those events. This also creates hidden coupling between the caller and the implementation, which breaks when the implementation codeunit is renamed or replaced under the interface.
|
||||
|
||||
See sample: `cmfrt-calls-from-base-table-only.bad.al`.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// Two global procedures in one codeunit — mixed concerns, impossible to
|
||||
// apply interface injection independently to each operation.
|
||||
codeunit 2045710 "CMFRT BA Price Utilities"
|
||||
{
|
||||
procedure CMFRTBACreateSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal)
|
||||
begin
|
||||
end;
|
||||
|
||||
// Second global entry point belongs in its own codeunit with its own interface.
|
||||
procedure CMFRTBADeleteExpiredPrices(ItemNo: Code[20]; CutoffDate: Date)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// One codeunit — one global entry point — one interface.
|
||||
interface "CMFRT BA ICreateSalesPrice"
|
||||
{
|
||||
procedure CMFRTBACreateSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal);
|
||||
}
|
||||
|
||||
codeunit 2045710 "CMFRT BA CreateSalesPrice Impl" implements "CMFRT BA ICreateSalesPrice"
|
||||
{
|
||||
procedure CMFRTBACreateSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal)
|
||||
begin
|
||||
CMFRTBAValidateItem(ItemNo);
|
||||
CMFRTBAWriteSalesPrice(ItemNo, UnitPrice);
|
||||
end;
|
||||
|
||||
local procedure CMFRTBAValidateItem(ItemNo: Code[20])
|
||||
begin
|
||||
end;
|
||||
|
||||
local procedure CMFRTBAWriteSalesPrice(ItemNo: Code[20]; UnitPrice: Decimal)
|
||||
begin
|
||||
end;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: architecture
|
||||
keywords: [codeunit, single-responsibility, entry-point, interface, architecture, coupling]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# CMFRT one codeunit one global function
|
||||
|
||||
## Description
|
||||
|
||||
In the CMFRT extension architecture, each implementation codeunit exposes exactly one global procedure. That procedure is the codeunit's entry point and corresponds directly to the single interface the codeunit implements. Helper logic is placed in local procedures within the same codeunit and is never exposed as additional global procedures. This rule enforces single responsibility at the AL codeunit boundary: one codeunit, one concern, one interface, one entry point.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Create one implementation codeunit per functional concern. If a codeunit accumulates a second global procedure that has a distinct concern, extract that procedure into its own codeunit with its own interface. Keep local procedures `local` so callers outside the codeunit cannot bypass the interface contract.
|
||||
|
||||
See sample: `cmfrt-one-codeunit-one-function.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Placing multiple global procedures in a single implementation codeunit. A multi-entry-point codeunit mixes concerns, prevents the interface injection pattern from being applied independently to each concern, and grows into a difficult-to-test utility class. Callers that skip the base-table entry point and call implementation procedures directly bypass integration events and violate the architecture.
|
||||
|
||||
See sample: `cmfrt-one-codeunit-one-function.bad.al`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue