aelf Tech Talks: Dependency Injection Part 2
SOLID Design Principles
In Part 1 of this series, we discussed Dependency Injection and Inversion of Control (IoC). Before going into depth on Dependency Injection, it is important to understand the five SOLID principles of object-oriented design pattern. This part will look at the first three principles.
1) Single Responsibility Principle (SRP, Single Responsibility Principle)
> There should never be more than one reason for a class to change.
You may have heard of a joke:
- Have you written code without bugs?
- Yes, I did it earlier: Hello World.
This paragraph tells us the truth: the simpler the code, the less likely it is to have a bug.
So how do we make code simple? By writing each line of code to do just one thing. In practice, we just let a class do just one thing. And if we can give this class a simple and intuitive name, the self-reporting of the code itself will be very good, I remember that Alibaba’s Java specification has requirements for naming, such as if a class uses a design pattern, It is required to embody the name of the design pattern in the name. At this time, the comments are redundant, and the code is easy for humans to read and to maintain.
Going back to the description of the single responsibility principle: Martin defines a responsibility as a reason to change, and concludes that a class or module should have one, and only one, reason to be changed (i.e. rewritten). As an example, consider a module that compiles and prints a report. Imagine such a module can be changed for two reasons. First, the content of the report could change. Second, the format of the report could change. These two things change for very different causes; one substantive, and one cosmetic. The single responsibility principle says that these two aspects of the problem are really two separate responsibilities, and should therefore be in separate classes or modules. It would be a bad design to couple two things that change for different reasons at different times.
The reason it is important to keep a class focused on a single concern is that it makes the class more robust. Continuing with the foregoing example, if there is a change to the report compilation process, there is greater danger that the printing code will break if it is part of the same class.
There is also a standard for judging whether a class conforms to the principle of single responsibility. It depends on the relevance of the elements in this class. You can’t have a writing function in a class that implements a repeater. Don’t think about it. He care the most will be checking the size of the clipboard. In other words, the clipboard itself should not be included in the class of repeaters, but exist as a service alone, and you can consider using the Repository mode.
High correlation means high cohesion and a smaller granularity in function.
The class mentioned here can refer to either an entity or a service. In the book of Domain-driven Design, Eric divides objects in the application into three types: values, entities, and services. An entity can be thought of as a combination of values, and a service is code that contains business logic.
2) Open Closed Principle (OCP)
> Software entities like classes, modules and functions should be open for extension but closed for modifications, such that an entity can allow its behaviour to be extended without modifying its source code.
Speaking of the Open Closed Principle, this is a design pattern that is particularly easy to understand, through the decorator pattern. We can imagine a scenario. We are making a supermarket’s cash register system, or an e-commerce website, either way there is a commodity entity, the commodity entity has a GetPrice method, and now you need to add a discount function to the product. Assuming that the class category of goods meets the principle of single responsibility, there are now three options:
1. Add a method to the interface to provide the functionality you need now.
2. Modify the implementation of the class and use the previous method to sign and provide new features.
3. Re-create a class, inherit the previous implementation class, wrap a layer on the previous method, and provide a new implementation.
Open Closed Principle suggests that we will choose the third solution. Reasoning being that, first, by modifying the interface other classes that seek to implement this interface need to add the implementation. This new implementation is likely to be unnecessary, since all the products are not likely to be discounted in the same day. The second solution is simply a dangerous operation, it will definitely affect the unit test (if any), and has a huge probability that it will introduce bugs. The third option is the safest. We can provide a decorator for the previous implementation and then call the decorator to provide new functionality. In the previous example, the previous implementation is to return the original price of the product, what the decorator can call the “discount decorator”, and then call the same method to return the discounted price. The previous implementation is injected into the decorator through the constructor parameters. Actual use may not be so crude, but that’s a simple explanation of the principle.
Later, when you talk about the combined root of injection, you will mention Open Closed Principle again.
3) Liskov Substitution Principle (LSP)
> Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
This seems to be one of the most overlooked principles.
The classic example of the “is a” technique causing problems is the circle-elipse problem (a.k.a the rectangle-square problem). However, I’m going use penguins.
First, consider an application that shows birds flying around in patterns in the sky. There will be multiple types of birds, so the developer decides to use the Open Closed Principle to “close” the code to the addition of new types of birds. To do this, the following abstract Bird base class is created:
Version one of BirdsFlyingAroundApp is a huge success. Version two adds another 12 different types of birds with ease, and is also a success. Hooray for the Open Closed Principle. However, version three of the app is required to support penguins. The developer makes a new Penguin class that inherits from the Bird class, but there is a problem:
If an override method does nothing or just throws an exception, then you’re probably violating the LSP.
When the app is run, all the flying patterns look wrong because the Penguin objects ignore the setAltitude method. The penguins are just flopping around on the ground. Even though the developer tried to follow the OCP, they failed. Existing code must be modified to accommodate the Penguin class.
While a penguin technically “is a” bird, the Bird class makes the assumption that all birds can fly. Because the Penguin subclass violates the flying assumption, it does not satisfy the Liskov Substitution Principle for the Bird superclass.
Why Violating The LSP is Bad
The whole point of using an abstract base class is so that, in the future, you can write a new subclass and insert it into existing, working, tested code. This is the essence of the Open Closed Principle. However, when the subclasses don’t adhere properly to the interface of the abstract base class, you have to go through the existing code and account for the special cases involving the delinquent subclasses. This is a blatant violation of the Open Closed Principle.
For example, take a look at this fragment of code:
The LSP says that the code should work without knowing the actual class of the Bird object. What if you want to add another type of flightless bird, like an emu? Then you have to go through all your existing code and check if the Bird pointers are actually Emu pointers. You should be wrinkling your nose at the moment, because there is definitely a code smell in the air.
Two Possible Solutions
We want to be able to add the Penguin class without modifying existing code. This can be achieved by fixing the bad inheritance hierarchy so that it satisfies the LSP.
One not-so-great way of fixing the problem is to add a method to the Bird class named isFlightless. This way, at least additional flightless bird classes can be added without violating the OCP. This would result in code like so:
This is really a band-aid solution. It hasn’t fixed the underlying problem. It just provides a way to check whether the problem exists for a particular object.
A better solution would be to make sure flightless bird classes don’t inherit flying functionality from their superclasses. This could be done like so:
I don’t think the English language has a word that means the opposite of “flightless”, but let’s be Shakespearian and invent the word “flightful” to fill the position. In the above solution the Bird base class does not contain any flying functionality, and the FlightfulBird subclass adds that functionality. This allows some functions to be applied to both Bird and FlightfulBird objects; drawing for example. However, the Bird objects, which may be flightless, can not be shoved into functions that take FlightfulBird objects.
The next part in this series will look at the remaining 2 principles before the final part discusses Dependency Injection in more detail.
Other Topics in aelf Tech Talks:
— Join the Community:
· Read weekly articles on the aelf blog
· Catch up with the develop progress on Github
· Instagram: aelfblockchain
· YouTube Channel: aelf
For more information, visit aelf.io