In this series I will explain some of the techniques we use at Procurios to develop software. In our framework, called ProBase, we use modules to partition our codebase. While this is not unusual, in the past ten years we have developed some of our own ways to deal with the complexity of a large codebase. I would like to share this knowledge because we hope it may benefit other projects as well, and we would like to spark your interest in our company: we are looking for enthousiastic developers.
A module contains a collection of functionality with a common purpose. We have created a set of global modules that we reuse for all of our projects, and a set of custom modules for many of our customers.
In this first part I will talk about a part of the module that we just recently developed: the API. An API, or Application Programming Interface, contains the only functions that other modules are allowed to call. It is a fortress wall around the business logic of the module. A module can have one or more API classes that are composed of functions that share a common theme or responsibility.
Before we developed this layer of APIs, the code in one module could call a member of any class in another module directly. It could also directly access the database tables of another module. This caused the following problems:
- It was hard to change a data structure: if you wanted to change a data structure, you would need to find out where this structure was used in all other modules and apply changes in all of those.
- It was hard to find out what a module could do. If you wanted to know if a certain function was possible in a module, you needed to search through all of its classes to find it.
- The temptation to hide functionality in controller code was large, and that meant that even if functionality was present in a module, it could not be accessed and reused, because it was hard coded as a handler to build a specific web page.
To give you an example of what a module API looks like, here's part of the RelationAPI, the API that exposes functionality with regard to relations. I will only show the function signatures, not their contents, since it is not relevant here.
public function createRelation()
public function getPrimaryEmailAddress($relationId)
public function getRelation($relationId)
public function hasReadAccess($relationId, $Object)
Everything you want to do with the relations that are controled by this module, can be done through this API. The data that goes in and out of these functions is as simple as possible, and exposes as little of the interal data structures as can be done. The function createRelation for example, returns an object that contains only getter and setter functions to access its attributes. That's it. If you want to get anything more complex done, go through the API. If want to save the relation, for example, you cannot tell the relation to save itself. It has no function to do so. The reason for this closedness is to allow any part of the module to change easily. In fact, it is an expression of the Law of Demeter.
And if it is not possible to return such a simple object, we have the object implement a simple interface that just exposes the simple getters and setters we desire. This follows the Interface segregation principle.
It is a common practice to stuff all business logic into the model classes, Relation in this example. We did that too. The class would contain all the code of how to store itself, how to export a set of relations, and to build a form to edit its attributes. Large source code files were the result and they were very inflexible. The good thing about it is that the class contains a lot of standard ways of relation functions. However, if you want to do things just a little different, you have two options: 1) add more methods or option-parameters and 2) add new classes for relation methods. Neither of these shows a clear design of how business logic functionality is structured.
Using explicit API classes helps the developer who is new to a module to find its functions quickly. It also allows the original developer of the module to specify which functions may be used by other modules and finally, the module's internals are not exposed and can be changed later on. It is the analog of encapsulation at the module level.