When I do Object Oriented Programming with Ada, I tend to follow a design pattern that makes it easier for me and hopefully also for people reading my code.
For this post I will use the all time classic example of OOP, a Graphic User Interface framework. Here’s what a widget specification looks like:
package Widget.Button is subtype Parent is Widget.Instance; type Instance is new Parent with private; subtype Class is Instance'Class; type Acc is access all Instance; type Any_Acc is access all Class; overriding procedure Event (This : in out Instance; Evt : Event_Kind); overriding procedure Draw (This : in out Instance); private subtype Dispatch is Instance'Class; type Instance is new Parent with record C : Boolean := False; end record; end Widget.Button;
Now let’s have a look at each element.
Type Instance is [...]
package Widget.Button is type Instance is [...] with private;
I define one tagged type (object) per package and the name of this type is always “Instance”.
As we all know, naming is the hardest thing in programming, so having to find meaningful names for both the package and type is annoying at best.
Another solution is to use plural for packages names and singular for the types:
package Widgets.Buttons is type Button is [...] with private;
But there is one other benefit to using the same type name in every package: easier refactoring.
We know that Ada is an amazing language when it comes to safe refactoring, thanks to its strong typing and powerful specifications. One might say that Ada provides “fearless refactoring”. The drawback is that changing the signature of a method, for instance, means a lot of code to edit.
With this design pattern, an inherited methods look exactly the same for all types:
overriding procedure Event (This : in out Instance; Evt : Event_Kind);
So we can just copy/past it everywhere it is needed when the signature changes.
Using the other plural/singular naming convention, the type of “This” changes every time.
overriding procedure Event (This : in out Button; Evt : Event_Kind); overriding procedure Event (This : in out Checkbox; Evt : Event_Kind);
This may look like a detail but it makes sense to me, and I like the consistency of this convention.
The declaration of widgets looks like this:
with Widget.Button; with Widget.Checkbox; use Widget; [...] B : Button.Instance; C : Checkbox.Instance;
Class, Acc and Any_Acc
subtype Class is Instance'Class; type Acc is access all Instance; type Any_Acc is access all Class;
The next types and subtype declaration follow the same idea, they are always the same for every object.
The subtype “Class” is useful when writing class-wide subprograms, e.g.:
procedure Something (B : Button.Class);
The type “Acc” is a general access type for the instance:
B : Button.Acc := new Button.Instance;
The type “Any_Acc” is a access type for any object in the hierarchy:
procedure Something (B : not null Button.Any_Acc);
You can even define more access types like:
type Const_Acc is access constant Instance; type Any_Const_Acc is access constant Class;
And if you don’t like “Acc” you can use “Reference” or Ref, just stay consistent across your hierarchy:
type Reference is access all Instance; type Any_Reference is access all Class; type Ref is access all Instance; type Any_Ref is access all Class;
subtype Parent is Widget.Instance;
subtype Parent is Widget.Instance; type Instance is new Parent with private;
Emmanuel Briot already wrote a blog post here on this pattern. Always defining a subtype that names the parent type of the object makes inherited subprogram call easy, readable and safe:
overriding procedure Event (This : in out Instance; Evt : Event_Kind) is begin Parent (This).Event (Evt); end Event;
I will let you read Emmanuel’s post to see the pitfalls of other approaches.
subtype Dispatch is Instance'Class;
private subtype Dispatch is Instance'Class;
In Ada, dynamic dispatching on subprograms only occurs on class-wide types. This is unsettling for many, and I was myself caught by this when I started OOP in Ada.
By defining a “Dispatch” subtype, we can make dispatching call explicit, easier to spot and use:
overriding procedure Draw (This : in out Instance) is begin Dispatch (This).Event (Draw_Event); end Draw;
I hope this pattern will be useful to some of you. Let me know in the comments what is your opinion on this, and maybe what other patterns you are using in Ada.