This article concludes the series on ProBase module design. It provides an overview of the main parts of the architecture and shows how the previous articles fit in.
The diagram below shows all parts of our new module architecture. The concentric circles and arcs represent parts of the module, that are built from classes and interfaces. The blue arrows represent the control flow of a request. The pink dashed arrows represent dependency injection relations.
As you can see, the diagram is formed in a way to show that Business Logic is central to the architecture. The Business Logic also provides Interfaces for its dependencies: the code that it uses, but that is not part of it. The reason for this position for the "logic" part of the code is described in part 5.
The Logic code may only be accessed via the API layer. This layer consists of one or more API classes. This is to ensure that the business logic may change its implementation while still keeping a stable outside appearance. API's were described in part 1.
The API layer is again wrapped by a single API Factory class. This class creates the default dependencies of the API so that they need to be built only in a single place. Only tests access the API's directly, while passing mock object dependencies. The factory was described in part 2.
Diagram: The ProBase module architecture
Let me explain the architecture by going through two common ways of its use:
The page request
When a page request is made, ProBase analyses the request url and uses the Index to map this url to a Controller function. This process was described in part 3.
The Controller actually handles the request. It builds a view and calls the necessary API's to move data to the view and back to the model. A view may be a webpage with a form, or an Ajax call. But is can also be a PDF file that needs to be generated. The API's are first requested from the module's own API Factory and from the API Factories of dependent modules.
The factory instantiates the required repositories (the model) and other dependencies and passes these to the constructor of the API. Read more about repositories in part 4.
The API's logic specifies certain algorithms. When these algorithms need to access external code, they call the dependencies. Repositories are an common example of such dependencies. When an API needs to combine data from different repositories, it may use techniques like the one described in part 6.
Each test method tests the functionality of single function of an API. The API specifies what the module does. First it mocks the external objects that the API needs. Then it instantiates the API and passes these dependency mock objects. The API's logic then calls the dependent classes when it needs them. The logic specifies how the module works. Most logic is located in the API classes, but when it becomes to wieldly, it can be separated in other classes.
The separation of model code and logic code makes it easier to test just the code that is specific to this module. The API's make it clear what needs to be tested. The tests make sure that the output of the API's function is, and stays, that what the developer expects it to be.
The introduction of API's and repositories was a big step for us. In general, it is seen as a step forward, But we have also found a number of cases where it still needs to be improved. In some cases the architecture causes the creation of too much "boiler plate" code. Also, the API factory is too dominant and needs to be integrated more subtly into the architecture. For all of us this is a journey into terra incognita, and we hope to avoid the dragons that may live there :)
This series does not describe the module exhausively. We haven't even touched event handling, plugins, translation, global code, and the use of library functions. I justed wanted to give you an inside look into the way we tackled the challenge of creating robust large-scale web applications. It really helps us to create better code.