Fortran: Service Composition, part 1: Aggregation
Matthias Noback
With our new service abstraction logger_t
we are able to easily implement more features for our logger. We can do this without affecting current user code, because their code relies only on the abstraction, never on concrete implementations.
A terminal output logger
One thing we can do is define a second logger type, one that prints each message to the terminal output (stdout
). Being a logger, it should also extend the abstract logger_t
type, and provide an implementation for log()
:
module logging
! ...
type, extends(logger_t) :: stdout_logger_t
contains
procedure :: log => stdout_logger_log
end type stdout_logger_t
contains
! ...
subroutine stdout_logger_log(self, message)
class(stdout_logger_t), intent(in) :: self
character(len=*), intent(in) :: message
print *, message
end subroutine stdout_logger_log
end module logging
The multi-logger
Now how would we go about logging both to the screen and to a file? Because we have a logger_t
abstraction, we could make a new subtype called multi_logger_t
. Its data components should be a file_logger_t
and an stdout_logger_t
instance:
type, extends(logger_t) :: multi_logger_t
private
type(file_logger_t), allocatable :: file_logger
type(stdout_logger_t), allocatable :: stdout_logger
contains
procedure :: log => multi_logger_log
end type multi_logger_t
A call to its log()
procedure should do nothing new, except call both of these logger’s log()
procedures, passing the original message:
subroutine multi_logger_log(self, message)
class(multi_logger_t), intent(in) :: self
character(len=*), intent(in) :: message
call self%file_logger%log(message)
call self%stdout_logger%log(message)
end subroutine multi_logger_log
Now we only have to modify the get_logger()
factory to return this new multi_logger_t
instead of just the file_logger_t
:
function get_logger() result(logger)
class(logger_t), allocatable :: logger
class(logger_t), allocatable, save :: shared_logger
if (.not. allocated(shared_logger)) then
shared_logger = multi_logger_t( &
file_logger_t('debug.log'), &
stdout_logger_t() &
)
end if
logger = shared_logger
end function get_logger
Automatically, any user code that calls get_logger()
(even indirectly, when they still call the old log()
subroutine) will benefit from the new terminal output logging. It proves that we can safely evolve our design and add new functionality behind the logger_t
abstraction. That’s very powerful.
An array of abstract loggers?
The multi_logger_t
is oddly specific, because its data components specifically mention the exact types (file_logger_t
and stdout_logger_t
) that can be assigned. For multi_logger_t
’s log()
implementation, the concrete type is irrelevant. So we can make multi_logger_t
more flexible by allowing any logger, i.e. class(logger_t)
as the type of the data components:
type, extends(logger_t) :: multi_logger_t
private
class(logger_t), allocatable :: logger_1
class(logger_t), allocatable :: logger_2
contains
procedure :: log => multi_logger_log
end type multi_logger_t
However, allowing only two “child” loggers is quite limiting. We should make that more flexible too, by using an allocatable array of logger_t
instances instead. By specifying dimension(:)
, this array can be of any size:
type, extends(logger_t) :: multi_logger_t
private
class(logger_t), dimension(:), allocatable :: loggers
contains
procedure :: log => multi_logger_log
end type multi_logger_t
Unfortunately, this code - that looks intuitively right - results in a compiler error:
error #6315: The array-constructor has ac-values of differing types.
stdout_logger_t() &
----------------------------------------^
I think this very surprising. The array is typed as a class(logger_t)
array, which would mean any subtype of logger_t
could be stored in it. The values in the array are subtypes of logger_t
so this should work. Anyway, it’s not allowed.
What about this approach? Inside get_logger()
we first allocate the array to 2 elements, then assign the specific loggers:
type(multi_logger_t), allocatable, save :: multi_logger
if (.not. allocated(multi_logger)) then
allocate (multi_logger)
allocate (multi_logger%loggers(2))
multi_logger%loggers(1) = file_logger_t('debug.log')
multi_logger%loggers(2) = stdout_logger_t()
end if
This results in a different error:
error #8282: If any object being allocated in the ALLOCATE statement
is of abstract type, either type specification or SOURCE= or MOLD=
specifiers shall appear. [LOGGERS]
allocate (multi_logger%loggers(2))
--------------------------------^
We have to conclude that we can’t allocate an array of abstract types, unless we tell which exact type should be used for all the elements. This is something we don’t want to do because we actually want to allow each element to be of a different type. We want to make room for polymorphism.
An array of logger references
So we need an array of concrete types, each of which holds an abstract type. This requires the introduction of an intermediate concrete type, one that we could call logger_reference_t
:
type :: logger_reference_t
class(logger_t), allocatable :: logger
end type logger_reference_t
Now we can change multi_logger_t
to hold an allocatable array of logger_reference_t
instances:
type, extends(logger_t) :: multi_logger_t
private
- class(logger_t), dimension(:), allocatable :: loggers
+ type(logger_reference_t), dimension(:), allocatable :: logger_references
contains
procedure :: log => multi_logger_log
end type multi_logger_t
When instantiating the multi_logger_t
inside get_logger()
, we have to “wrap” our concrete loggers inside logger_reference_t
instances:
if (.not. allocated(shared_logger)) then
shared_logger = multi_logger_t([ &
- file_logger_t('debug.log'), &
+ logger_reference_t(file_logger_t('debug.log')), &
- stdout_logger_t() &
+ logger_reference_t(stdout_logger_t()) &
])
end if
No more compile errors: all the elements of the logger_references
array are of the same type (logger_reference_t
), yet each of the elements can hold a different type of logger, as long as it’s a subtype of logger_t
.
Delegating to any number of loggers
The implementation of the log()
procedure of multi_logger_t
can use a do
loop to iterate over the logger references. For each reference it calls the log()
procedure of the logger
data component:
subroutine multi_logger_log(self, message)
class(multi_logger_t), intent(in) :: self
character(len=*), intent(in) :: message
integer :: i
do i = 1, size(self%logger_references)
call self%logger_references(i)%logger%log(message)
end do
end subroutine multi_logger_log
We have successfully aggregated an undefined number of similar services under the same abstraction. The result is that multiple of those services now behave as a single service. The behavior of this new service consists of the behavior of the subsumed services. The way this happens is often called delegation. In our example: the log()
call is delegated to all the loggers.
In the next post we look at another way to compose abstract services, namely decoration.