Lesson 1 - Introduction to software architectures
There's a big fuss over the dependency injection. This design pattern is used by larger frameworks to pass dependencies within the application. Understanding DI is not quite trivial, it took me quite a while to start using it at least intuitively and I understood the pattern completely not before I implemented it myself in my framework. In this software design course, I'll guide you through different ways of passing dependencies within the application, we'll attempt to use various mechanisms to pass dependencies step by step and come to a conclusion that DI is the only right option. We'll, of course, show examples and describe them in detail. This course puts the DI into the context of other bad dependency management practices such as Singletons or ServiceLocators and explains their disadvantages. This is why the course is different from other separate articles here on the network, where different ways of passing dependencies are described separately, but the context disappears, making it very difficult to understand a fundamentally simple principle. At the end of the course, we'll even show a minimalist implementation of our own DI container in just 50 lines. This course assumes you know the fundamentals of the object-oriented programming.
Dependency Injection (DI)
Although DI is used mainly in web frameworks, it's not limited to web applications. The problem of sharing dependencies between objects exists in every object-oriented application even though it has no more than 10 classes. Without DI, design problems escalate with the increasing number of classes. As an example, I chose a simple web application in PHP, but the DI is, of course, used the same way in Java (beans) or in C# .NET or, eventually, in other languages. You'll certainly find this knowledge useful during job interviews, I can confirm that they ask about it almost everywhere. It's a kind of knowledge advanced programmers have.
What is a dependency?
Information systems usually consist of a large amount of code. For the comparison, the ICT.social system has several hundreds of thousands of lines of code. In order to understand such code, we divide information systems into objects. ICT.social is composed of hundreds of objects that are gathered in namespaces (packages). According to the SRP (Single Responsibility Principle), each object should be responsible only for one area. The resulting application is then composed of a larger number of smaller objects that communicate with each other. By this division, we'll get readable and reusable components. We can compose other applications from existing universal objects. In monolithic applications written in a single file, it's often a problem to use any part anywhere else and to even read the code at all.
SRP is related to other principles:
- High Cohesion - The responsibility for a certain
area of the application (e.g. user management) should be
concentrated to one place, in the minimum number of classes
UserManagershould be responsible for users).
- Low Coupling - The responsibility should
be assigned to objects so that they have to communicate with as few
other objects as possible. Since objects focus only on a small part of
the application, they logically need to use the features of other objects from
time to time. E.g. the
UserManagerwill not normally communicate with a car manager, it shouldn't have any reason to do so. There are few other principles how to divide responsibility, but they are not the subject of this course.
It happens in every object-oriented application that an object needs to
communicate with another object. That's how dependencies are
created. Of course, we don't want objects to be created over and over again, but
to pass just one single instance of each dependency (sometimes
referred to as services) to where it's needed. For example, we'd create an
instance of the
Db database class once and then pass it to all the
objects that need it to communicate with the database, such as to the
UserManager class. We'd certainly think of many similar services
needed by other objects, besides the database, these are e.g. email senders,
loggers, the currently logged user, and so on.
Dependency is when an object needs to access other objects. Sometimes we talk about composing objects, when one object uses several other objects to function.
How it all began
In order to understand all the DI benefits, let's take it from the beginning. I've already mentioned that we'll write examples in the PHP language. It has a standard C-like syntax, so it should be readable for the vast majority of programmers. I think it's also great for examples, because you can very easily project the principles into your programming language.
In early applications, people used to connect to the database at the beginning of a file, executed an SQL query, rendered some HTML, printed data using the programming language, and then rendered the rest of the page. Such an application looked like this:
<?php $database = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8mb4', 'name', 'password'); $cars = $database->query("SELECT * FROM cars")->fetchAll(); ?> <table> <?php foreach ($cars as $car) : ?> <tr> <td><?= htmlspecialchars($car['licenseplate']) ?></td> <td><?= htmlspecialchars($car['color']) ?></td> </tr> <?php endforeach ?> </table>
We can see that SQL is mixed with PHP (the PDO database library) and with HTML code. All this in a single file. In this example, the chosen strategy seems to be correct. However, when there are tens of thousands of lines in a single file and we'll try to debug an SQL query mixed with a code for button styling, we'll surely find out that this is not the way we can create a real commercial application. Even ICT.social worked this way in the past, but it's been quite a long time ago. We were in a high school and we had no experience with software architecture yet.
Even though we can split the application into multiple files, we'll still never have complete control over it. All the files will have access to all data from the other files. This creates global couplings and we'll start to overwrite our data and identifiers uncontrollably. One can just choose the same function name or variable name as was used in some another file. And it's very easy to do so, trust me Fortunately, in modern languages, a similar non-object-oriented code can't be even written anymore.
We wouldn't solve the problem by splitting the code into multiple functions
either. Functions, unlike object methods, have no context. Even if we
create multiple files, each with several functions, we'll not avoid function
name collisions. PHP solved the problem with the huge number of its
internal functions by giving them all really ugly and long names that are mostly
even not standardized. For example, most PHP array functions start with
array_, but we get the length of an array by the
count() function. When functions don't belong to objects, we'll get
lost in their names after a while and. We'll have hard times sharing data
between them, which will result either in using dangerous global variables or in
passing too many parameters.
Let's show one more bad example from the Wordpress content management system, probably one of the both worst-designed and popular applications in PHP. See what happens if you write an application with completely no architecture:
Only a small demonstration of global functions shows that some start with "the", probably colliding with others without "the", some with "wp_", some with "get_". Urgh. Hopefully, I have convinced you that functions need to be bound to objects to avoid name collisions. Multiple objects can then easily have a function with the same name, it can be easily determined which function to call according to the object and our IDE also offers function lists automatically when typing their names. We don't have to remember them anymore and don't need silly cheat sheets like the one in the picture above. It's a shame that some people are not programming this way yet. Let's shake it off and leave the non-object-oriented world.
The example listing cars will follow us through the entire course and we'll demonstrate all the possible ways of passing dependencies on it until we get to the Inversion of Control and the Dependency Injection, which is one of the IoC techniques.
The object-oriented programming solves the problems of "having it all in one file" or of "lots of strangely named functions with many parameters" (and brings other benefits like inheritance, etc.). But we already know that we have to thing about how to divide the application into objects and mostly, how they will communicate with their dependencies. I suppose that you know the basics of OOP, if not, please complete the OOP course for your programming language first, see the navigation menu.
We'll continue in the next lesson, Monolithic and two-tier architecture, where we'll introduce different object architectures.