What is a “Composition Root” and what does it do?

Quoting van Deursen and Seemann (2019, 85):

The COMPOSITION ROOT composes the object graph, which subsequently performs the actual work of the application.

I think this one by Freeman and Pryce (2010, 13) also helps:

An object-oriented system is a web of collaborating objects. A system is built by creating objects and plugging them together so that they can send messages to one another. The behavior of the system is an emergent property of the composition of the objects […]

So a Composition Root creates objects and combines them – ie, Composes them – so that they can then do whatever is required to be done by the application. van Deursen and Seemann discuss Composition Root as a pattern related to Dependency Injection in their book Dependency Injection: principles, practices and patterns. According to them, Composition Root is the answer to the question: “If objects are not supposed to create their dependencies, where are those then supposed to be created?” van Deursen and Seemann propose that as the responsibility for creating dependencies is pushed out to the clients, object creation will ultimately concentrate in a single place in the application, the Composition Root. It will be the single point responsible for creating the objects and any required dependencies.

Why should we bother with a Composition Root? Isn’t it much easier to create dependencies directly with NEW at the point when they are needed? It might be, yes, but such practices create tightly coupled code that will be hard to test. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CLASS zcl_delivery_rest_api DEFINITION.

  PUBLIC SECTION.
    METHODS get_inbound_deliveries
      RETURNING VALUE(r_result) TYPE zmm_t_inbound_delivery.

ENDCLASS.

CLASS zcl_delivery_rest_api IMPLEMENTATION.

  METHOD get_inbound_deliveries.
  
    cl_http_client=>create_by_destination(
      EXPORTING
        destination = 'DELIVERY_API'
      IMPORTING
        client      = DATA(lo_http_client)
    ).
	
    " Proceed to call the API using lo_http_client
  
  ENDMETHOD.

ENDCLASS.

The above example does not directly create the required HTTP client dependency (lo_http_client) using NEW, but a static factory method (without any support for injecting test doubles) produces an identical situation: it does not allow the HTTP client to be intercepted or replaced with a mock for testing. “There’s no seam,” as Michael Feathers might describe the situation (he might also suggest to extract a factory method to create an object seam for testing, but we’ll focus on Dependency Injection). If we cannot control what the HTTP client does, it will be hard to test success scenarios and especially arduous to test error scenarios.

If the zcl_delivery_rest_api class was created with Dependency Injection in mind, the class would request HTTP client instances from some other object. Then it would be easy to mock the HTTP client for testing and to substitute it for other objects to add or change features.

van Deursen and Seemann spend chapter 7 of their book on the topic of application composition. They go over composition in console applications (think anything where there is a clear entry point like C’s main()) and give specific examples for Universal Windows Programming (UWP) and ASP.NET Core MVC. Given that a previous edition of the book was called “Dependency Injection in .NET,” ABAP examples are obviously not included. The part about console applications can be easily applied to ABAP reports, but there is little help for cases like web services, custom outputs, ABAP Push Channels or OData services.

The Principle

van Deursen and Seemann propose (2019, 85) that the following is used as the guiding principle for where to locate a Composition root:

Where should we compose object graphs?

As close as possible to the application’s entry point.

This suggests that it is crucial to know the flow of program execution. This is easy for basic ABAP reports, but harder for some other development artifacts.

ABAP Reports

The humble (executable) report is the easiest case, as it’s mostly analogous to command line programs described by van Deursen and Seemann (2019, 213–218). The only major difference is that SAP reports can include a “Selection screen” which is displayed before the rest of the program is executed.

Selection screen

However, a selection screen typically involves very little logic. It is mostly declarative and often just passes inputs from the user to the program. The screen in the previous screencap was created with the following snippet:

1
2
3
4
PARAMETERS pa_plant TYPE werks_d. " <- Will contain FI01 from the previous screenshot

TABLES mara. " Required for declaring a select option
SELECT-OPTIONS so_matnr FOR mara-matnr.

I think a selection screen can be thought of as an argv for ABAP reports.

The execution of an ABAP report happens like this:

ABAP report sequence

LOAD-OF-PROGRAM, INITIALIZATION and START-OF-SELECTION are “events,” named blocks of code in an ABAP report:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
REPORT zhello_world.

DATA gv_greeting_start TYPE string.
DATA gv_greeting TYPE string.

LOAD-OF-PROGRAM.
  gv_greeting_start = 'Hello'.


INITIALIZATION.
  gv_greeting = |{ gv_greeting_start }, { sy-uname }|. " sy-uname is automatically filled


START-OF-SELECTION.
  WRITE gv_greeting. " Would print 'Hello, user'

LOAD-OF-PROGRAM is executed when a report is loaded and only executes once per report in an internal session. INITIALIZATION is always executed before the first selection screen of the report is called.

Events are optional. In a report without explicit events all ABAP statements that are not data or parameter declarations are collected under an implicit START-OF-SELECTION event.

Based on the sequence graph, where should we place the Composition Root? LOAD-OF-PROGRAM is obviously the earliest we can execute any code of our own. However, placing the Composition Root there means that it must be separate from the main entry point which will be placed in START-OF-SELECTION and we’ll thus need to create two methods, for example =>initialize( ) and =>run( ).

Also, depending on how the report is accessed, LOAD-OF-PROGRAM might or might not execute (SUBMIT creates a new internal session, whereas PERFORM subroutine IN PROGRAM x does not), making it harder to reason about when exactly does what execute.

How about INITIALIZATION? It executes each time the report is run. But it too is separate from the main entry point in START-OF-SELECTION:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
REPORT zsome_report.

PARAMETERS pa_matnr TYPE matnr.


INITIALIZATION.
  lcl_main=>initialize( ).


START-OF-SELECTION.
  lcl_main=>run( ).

Using transactions also introduces situations where INITIALIZATION is not executed. But I think that separating =>initialize( ) and =>run( ) is pointless, especially when the selection screen is simple.

Thus we are left with START-OF-SELECTION, which now serves as our main() -like single point of entry. As our code takes over, we can do the work of the Composition Root before continuing into the actual work of the report, as we’d do in a more familiar command line program.

Placing the main entry point in START-OF-SELECTION (and the Composition Root therein) is almost always the correct choice. Some transaction types are an exception to this rule, however.

Transactions

In day-to-day usage, SAP users do not execute reports by name. Instead they access whatever’s required via “transactions.” For example, a sales order is created using transaction VA01, can be edited afterwards using transaction VA02 and a delivery for it can be created using transaction VL01N.

Although creating, modifying and displaying a sales order all could be distinct reports, they will inevitably have much similar or identical functionality. Transactions allow a single report to have “multiple names”: accessed with one, a sales order can only be displayed; using another, it can be modified.

And as it turns out, VA01, VA02, VA03 (display sales order) are all actually ways of accessing the same report: SAPMV45A.

There are different types of transactions:

  1. Program and dynpro - Execution starts from a specific screen
  2. Program and selection screen - Execution starts from a specific selection screen
  3. Method of a class - Execution starts from a class method
  4. Transaction with variant - Previously defined transaction with saved values
  5. Transaction with parameters - Previously defined transaction with specified values

Of these, 2. is portrayed in the ABAP report sequence sequence graph.

For 1. the sequence graph looks like this:

Screen transaction

As you can see, no events other than LOAD-OF-PROGRAM are executed. After it the earliest that any of our code gets executed is in the Process Before Output (PBO) modules of the executed screen:

1
2
3
4
5
6
7
8
PROCESS BEFORE OUTPUT.
  MODULE initialize_program.
  MODULE initialize_screen.
  MODULE status_3000.


PROCESS AFTER INPUT.
  MODULE user_command_3000.

Screen processing has to pull double-duty by initializing both the object graph and the screen state. This also implies that there is a similar separation between initialization and main program execution as there was when the Composition Root was placed in INITIALIZATION.

I personally find this to be unusual and confusing: program initialization almost always takes place before any screen is called, whereas with this type of transaction the screen serves as the entry point of the program. “Method of a class” transactions serve as a better alternative to screen transactions:

Object-oriented transaction

With this kind of transaction, we can set lcl_main=>run( ) as the entry point, compose the object graph and then proceed to call any required screens. Note that here, too, INITIALIZATION and START-OF-SELECTION are missing.

Transactions and events can also be mixed: a report could have a default entry point in START-OF-SELECTION and a method as an alternative entry point via a transaction. Or multiple different methods with different transactions.

Proto-Compositions Roots

All of the following are typical examples of what one might see in a START-OF-SELECTION event.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
" Option 1
START-OF-SELECTION.
  PERFORM initialize.
  
  CALL SCREEN 3000.
  

" Option 2
START-OF-SELECTION.
  PERFORM run.
  

" Option 3
START-OF-SELECTION.
  CALL SCREEN 3000.
  
  
" Option 4
START-OF-SELECTION.
  PERFORM select_data.
  
  CALL SCREEN 3000.


" Option 5
START-OF-SELECTION.
  DATA(go_object) = NEW lcl_class( ).
  
  go_object->initialize( ).
  
  CALL SCREEN 3000.

In broad strokes, they do the same thing with some variation: initialize some state and then proceed into the main entry point, which takes the form of a form or a screen, depending on the type of report in question. Some collapse both initialization and execution together, either to take place in screen processing (like a screen transaction, with similar cons) or in a subroutine (somewhat like a lcl_main=>run). Occasionally you see the initialization step inlined into START-OF-SELECTION (in the worst case declaring global variables inline).

I think these are all examples of Composition Roots taking shape. Strictly speaking, the initialization step is where we expect to see the work of the Composition Root done.

As you can see, however, the subroutines don’t accept any parameters and screens can’t even have parameters. This means that any and all data shared between the two will have to be declared as global data. This global data can only be eliminated to a point (screens require global variables to pass input/output to/from the GUI), but I think it’s worth the effort to only include the global variables that are absolutely required (ie. those required by screens). Although global variables in a report are not nearly as awful as global data via static attributes of global classes or such, in a large program they make things harder to understand than is necessary.

Subroutine

Somewhat perplexingly all subroutines of a program can be called from other programs.

1
PERFORM a_subroutine IN PROGRAM zanother_program.

Given that function modules and classes provide a better alternative for modularizing code, there’s little reason to use this option extensively. However, in some cases it’s required.

For example, when defining a custom output type, a program and a subroutine need to be defined as the entry point for processing. In such cases – ie. when a subroutine acts as the entry point called from outside the program – I think it makes sense to treat it like START-OF-SELECTION: it is a named entry point which then calls =>run( ) or some other method to invoke the main execution (including the Composition Root), somewhat like in an object-oriented transaction.

Report with a subroutine

1
2
3
4
5
6
7
8
TABLES nast.


FORM custom_output_entry.

  lcl_main=>run( im_nast = nast ).

ENDFORM.

Web services

SAP has a framework which allows function modules to be turned into SOAP-based web services (think REST with additional metadata). Although SOAP is not the hottest technology on the block, function modules turned into web services are a battle-tested, convenient and familiar way to expose SAP services and data to other systems and to create integrations.

As for where to put the Composition Root, the sequence graph looks like this:

Web service sequence 1

The case is quite simple: the function module is invoked once per request and executes from beginning to end. All that SAP contributes is that it passes parameters into the function module and passes return values back to the caller. Unlike with reports, there are no alternative scenarios like transactions and such.

However, I think confusion arises with web services. A web service function module is, like all other function modules, a module of code and thus one tends to think that the work should also be done there. But when a function module is created with a web service definition in mind, the role of the function module is not to perform work. Instead, the function module is just a development artifact we use to define an interface based on which SAP can then generate a service definition and which then serves as the entry point – and the Composition Root – for that web service. In other words, the function module is “special” and should not be implemented like “normal” function modules.

Unfortunately it’s not rare to see a web service with hundreds of lines of code, often without any modularization: each step just follows the previous in the same scope. Such implementations would benefit from extracting the implementation into a class and then extracting the individual steps into clearly named methods. And while we’re modularizing the code, we could add dependency injection via the constructor.

We’d end up with a class that supports dependency injection (and is thus testable), all code would be in a class to allow for easier refactoring in the future and our function module serves only as a Composition Root/interface definition. Win-Win.

Web service sequence with command object

(Seeing a LOAD-OF-PROGRAM in a function module sequence might look strange, but this is due to the fact that function groups and classes are both technically special reports and thus inherit some implementation details from basic reports. Note that a class cannot have a LOAD-OF-PROGRAM, but class_constructor is basically the same thing)

OData

OData is a protocol supported by SAP for mobile&web applications. It’s in some ways similar to SOAP: it’s carried over HTTP and it has metadata that defines the services offered, the data types involved and the links between data types.

The case of Composition Roots in OData services is quite similar to web services, though more object-oriented. Instead of function modules, we define structures that represent entities that are offered for retrieval & manipulation via the service. SAP then generates a class based on the these structures. This generated class has methods that are invoked based on the entity (defined by the aforementioned structures) and the operation (defined by HTTP methods). The generated class is also automatically subclassed, and it is this generated subclass that all our custom developments are meant to be done in. Because the (super)class is automatically generated, its constructor parameters are set in stone: there are none because SAP will not supply any.

At first glance it seems like we cannot practice Dependency Injection or use a Composition Root because SAP automatically creates the instance for us (without any parameters) and then invokes its methods based on the request. We’re forced to practice Control Freak anti-pattern (van Deursen & Seemann 2019, 127), creating dependencies in the constructor or in the methods as necessary. We could maybe support testing by creating all dependencies in the constructor and then replacing those with mocks by redefining the constructor, which would be better than nothing.

However, this is a misunderstanding similar to the one that applies to web service function modules. Although the generated classes look like normal classes, it does not mean that we should treat them like all other classes. Like the function module, these classes really only exist to define an interface and to serve as the entry point to our service. No real work has to, or even should, be done in them. Instead, we can follow SAP’s lead and take advantage of subclassing: we can create a Delegate which inherits the interface from the superclass and to which the automatically generated subclass will then delegate requests:

OData class diagram

Because our code (in the generated subclass) creates this Delegate object, we have the freedom to use Dependency Injection in the Delegate. The original subclass that SAP automatically generated will now serve as the Composition Root which creates the Delegate that does the actual work. Because the Delegate is designed with Dependency Injection in mind, it is easy to unit test it using mocks.

This setup creates the following sequence graph:

OData sequence

The Delegate could also be created in the constructor of the generated class, but the main thing is that most of our OData code is now in an object that uses Dependency Injection and we have recognized a place for our Composition Root.

ABAP Push Channels

ABAP Push Channels (APCs) allow SAP systems to support WebSocket connections for push notifications and other uses in browser apps.

The main issue with APC and OData is basically the same: the base class of the implementation is automatically generated and invoked by SAP. This special class has a constrained constructor (van Deursen & Seemann 2019, 154), ie. constructor with no parameters. This then rules out Dependency Injection in this special class.

Likewise, the solution is more or less the same: we create a Delegate and only use the automatically generated class as the service entry point and the Composition Root. In contrast with OData, APC is even simpler because the interface that the Delegate has to implement is completely general, whereas in OData the interface was defined by the generated class. This is because in APC only text/binary data moves through the connection and the service is responsible for interpreting/deserializing it, whereas deserialization is done automatically in OData.

APC class diagram

The sequence diagram also looks more or less the same, the main difference is that an APC connection might handle multiple messages/requests (though this “statefulness” depends on the APC service settings):

APC sequence

Sources

  • Feathers, M. (2005) Working effectively with legacy code. Prentice Hall Professional Technical Reference. 978-0-13-117705-5.
  • Freeman, S. – Pryce, N. (2010) Growing object-oriented software, guided by tests. Addison Wesley. 978-0-321-50362-6.
  • van Deursen, S. – Seemann, M. (2019) Dependency Injection: principles, practices, and patterns. Manning Publications. 978-1-61729-473-0.