Image: Pixabay |
Laravel is a PHP framework that implements the model-view-controller (MVC) pattern. A lot of people think that their responsibility for OOP design ends with adopting a framework, but actually Laravel is relatively un-opinionated on your OOP design and you still need to think about writing code that is testable and maintainable.
The reason that SOLID principals matter becomes apparent when you work on a single project for a long time. If you're writing throwaway applications for clients that you never expect to work on again (presumably because the client won't hire you again) then the quality of your code doesn't matter. But if you're the guy stuck with managing and implementing change in an application that is actively growing and improving then you're going to want code that is easy to change, easy to test, and easy to deploy.
The most common problem I've seen in Laravel is "fat controllers" that use the ORM to get some data and then push it through to a view. Let's take a look at an example I've made. Imagine that we're writing a payroll program. We might write something like the following controller methods:
This is an unfortunately common Laravel pattern that is taught in countless tutorials. We call the model from the controller, format the data, and then pass it on to the view. This is the easiest way to teach the MVC pattern to beginners but unfortunately it violates the SOLID principals. Let's see why, and how we can improve this code.
The "S" in SOLID stands for single responsibility principal which requires that each module or class should have a single responsibility for the functionality of the application. A more subtle understanding is put forward by Robert C Martin who says that "A class should have only one reason to change".
The thinking behind limiting the reasons for changing a class comes from the observation that software is often developed by teams and that often a team is implementing a feature for a particular actor. In our example the CEO of the company will have different requirements from the CFO, and when either of them requests a change then we want to limit the impact of that change. The actor is the reason for software to change - they request a feature and a team goes ahead and implements it.
In our controller above if the CEO requested a change then that change would definitely affect the CFO. The teams working on the code would need to merge in code from each other. If our code was properly designed then the controller class would be responsible to just one actor.
In this example I've moved the responsibility for calculating the employee pay to its own object. This object will only change if the CFO requests a change to the way that wages are calculated and so adheres to the single responsibility principal. We would similarly have an object that is responsible for counting the hours. I've chosen this way of solving the problem because the Facade pattern is very loaded in Laravel and I think it would just muddy the waters to use it here.
Let's move on to "O" which is the open-closed principal which requires that "A software artifact should be open for extension but closed for modification". It was developed by Betrand Meyer and holds that you should be able to extend on a modules functionality without having to change that module.
The aim of the OCP is to protect important code that implements high level policies from changes made to code that implements low-level details. We want the part of our code that is central to our application to be insulated from changes in other parts of the application.
There is some level of separation in our Laravel application. We can make a change to the View without there being any impact on the Controller, but within the controller above we have no such insulation. If we make a change to the way we read the database then we will be affecting exactly the same function that is responsible for calculating wages!
The open-closed principal seeks to prevent you from changing core functionality as a side-effect of adding new functionality to your application. It works by separating out the application into a hierarchy of dependencies. You can extend functionality from the lower levels of the hierarchy without changing the code in the higher levels.
The "L" in SOLID is named for Barbara Liskov who put forward what is now known as the Liskov substitution principal. The principal holds that "if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program".
In the example above I've amended the object to make it inherit from an interface. Both the PermanentEmployeePayCalculator class and the TemporaryEmployeePayCalculator implement this interface and can be substituted for each other. This makes a lot more sense if you consider an LSP violation, such as this one:
This violates the Liskov Substitution Principal because the methods have got different signatures. You cannot substitute the subtypes of PayCalculator each other because they're incompatible. The object that depended on them would need to implement some logic to be able to know how many parameters to pass to the method. Adhering to the Liskov substitution principal removes this need and removes special cases from your code.
Adhering to the Liskov substitution principal |
Let's imagine that we separated out our controller into classes like the below diagram. We have an Employee data object that is responsible for interacting with the persistence layer and returning results. It has a method that the PayCalculator object uses to determine whether the employee needs to earn their overtime rate and a method that both objects use to fetch the list of hours that an employee has worked (which may or may not violate the single responsibility principal).
Violation of the ISP |
The problem here is that the HoursReporter is forced to depend on the isHourOvertime() function. This introduces an additional coupling between the classes that we need to avoid if we want to adhere to the interface segregation principal.
Adhering to the ISP |
We can easily solve this problem by declaring an interface for the classes to depend on. The interface for the HoursReporter class excludes the function that we do not want to depend on.
The last letter in SOLID is "D" which stands for the dependency inversion principal which holds that the most flexible modules are those in which source code dependencies refer only to abstractions rather than concretions.
To understand dependency inversion consider two things: flow of control and source code dependency. We want to be able to have our source code dependencies to be independent of how control flows through our application.
Some classes and files in our application are more prone to change than others. They are "volatile" classes. We want to minimise the effect of the changes in these classes to the more stable classes. Ideally we want our business logic to be very stable and highly insulated from changes elsewhere in our system.
In the diagram below I'm illustrating a source code dependency hierarchy. High level classes call functions in lower level classes, but in order to do so they need to depend on that class. This means that your source code dependencies are unavoidably tied to how your flow of control works.
Source code dependency hierarchy |
Let's say, for example, we had a class that outputs the Employee wages to screen. In the diagram above we would see the Employee object as the High Level object and perhaps a "ScreenOutput" object as a low-level object. Our Employee object calls the ScreenOutput class directly, and so we have to mention the source code in Employee, like this:
Now our CFO asks us to be able to print out the wages using the black and white printer in her office. Uh-oh, now we need to rewrite our source-code dependency because the "use" statement refers specifically to a concretion.
What happens if we want to make a change to the way that wages are displayed on the screen? We can easily tweak the ScreenOutput object, but can we deploy it separately? What impact is it going to have on all the places that depend on it?
How could we fix this problem and allow ourselves to swap functionality in and out without affecting our source code dependencies? How do we actually decouple these objects?
The answer is to always depend on abstractions rather than concretions. This insulates you from changes in the underlying files and lets you change and deploy parts of your application separately.
Using an interface to implement dependency inversion |
The rules to follow for the dependency inversion principal are:
- Do not reference volatile concrete classes
- Do not derive from volatile concrete classes
- Do not override concrete functions
- Never mention the name of anything concrete and volatile
One way that you can accomplish this is through using a Factory to instantiate volatile concrete classes. This removes the requirement to have a source code dependency on the class that you're instantiating in the object where you need it.
Laravel approaches dependency inversion by using a "service container". Your code no longer depends on a concrete implementation of a class, but rather requests an instance of an object from the IoC container.
In our controller code above the IoC container returns an instance of the HoursWorked model through the Facade pattern. The controller is not directly dependent on the source code file of the HoursWorked model. So in this particular case we're just lucky to be adhering to a SOLID principal!
Comments
Post a Comment