finish documentation
This commit is contained in:
parent
9baf89b8d2
commit
cff6483fff
2 changed files with 195 additions and 82 deletions
|
|
@ -8,7 +8,7 @@ _Created by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Gang of Fo
|
||||||
|
|
||||||
## Abstract
|
## Abstract
|
||||||
|
|
||||||
The *Builder Pattern* is one of 23 remarkable design patterns described in the book "Design Patterns". Its intent is to make the costruction of complex objects in a beautiful way not so complex, while solving various other object creation problems.
|
The *Builder Pattern* is one of 23 remarkable design patterns described in the book "Design Patterns". Its intent is to make the construction of complex objects more elegant and less complex, while solving various other object creation problems.
|
||||||
|
|
||||||
With the newly introduced *"this"* operator in Version 14 of AL, the Builder Pattern finally found its way to BC. Let me show you how it works.
|
With the newly introduced *"this"* operator in Version 14 of AL, the Builder Pattern finally found its way to BC. Let me show you how it works.
|
||||||
|
|
||||||
|
|
@ -41,30 +41,10 @@ A so-called builder simplifies the creation of objects by allowing the user to a
|
||||||
It exposes several small, focused methods that store data in global variables within the builder.
|
It exposes several small, focused methods that store data in global variables within the builder.
|
||||||
A key feature is that these methods return the builder itself, enabling method chaining for fluent and readable object construction.
|
A key feature is that these methods return the builder itself, enabling method chaining for fluent and readable object construction.
|
||||||
|
|
||||||
<!-- The Builder Pattern provides these feats:
|
|
||||||
|
|
||||||
### Improved readability by avoiding long parameter lists
|
|
||||||
Instead of passing many arguments to a single procedure, the Builder Pattern allows each value to be set individually with clearly named methods, making the code easier to read and understand.
|
|
||||||
|
|
||||||
Handles optional parameters gracefully
|
|
||||||
Only the necessary values need to be set explicitly. Optional fields can be left out, defaulted, or conditionally assigned without overloading methods or writing additional logic.
|
|
||||||
|
|
||||||
Encapsulates complex creation logic in one place
|
|
||||||
All setup steps, validations, and conditionals are contained within the builder object, keeping the main business logic clean and focused while promoting reusability.
|
|
||||||
|
|
||||||
Promotes immutability and consistency
|
|
||||||
The builder ensures that objects are created in a controlled way, reducing the chance of invalid or inconsistent states.
|
|
||||||
|
|
||||||
Enhances testability
|
|
||||||
By centralizing and separating the creation logic, it's easier to write unit tests for how objects are constructed, especially in complex scenarios. -->
|
|
||||||
|
|
||||||
### The Pattern
|
### The Pattern
|
||||||
|
|
||||||
The Builder Pattern is easy to implement and understand.
|
|
||||||
|
|
||||||
|
|
||||||
This pattern is particularly easy to understand and implement because of its clear structure.
|
This pattern is particularly easy to understand and implement because of its clear structure.
|
||||||
Imagine this very simple example of a SMTP Mail setup table, that prove the point of the pattern:
|
Imagine this very simple example of a SMTP Mail setup table:
|
||||||
|
|
||||||
| Field Name | Type |
|
| Field Name | Type |
|
||||||
| ----------------- | ---------- |
|
| ----------------- | ---------- |
|
||||||
|
|
@ -92,16 +72,16 @@ var
|
||||||
Setup: Record "SMTP Mail Setup";
|
Setup: Record "SMTP Mail Setup";
|
||||||
begin
|
begin
|
||||||
Setup.Init();
|
Setup.Init();
|
||||||
Setup."Server" := Server;
|
Setup.Server := Server;
|
||||||
Setup."Port" := Port;
|
Setup.Port := Port;
|
||||||
Setup."AuthRequired" := AuthRequired;
|
Setup.AuthRequired := AuthRequired;
|
||||||
Setup."UserID" := UserID;
|
Setup.UserID := UserID;
|
||||||
if UseSSL then
|
if UseSSL then
|
||||||
Setup."ConnectionType" := Enum::ConnectionType::SSL
|
Setup.ConnectionType := Enum::ConnectionType::SSL
|
||||||
else
|
else
|
||||||
Setup."ConnectionType" := Enum::ConnectionType::Other;
|
Setup.ConnectionType := Enum::ConnectionType::Other;
|
||||||
Setup."UseSSL" := UseSSL
|
Setup.UseSSL := UseSSL
|
||||||
Setup."IsDefaultConfig" := IsDefaultConfig;
|
Setup.IsDefaultConfig := IsDefaultConfig;
|
||||||
exit(Setup);
|
exit(Setup);
|
||||||
end;
|
end;
|
||||||
```
|
```
|
||||||
|
|
@ -126,7 +106,7 @@ begin
|
||||||
end;
|
end;
|
||||||
```
|
```
|
||||||
|
|
||||||
You probably see that the procedure itself as well as the calling of said procedure is or could become very confusing and tedious.
|
As you can see, both the procedure itself and the way it is called can quickly become confusing and hard to maintain.
|
||||||
Let me show you the definition of the corresponding builder.
|
Let me show you the definition of the corresponding builder.
|
||||||
|
|
||||||
```C#
|
```C#
|
||||||
|
|
@ -139,37 +119,37 @@ Codeunit 99999 "SmtpSetupBuilder" {
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure Server(Value: Text[100]): Codeunit "SmtpSetupBuilder" begin
|
procedure Server(Value: Text[100]): Codeunit "SmtpSetupBuilder" begin
|
||||||
SmtpSetupProduct := Value;
|
SmtpSetupProduct.Validate(Server, Value);
|
||||||
exit(this);
|
exit(this);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure Port(Value: Integer): Codeunit "SmtpSetupBuilder" begin
|
procedure Port(Value: Integer): Codeunit "SmtpSetupBuilder" begin
|
||||||
SmtpSetupProduct := Value;
|
SmtpSetupProduct.Validate(Port, Value);
|
||||||
exit(this);
|
exit(this);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure UserID(Value: Text[100]): Codeunit "SmtpSetupBuilder" begin
|
procedure UserID(Value: Text[100]): Codeunit "SmtpSetupBuilder" begin
|
||||||
SmtpSetupProduct := Value;
|
SmtpSetupProduct.Validate(UserID, Value);
|
||||||
exit(this);
|
exit(this);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure UserPassword(Value: SecretText): Codeunit "SmtpSetupBuilder" begin
|
procedure UserPassword(Value: SecretText): Codeunit "SmtpSetupBuilder" begin
|
||||||
SmtpSetupProduct := Value;
|
SmtpSetupProduct.Validate(UserPassword, Value);
|
||||||
exit(this);
|
exit(this);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure AuthRequired(Value: Boolean): Codeunit "SmtpSetupBuilder" begin
|
procedure AuthRequired(Value: Boolean): Codeunit "SmtpSetupBuilder" begin
|
||||||
SmtpSetupProduct := Value;
|
SmtpSetupProduct.Validate(AuthRequired, Value);
|
||||||
exit(this);
|
exit(this);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure ConnectionType(Value: Enum ConnectionType): Codeunit "SmtpSetupBuilder" begin
|
procedure ConnectionType(Value: Enum ConnectionType): Codeunit "SmtpSetupBuilder" begin
|
||||||
SmtpSetupProduct := Value;
|
SmtpSetupProduct.Validate(ConnectionType, Value);
|
||||||
exit(this);
|
exit(this);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure IsDefaultConfig(Value: Boolean): Codeunit "SmtpSetupBuilder" begin
|
procedure IsDefaultConfig(Value: Boolean): Codeunit "SmtpSetupBuilder" begin
|
||||||
SmtpSetupProduct := Value;
|
SmtpSetupProduct.Validate(IsDefaultConfig, Value);
|
||||||
exit(this);
|
exit(this);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
@ -180,6 +160,19 @@ Codeunit 99999 "SmtpSetupBuilder" {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You should note some things in the builder-codeunit above:
|
||||||
|
|
||||||
|
- The builder has a global record variable storing the current building-state called "product".
|
||||||
|
|
||||||
|
- Every field of the table "SmtpSetupBuilder" has its own setter-procedure that is named like the field-caption.
|
||||||
|
|
||||||
|
- The setter-procedures return the codeunit itself with the this-operator.
|
||||||
|
|
||||||
|
- The builder has a init-procedure that initializes a new product.
|
||||||
|
|
||||||
|
- The builder has a build-procedure that copies the building-product into a referenced argument.
|
||||||
|
|
||||||
|
Again. To use the builder you call the procedures like this:
|
||||||
|
|
||||||
```C#
|
```C#
|
||||||
procedure XYZ()
|
procedure XYZ()
|
||||||
|
|
@ -188,15 +181,110 @@ var
|
||||||
SmtpSetup: Record "SMTP Mail Setup";
|
SmtpSetup: Record "SMTP Mail Setup";
|
||||||
begin
|
begin
|
||||||
SmtpSetupBuilder
|
SmtpSetupBuilder
|
||||||
SmtpSetup := CreateSmtpSetup(
|
.Init()
|
||||||
'smtp.office365.com',
|
.Server('smtp.office365.com')
|
||||||
587,
|
.Port(587)
|
||||||
'admin@yourdomain.com',
|
.UserID('admin@yourdomain.com')
|
||||||
'JohnDoe12345',
|
.UserPassword('JohnDoe12345')
|
||||||
true,
|
.AuthRequired(true)
|
||||||
true,
|
.ConnectionType(Enum::ConnectionType::SSL)
|
||||||
true
|
.IsDefaultConfig(true)
|
||||||
);
|
.Build(SmtpSetup);
|
||||||
SmtpSetup.Insert(true);
|
SmtpSetup.Insert(true);
|
||||||
end;
|
end;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Constructing complex objects was never so easy and readable. You have everything capsuled in its own procedure, you know exactly what is being set and have full control of how it is constructed.
|
||||||
|
|
||||||
|
Because the current state of the building-object is stored inside the instance, you can even omit the setting of the same fields, when creating similar objects like this:
|
||||||
|
|
||||||
|
```C#
|
||||||
|
procedure XYZ()
|
||||||
|
var
|
||||||
|
SmtpSetupBuilder: Codeunit "SmtpSetupBuilder";
|
||||||
|
SmtpSetup1: Record "SMTP Mail Setup";
|
||||||
|
SmtpSetup2: Record "SMTP Mail Setup";
|
||||||
|
SmtpSetup3: Record "SMTP Mail Setup";
|
||||||
|
begin
|
||||||
|
// Initialize the fields, that are the same
|
||||||
|
// for all constructing objects.
|
||||||
|
SmtpSetupBuilder
|
||||||
|
.Init()
|
||||||
|
.Server('smtp.office365.com')
|
||||||
|
.Port(587)
|
||||||
|
.AuthRequired(true)
|
||||||
|
.ConnectionType(Enum::ConnectionType::SSL)
|
||||||
|
.IsDefaultConfig(true);
|
||||||
|
|
||||||
|
// Only set the changing fields for each needed
|
||||||
|
// object.
|
||||||
|
SmtpSetupBuilder
|
||||||
|
.UserID('admin@yourdomain.com')
|
||||||
|
.UserPassword('JohnDoe12345')
|
||||||
|
.Build(SmtpSetup1);
|
||||||
|
|
||||||
|
SmtpSetupBuilder
|
||||||
|
.UserID('user1@yourdomain.com')
|
||||||
|
.UserPassword('PasswordIsSafe123')
|
||||||
|
.Build(SmtpSetup2);
|
||||||
|
|
||||||
|
|
||||||
|
SmtpSetupBuilder
|
||||||
|
.UserID('user2@yourdomain.com')
|
||||||
|
.UserPassword('12345')
|
||||||
|
.Build(SmtpSetup3);
|
||||||
|
|
||||||
|
// (...)
|
||||||
|
end;
|
||||||
|
```
|
||||||
|
|
||||||
|
I think this is especially useful, when used in automatic tests while constructing test-data.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The textbuilder is a nice example of the builder pattern implemented in AL.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
Again the nice feats that the Builder-Pattern provides you, when implemented:
|
||||||
|
|
||||||
|
The builder ...
|
||||||
|
|
||||||
|
### ...Improves readability by avoiding long parameter lists
|
||||||
|
Instead of passing many arguments to a single procedure, the Builder Pattern allows each value to be set individually with clearly named methods, making the code easier to read and understand.
|
||||||
|
|
||||||
|
### ...Handles optional parameters gracefully
|
||||||
|
Only the necessary values need to be set explicitly. Optional fields can be left out, defaulted, or conditionally assigned without overloading methods or writing additional logic.
|
||||||
|
|
||||||
|
### ...Encapsulates complex creation logic in one place
|
||||||
|
All setup steps, validations, and conditionals are contained within the builder object, keeping the main business logic clean and focused while promoting reusability.
|
||||||
|
|
||||||
|
### ...Promotes immutability and consistency
|
||||||
|
The builder ensures that objects are created in a controlled way, reducing the chance of invalid or inconsistent states.
|
||||||
|
|
||||||
|
### ...Enhances testability
|
||||||
|
By centralizing and separating the creation logic, it's easier to write unit tests for how objects are constructed, especially in complex scenarios.
|
||||||
|
|
||||||
|
## When not to use
|
||||||
|
|
||||||
|
In some cases the usage of the builder pattern could not be beneficial. Like for example where...
|
||||||
|
|
||||||
|
### ...the object is too simple.
|
||||||
|
If the object has only a few fields or doesn't require conditional setup logic, the builder introduces unnecessary complexity.
|
||||||
|
|
||||||
|
### ... performance is critical.
|
||||||
|
Builders rely on method chaining and object encapsulation, which can introduce a slight performance overhead. This could become a problem when you develop performance-critical batch jobs that process thousands of records.
|
||||||
|
|
||||||
|
### ... the implementation generally leads to overengineering and bloated abstractions
|
||||||
|
Applying the builder pattern everywhere may lead to overengineering and bloated abstractions.
|
||||||
|
A good rule of thumb is to use the pattern only when it adds real value — not just for the sake of applying a pattern.
|
||||||
|
|
||||||
|
## Snippets
|
||||||
|
|
||||||
|
An example snippet is provided [here](/content\docs\patterns\builder-pattern\snippets\AL.json).
|
||||||
|
|
||||||
|
## List of references
|
||||||
|
|
||||||
|
- Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley. ISBN 0-201-63361-2.
|
||||||
|
|
||||||
|
- [Refactoring-Guru: Builder](https://refactoring.guru/design-patterns/builder) - A very nice page for different kinds design patterns with beautiful examples.
|
||||||
25
content/docs/patterns/builder-pattern/snippets/AL.json
Normal file
25
content/docs/patterns/builder-pattern/snippets/AL.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"Generic Builder Codeunit": {
|
||||||
|
"prefix": "tbuilder",
|
||||||
|
"body": [
|
||||||
|
"codeunit ${1:ID} \"${2:YourName}Builder\"",
|
||||||
|
"{",
|
||||||
|
" var",
|
||||||
|
" MyProduct: Record ${3:YourTable};",
|
||||||
|
"",
|
||||||
|
" procedure Init(): Codeunit \"${2:YourName}Builder\"",
|
||||||
|
" begin",
|
||||||
|
" MyProduct.Init();",
|
||||||
|
" exit(this);",
|
||||||
|
" end;",
|
||||||
|
"",
|
||||||
|
" procedure Build(var Result: Record ${3:YourTable}): Codeunit \"${2:YourName}Builder\"",
|
||||||
|
" begin",
|
||||||
|
" Result := MyProduct;",
|
||||||
|
" exit(this);",
|
||||||
|
" end;",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "Builder-Pattern Codeunit with placeholders for ID, Name (ending with 'Builder') and Record-Type"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue