3
votes

I am still new to Ada and not very proficient in the way object orientation is handled in Ada. :(

I would like to know if it is possible to implement a builder like pattern in Ada? This pattern is quite common in the Java programming language.

A simple example: Let's say I want to model a person object. A person has the following attributes:

  • First name
  • Middle name (optional)
  • Last name
  • Date of birth
  • Place of birth (optional)

I could implement four (overloaded) Create functions to cover all possible combinations:

declare
    Person_1 : Person;
    Person_2 : Person;
    Person_3 : Person;
    Person_4 : Person;
begin
    Person_1 := Create(First_Name    => "John",
                       Last_Name     => "Doe",
                       Date_Of_Birth => "1990-02-27");

    Person_2 := Create(First_Name    => "John",
                       Middle_Name   => "Michael",
                       Last_Name     => "Doe",
                       Date_Of_Birth => "1990-02-27");

    Person_3 := Create(First_Name     => "John",
                       Last_Name      => "Doe",
                       Date_Of_Birth  => "1990-02-27",
                       Place_Of_Birth => "New York");

    Person_4 := Create(First_Name     => "John",
                       Middle_Name    => "Michael",
                       Last_Name      => "Doe",
                       Date_Of_Birth  => "1990-02-27",
                       Place_Of_Birth => "New York");
end;

Builder pattern like (don't know if this is possible in Ada):

declare
    Person_1 : Person;
    Person_2 : Person;
    Person_3 : Person;
    Person_4 : Person;
begin
    Person_1 := Person.Builder.First_Name("John")
                              .Last_Name("Doe")
                              .Date_Of_Birth("1990-02-27")
                              .Build();

    Person_2 := Person.Builder.First_Name("John")
                              .Middle_Name("Michael")
                              .Last_Name("Doe")
                              .Date_Of_Birth("1990-02-27")
                              .Build();

    Person_3 := Person.Builder.First_Name("John")
                              .Last_Name("Doe")
                              .Date_Of_Birth("1990-02-27")
                              .Place_Of_Birth("New York")
                              .Build();

    Person_4 := Person.Builder.First_Name("John")
                              .Middle_Name("Michael")
                              .Last_Name("Doe")
                              .Date_Of_Birth("1990-02-27")
                              .Place_Of_Birth("New York")
                              .Build();
end;

First question: How could this example be implemented in Ada?

The Build function could check (at runtime) if all required attributes where initialized by the belonging functions.

Second question: Could this check be delegated (in a magic way) to the compiler so the following example would not compile?

declare
    Person : Person;
begin
    -- Last_Name function not called
    Person := Person.Builder.First_Name("John")
                            .Date_Of_Birth("1990-02-27")
                            .Build();
end;
4
Does java allow to perform such a check at compile time? - user7860670
@VTT: To the extent that Java generics can check compile-time type safety, yes; but the approach may scale poorly for deeply nested, evolving APIs, for example. - trashgod
This doesn’t appear to be the GoF Builder pattern, which I find quite hard to imagine a need for (in any case, but especially in this very simple example) - Simon Wright
Most of these design patterns exist simply to work around deficiencies in the languages the GoF used. For example, the singleton pattern is a workaround for the lack of modules. In Ada, a singleton is simply a package. This pattern appears to be a workaround for the lack of default parameters, as Wright shows. - Jeffrey R. Carter

4 Answers

8
votes

One Ada way of supporting the problem as stated would be to use default values for the parameters whose values aren’t required:

function Create (First_Name     : String;
                 Middle_Name    : String := "";
                 Last_Name      : String;
                 Date_Of_Birth  : String;
                 Place_Of_Birth : String := "")
                return Person;

which accepts all your examples.

3
votes

So yes, this is possible.

I strongly recommend you do not go with this approach. It have very bad performance problems, not to mention is harder to maintain. That being said, it is in fact possible (and should be in any language with dispatching). It's accomplished by an extension to a fluent pattern, which uses an intermediary type, to keep the primary type readonly. Because Ada does not have readonly fields, you will also need to use a property pattern to expose the fields in a readonly way.

Here's the spec

with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;

package Persons is

    type Person is tagged private;

    function First_Name(Self : in Person) return String;

    function Middle_Name(Self : in Person) return String;

    function Last_Name(Self : in Person) return String;

    function Date_of_Birth(Self : in Person) return String;

    function Place_of_Birth(Self : in Person) return String;

    type Person_Builder is tagged private;

    function Builder return Person_Builder;

    function First_Name(Self : in Person_Builder; Value : in String) return Person_Builder;

    function Middle_Name(Self : in Person_Builder; Value : in String) return Person_Builder;

    function Last_Name(Self : in Person_Builder; Value : in String) return Person_Builder;

    function Date_of_Birth(Self : in Person_Builder; Value : in String) return Person_Builder;

    function Place_of_Birth(Self : in Person_Builder; Value : in String) return Person_Builder;

    function Build(Source : in Person_Builder'Class) return Person;

private
    type Person is tagged record
        First_Name : Unbounded_String;
        Middle_Name : Unbounded_String;
        Last_Name : Unbounded_String;
        Date_of_Birth: Unbounded_String;
        Place_of_Birth: Unbounded_String;
    end record;

    type Person_Builder is tagged record
        First_Name : Unbounded_String;
        Middle_Name : Unbounded_String;
        Last_Name : Unbounded_String;
        Date_of_Birth: Unbounded_String;
        Place_of_Birth: Unbounded_String;
    end record;

end Persons;

and the body

package body Persons is

    function First_Name(Self : in Person) return String is (To_String(Self.First_Name));

    function Middle_Name(Self : in Person) return String is (To_String(Self.Middle_Name));

    function Last_Name(Self : in Person) return String is (To_String(Self.Last_Name));

    function Date_of_Birth(Self : in Person) return String is (To_String(Self.Date_of_Birth));

    function Place_of_Birth(Self : in Person) return String is (To_String(Self.Place_of_Birth));

    function Builder return Person_Builder is
    begin
        return Person_Builder'(To_Unbounded_String(""), To_Unbounded_String(""), To_Unbounded_String(""), To_Unbounded_String(""), To_Unbounded_String(""));
    end Builder;

    function First_Name(Self : in Person_Builder; Value : in String) return Person_Builder is
    begin
        return Person_Builder'(To_Unbounded_String(Value), Self.Middle_Name, Self.Last_Name, Self.Date_of_Birth, Self.Place_of_Birth);
    end First_Name;

    function Middle_Name(Self : in Person_Builder; Value : in String) return Person_Builder is
    begin
        return Person_Builder'(Self.First_Name, To_Unbounded_String(Value), Self.Last_Name, Self.Date_of_Birth, Self.Place_of_Birth);
    end Middle_Name;

    function Last_Name(Self : in Person_Builder; Value : in String) return Person_Builder is
    begin
        return Person_Builder'(Self.First_Name, Self.Middle_Name, To_Unbounded_String(Value), Self.Date_of_Birth, Self.Place_of_Birth);
    end Last_Name;

    function Date_of_Birth(Self : in Person_Builder; Value : in String) return Person_Builder is
    begin  
        return Person_Builder'(Self.First_Name, Self.Middle_Name, Self.Last_Name, To_Unbounded_String(Value), Self.Place_of_Birth);
    end Date_of_Birth;

    function Place_of_Birth(Self : in Person_Builder; Value : in String) return Person_Builder is
    begin
        return Person_Builder'(Self.First_Name, Self.Middle_Name, Self.Last_Name, Self.Date_of_Birth, To_Unbounded_String(Value));
    end Place_of_Birth;

    function Build(Source : in Person_Builder'Class) return Person is
    begin
        return Person'(Source.First_Name, Source.Middle_Name, Source.Last_Name, Source.Date_of_Birth, Source.Place_of_Birth);
    end Build;

end Persons;

Then an example program using that package

with Ada.Text_IO, Persons;
use Ada.Text_IO, Persons;

procedure Proof is
    P : Person;
begin
    P := Builder
        .First_Name("Bob")
        .Last_Name("Saget")
        .Place_of_Birth("Philadelphia, Pennsylvania")
        .Build;

    Put_Line("Hello, my name is " & P.First_Name & " " & P.Last_Name & " and I am from " & P.Place_of_Birth);
    Put_Line("Middle Name: " & P.Middle_Name);
    Put_Line("Date of Birth: " & P.Date_of_Birth);
end Proof;

And here's the command line output

enter image description here

Now let me explain. Your primary type is of course Person, with Person_Builder acting as the mutable form of it. Builder converts from Person to Person_Builder and Build converts from Person_Builder back to Person. Person only supports readonly access to the fields through a property pattern. Similarly, Person_Builder supports mutation but not through a property pattern, rather through a fluent pattern which returns the new instance each call. These modifications can then be chained as a result of fluent application.

1
votes

I believe Java has the Builder pattern because it does not support parameters with defaults. The Builder pattern in Java creates a workaround for those who do not want to use function overloading. Ada does have default parameters, so an Ada way to address this need, (without using overloadding) is to use default parameters, as was suggested by Simon Wright.

A benefit of this approach, is that this gives you compile time checking, whereas using the Builder pattern, apparently it is a run-time check. Using the Create function as suggested by Simon, one cannot create a Person that doesn't have a first name, for example.

So in Ada, I'd say there isn't a need to implement a Builder pattern, since better mechanisms are built into the syntax. However, if one did want to implement a builder pattern, my approach would be to use Ada streaming capabilities to build a stream of attribute objects that can be passed into a Build procedure which reads the stream, and builds an object. That is essentially what the Java Build pattern is doing. This however puts the error checking back in the run-time, rather than at compile time, as it does in Java.

-1
votes

From my point of view at first sight, this would require the compiler to know the content of your builder object at compilation time and so this is not possible but I may be wrong.

One solution though, which is not really a builder pattern, might be to declare intermediate types such as

type Person_with_name is tagged record
    First_name : String(1..50);
end record;

type Person_with_last_name is new Person_With_First_Name with
record
    Last_Name : String(1..50);
end record;

type Person_with_last_name is new Person_With_Birth with
record
    Date_Of_Birth : Date;
end record;

And then each you would have in your Builder object, functions returning these types

function LastName(with_first : Person_With_First_Name, last_name : String(1..50)) return Person_With_Last_Name;

function Date_Of_Birth(with_last : Person_With_Last_Name, date_Of_Birth : Date) return Person_With_Birth;

And so on... But that's a bit ugly :D

Please keep in mind that I didn't compile such a code :)

On the other hand, by writing pre- and post- conditions, you could be able to check this property using Spark and then prove that, when calling Build on your Builder object, this latter is correctly initialized.