Lesson 2 - Monolithic and two-tier architecture
In the previous lesson, Introduction to software architectures, we explained the concept of dependencies in object-oriented programming, and shown why we should write object-oriented code while examining bad code examples. How the application is composed of the objects is determined by its architecture. Today's software design lesson is all about different architectures.
We can imagine programming an application as building a tower. If the architect designed it wrong, it will fall once it reaches a certain height. It must have a solid backbone structure. Adding more code to the application is bread and butter of programmers, so the application design must be of a high-quality to allow it. The term "quality" means, of course, so good division of object responsibilities that even in hundreds of classes we'll always know where to find the code a certain feature and the whole project will be 100% clear. Does it seem impossible? Certainly not. Perhaps you won't be surprised that we'll introduce more design patterns. Let's take it step by step to get a really detailed overview of the different architectures.
Architectures
Now we'll talk about logical architectures. Don't confuse them with physical architectures that describe the hardware structure, for example, the physical client-server architecture. In this course, we are interested in organizing code into objects.
Monolithic architecture
With this architecture, the whole application works only as a single layer. A layer is a group of objects. Smaller applications are usually written this way. The monolithic architecture is rather an antipattern and we shouldn't use it for larger applications. But, of course, we need to start somewhere
Pipes and filters
Just to have the whole picture, let's mention the pipes architecture used by UNIX operating systems. The functionality is divided into a large number of small separate programs that call each other and transfer data. Perhaps you can transfer data between different commands using pipes in Linux. Linux is also a proof that a system can work well when you build it from hundreds of small programs and each of these programs does well just its small part.
Two-tier architecture
The two-tier architecture is often used with API servers or services in general. Sometimes, we can also encounter the Service-Oriented Architecture (SOA) term, which, besides the software architecture, also includes the logical architecture on the server, typically client-server. Two-tier applications typically don't present data to users. They usually communicate with another applications, handle requests, process them and return the resulting raw data.
Now, we get to the promised architectural design patterns. These are:
- Indirection - We can simplify the references between objects and make the application more clear using middle men. You could say it's a paradox that if we add a purely fabricated layer to the application, which will mediate the communication between 2 object groups, the application will be simpler than without this layer. Sometimes we refer to these classes as service classes. However, we should not confuse it with services in general.
- Controller - The Controller design pattern talks about Indirection even more precisely and focuses on applications that communicate with the user (and that's the vast majority of applications). It says that all the communication with the user should be concentrated in separate objects intended for these purposes, the controllers. These then form the application's control layer. In some modifications, it can be also called the presentation layer. On the other hand, classes containing logical operations should be completely relieved from the communication with the user. Such business logic classes are then called Models, sometimes they are referred to as entity classes, managers or repositories.
So in a two-tier application, we have 2 object groups - Controllers and Models.
If we programmed an API server which we asked for all the cars in the database and it would send them back in an exchangeable format (not as an HTML page but as data for other machine processing, e.g. in JSON), the source code of the application would look as follows:
We divide the application into 2 folders - Models and Controllers.
Models/CarManager.php
The car manager provides the logic for working with cars. That's its
responsibility. Similarly, for example, the users logic would be concentrated in
the UserManager
class and so on. We pass the $database
dependency to the object through its constructor.
class CarManager { private $database; public function __construct($database) { $this->database = $database; } public function getCars() { return $this->database->query("SELECT * FROM cars")->fetchAll(PDO::FETCH_ASSOC); } // Other methods for working with cars, such as adding new cars, searching for cars, removing cars, editing cars ... }
Models don't handle the communication with the user at all, they only provide logical operations within their responsibility.
Controllers/CarController.php
The controller accepts requests from the user. In this case, the user will be the machine calling our API server, but even this counts as a user. The controller calls the appropriate models based on accepted requests, and returns the result of their work. Its purpose is only to mediate the communication between the user and the model/logical layer of the application. Typically, we try to write the controllers as short as possible and they serve only as "wires" between the user and the logic.
class CarController { private $carManager; private $database; public function __construct($database) { $this->database = $database; $this->carManager = new CarManager($this->database); } public function all() { echo(json_encode($this->carManager->getCars())); } // Other actions such as one($id), remove($id), ... }
The code should be clear. The action which is about to be executed is called
in the controller. The controller gets data from the model,
which doesn't know about any users or requests at all. It just takes care of
data within its responsibility, which is cars' data. On the contrary, the
controller doesn't care about the data, it receives them from the model and
deals only with which data the user wants and how will it send it back to them.
It sends these data to the user using the
json_encode()
function. In a real application, we'd pass some more
parameters and the controller might inherit from an ancestor, but let's not
worry about it now.
Because we like to provide complete solutions here on ICT.social, let's also show how the system would call the appropriate controller internally, based on the request. This is a very simplified example, if you find it interesting, a link to the complete implementation and other information can be found below.
index.php
Once index.php
is called, the autoloader is registered to search
classes in the Controllers/
folder first and then in the
Models/
folder. For a smarter implementation, see other PHP courses. If you are programming in a language other than PHP,
then your classes are probably loaded automatically and it doesn't matter in
what folder/package they are.
Let's consider we've opened:
localhost/index.php?controller=Cars&action=all
From this, we need to determine that CarController
should be
created and the all()
method called on it. The code should be at
least intuitively comprehensible, I added some comments to it.
<?php // Simple autoloader // Ignore for other programming languages spl_autoload_register(function($className) { if (file_exists("Controllers/$className.php")) require("Controllers/$className.php"); else require("Models/$className.php"); }); // Creates a database class instance $database = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'root', ''); // Creates the appropriate controller according to the URL address $controllerName = $_GET['controller'] . "Controller"; $controller = new $controllerName($database); // Calls the appropriate method by the URL address on the controller $controller->$_GET['action']();
To get all the cars from the server, we would then open this address:
localhost/index.php?controller=Cars&action=all
And we would get back a JSON as follows:
[ { "car_id":"1", "color":"blue", "license_plate":"123ABC" }, { "car_id":"2", "color":"red", "license_plate":"456DEF" }, { "car_id":"3", "color":"black", "license_plate":"789GHI" } ]
Of course, actions may also have parameters, and we typically use so-called pretty URLs. So a URL address of a two-tier application could also look like this:
application.com/articles/find/php
According to this, the ArticleController
would be instantiated,
and the find
() method called on it with the "php"
parameter. The part of a web application that calls the appropriate controller
by the URL address is called a Router. The example above was
very minimalist. A router that could pass method parameters, supported pretty
URLs and could redirect to error pages, for example, would be little longer. You
can have a look at it in the Simple object-oriented CMS in PHP
(MVC). The functional application can be downloaded in the attachment as
it's usual here.
In the next lesson, Three-tier architecture and other multi-tier architectures, we'll explain the three-tier architecture, including a functional example again.