This post is the second in a series of articles that will be devoted to explaining the Ushahidi Platform at a technical level for programmers and deployers.
In the last blog post, we explored how the architecture of the Ushahidi Platform is radically changing from v2 to v3. In this post, I will detail the different types of objects that make up the new architecture and what their role is within the system.
If we refer back to the diagram that illustrates the Clean Architecture, the very center is consists of the Entities, followed by the Use Cases layer, and then the layer of Gateways, Controllers, and Presenters.
Entities
In the Ushahidi Platform, all entities inherit from an abstract base class calledEntity
, which allows entity objects to define their properties and be constructed from associative arrays. Each entity also includes a Repository interface that defines methods to access that entity type from storage.
For example, this is how the User
entity is defined today:
And this is the corresponding UserRepository
interface today:
Every basic entity repository will have a get()
method, and because the User entity has two other unique fields, username and email, we can also fetch unique records by those values.
Use Cases
The next layer consists of Use Cases, which are responsible for specifying how various parts of the system interact with each other. Every use case defines its dependencies within the__construct()
method and contains an interact()
method.
For example, this is how we define a use case that registers a new user:
You might have noticed that the use case is not using UserRepository
, but rather a RegisterRepository
. This is because every use case also defines the storage interface it needs to persist changes to entities. By separating the interfaces for read access and write access, it is possible to achieve Interface Segregation, one of the five principles that make up SOLID. In this case, the interface currently looks like this:
In addition to the repository interface, every use also defines its expected input as a Data object, which follows the Data Transfer Object pattern. Data objects are similar to entities in that each object defines its public properties and is constructed from an associative array. The data object for this use case currently looks like:
Each use case also validates the data object as input by using a Validator object. Currently these validators are implemented at the gateway level (Kohana Framework layer) but this will most likely change in the near future. For now, just know that the validator interface looks like this:
It is important to note that use cases do not implement any specific logic for validation, authentication, or other concerns. Use cases act only as delegators to other interfaces, which is an example of Single Responsibility.
Gateways and Controllers
Now we can move into the next outer layer, which is currently implemented almost exclusively with the Kohana Framework. This layer is often described as the “delivery layer”, as the code that interacts with the outside world of user input and output lives here. To take user input from this layer and send it into the use case layer, we use objects and interfaces defined within the inner layers, which is an example of Dependency Inversion, one of the five principles that make up SOLID. We also use a Service Locator to gain access to all objects, in order to ensure that all objects can be replaced by other objects that implement the same interfaces. This helps us to achieve Liskov Substitution, as well as Open/Closed programming. In order to create these objects, we use a Parser object to verify that required input has been provided, and then create a data object from that input. Parsers are executed using the magic__invoke()
method, which allows an object to be used as a function:
Note that all the validation that happens here is not validating the input itself, as the parsers are only responsible for ensuring that the required values are provided by the user. Verification of the type and format of the input is handled by the use case. However, because the CSRF security token only matters at the web layer, we do validate it here. This is actually one of the most complex parsers in our system. Most parsers only use not_empty
validation rules.
Once the input data has been successfully created, it can be handed into the use case, which will respond by providing an entity id, an entity object, or a collection of objects. In the case of user registration, only an entity id is given back. In the case of user registration, we can forward the user from registration directly to login, and reuse the username and password provided to complete a user login.