http://blog.gauffin.org/2012/05/solid-principles-with-real-world-examples/
The following article aims to explain the five SOLID principles with real world examples. The SOLID principles are five programming principles which is considered to be the foundation of every well designed application. Following the principles will most likely lead to applications which are is easy to extend and maintain. That’s possible since you got small well defined classes with clear contracts.
All motivator images are created by Derick Baley.
Single Responsibility Principle
Single responsibility states that every class should only have one reason to change. A typical example is an user management class. When you for instance create a new user you’ll most likely send an welcome email. That’s two reasons to change: To do something with the account management and to change the emailing procedure. A better way would be to generate some kind of event from the account management class which is subscribed by a UserEmailService that does the actual email handling.
The most effective way to break applications it to create GOD classes. That is classes that keeps track of a lot of information and have several responsibilities. One code change will most likely affect other parts of the class and therefore indirectly all other classes that uses it. That in turn leads to an even bigger maintenance mess since no one dares to do any changes other than adding new functionality to it.
Making sure that a class has a single responsibility makes it per default also easier to see what it does and how you can extend/improve it.
Classes that are hard to unit test are often breaking SRP.
External links
Open/Closed principle
Open/Closed principle says that a class should be open for extension but closed for modification. Which means that you can add new features through inheritance but should not change the existing classes (other than bug fixes).
The reason is that if you modify a class, you’ll likely break the API/Contract of the class which means that the classes that depend on it might fail when you do so. If you instead inherit the class to add new features, the base contract is untouched and it’s unlikely that dependent classes will fail.
Example violation
Here is a real world parser method (from a SO question which I’ve answered):
public void Parse() { StringReader reader = new StringReader(scriptTextToProcess); StringBuilder scope = new StringBuilder(); string line = reader.ReadLine(); while (line != null ) { switch (line[0]) { case '$' : // Process the entire "line" as a variable, // i.e. add it to a collection of KeyValuePair. AddToVariables(line); break ; case '!' : // Depending of what comes after the '!' character, // process the entire "scope" and/or the command in "line". if (line == "!execute" ) ExecuteScope(scope); else if (line.StartsWith( "!custom_command" )) RunCustomCommand(line, scope); else if (line == "!single_line_directive" ) ProcessDirective(line); scope = new StringBuilder(); break ; default : // No processing directive, i.e. add the "line" // to the current scope. scope.Append(line); break ; } line = reader.ReadLine(); } } |
It works great. But the method have to be changed each time we want to add support for a new directive. It’s therefore not closed for modification.
Solution
Lets create an interface which is used for each handler (for
'$'
and'!'
in the example above):public interface IMyHandler { void Process(IProcessContext context, string line); } |
Notice that we include a context object. This is quite important. If we create a new parser called
SuperCoolParser
in the future we can let it create and pass aSuperAwsomeContext
to all handlers. New handlers which supports that context can use it while others stick with the basic implementation.
We comply with Liskovs Substitution Principle and doesn’t have to change the
IMyHandler.Process
signature (and therefore keeping it closed for modification) when we add new features later on.
The parser itself is implemented as:
public class Parser { private Dictionary< char , IMyHandler> _handlers = new Dictionary< char , IMyHandler>(); private IMyHandler _defaultHandler; public void Add( char controlCharacter, IMyHandler handler) { _handlers.Add(controlCharacter, handler); } private void Parse(TextReader reader) { StringBuilder scope = new StringBuilder(); IProcessContext context = null ; // create your context here. string line = reader.ReadLine(); while (line != null ) { IMyHandler handler = null ; if (!_handlers.TryGetValue(line[0], out handler)) handler = _defaultHandler; handler.Process(context, line); line = reader.ReadLine(); } } } |
If you go back and look at the
!
handling you’ll see a lot ofif
statements. That method likely have to be changed to add support for more features. Hence it do also violate the principle. Let’s refactor again.public interface ICommandHandler { void Handle(ICommandContext context, string commandName, string [] arguments); } public class CommandService : IMyHandler { public void Add( string commandName, ICommandHandler handler) { } public void Handle(IProcessContext context, string line) { // first word on the line is the command, all other words are arguments. // split the string properly // then find the corrext command handler and invoke it. // take the result and add it to the `IProcessContext` } } |
External links
Liskovs Substitution Principle
Liskovs Substitution Principle states that any method that takes class X as a parameter must be able to work with any subclasses of X.
The principle makes sure that every class follows the contract defined by its parent class. If the class
Car
has a method calledBreak
it’s vital that all subclasses breaks when theBreak
method is invoked. Imagine the suprise ifBreak()
in aFerrari
only works if the switchChickenMode
is activated.Violation
Let’s use the motivator image as inspiration and define the following classes:
public interface IDuck { void Swim(); } public class Duck : IDuck { public void Swim() { //do something to swim } } public class ElectricDuck : IDuck { public void Swim() { if (!IsTurnedOn) return ; //swim logic } } |
And the calling code:
void MakeDuckSwim(IDuck duck) { duck.Swim(); } |
As you can see, there are two examples of ducks. One regular duck and one electric duck.
The electric duck can only swim if it’s turned on.The
MakeDuckSwim
method will not work if a duck is electric and not turned on.
That breaks LSP since any user of the
IDuck
interface expects the duck to swim when callingMakeDuckSwim
.Solution
You can of course solve it by doing something like this (in the method that uses the ducks)
void MakeDuckSwim(IDuck duck) { if (duck is ElectricDuck) ((ElectricDuck)duck).TurnOn(); duck.Swim(); } |
But that would break Open/Closed principle and has to be implemented everywhere that the ducks are used (and therefore still generate instable code).
The proper solution would be to automatically turn on the duck in the
Swim
method and by doing so make the electric duck behave exactly as defined by theIDuck
interface.public class ElectricDuck : IDuck { public void Swim() { if (!IsTurnedOn) TurnOnDuck(); //swim logic } } |
External links
Interface Segregation Principle
ISP states that interfaces that have become “fat” (like god classes) should be split into several interfaces. Large interfaces makes it harder to extend smaller parts of the system.
There is nothing that says that there should be a one-to-one mapping between classes and interfaces. It’s in fact much better if you can create several smaller interfaces instead (depends on the class though).
Violation
The
MembershipProvider
in ASP.NET is a classical example of a violation. MSDN contains a large article (which 4 out of 34 have found useful) which contains a long and detailed instruction on how to properly implement the class.Solution
The provider could have been divided in several parts:
*
*
*
*
MembershipProvider
– A facade to the below interfaces*
IAccountRepository
– Used to fetch/load accounts*
IPasswordValidator
– Checks that the password is valid (according to business rules)*
IPasswordStrategy
– How to store PW (hash it, use encryption or just store it in plain text)
Now you only have to implement a small part if you need to customize the provider (for instance
IAccountRepository
if you are using a custom data source). There’s a saying: “Favor composition over inheritance” which the membership providers illustrates well. The original solution used inheritance while mine used composition.External Links
Dependency Inversion Principle
The principle which is easiest to understand. DIP states that you should let the caller create the dependencies instead of letting the class itself create the dependencies. Hence inverting the dependency control (from letting the class control them to letting the caller control them).
Before
public class Volvo { B20 _engine; public Volvo() { _engine = new B20(); } } |
After
public class Volvo { IEngine _engine; public Volvo(IEngine engine) { if (engine == null ) throw new ArgumentNullException( "engine" ); _engine = engine; } } |
Which makes it a lot more fun since we now can do the following:
var myVolvo = new Volvo( new BigBadV12()); |
(Nice real world example, huh? ;))
Update
I messed up a bit. Dependency Inversion states that you should depend upon abstractions and that lower layers should not be aware of higher layers. I’ll get back to that. Dependency Injection is when you inject dependencies into the constructors instead of creating them in the class. Inversion Of Control is that the container controls your objects and their lifetime.
A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS
The original principle targets modules while I also like to apply it at class level too. It makes the principle easier to apply (so the text below is how I apply the principle).
Depend on abstractions simply means that you should depend on as generic class/interface as possible. For instance
IUserRepository
instead ofDbUserRepository
orHttpRequestBase
instead ofHttpRequest
. The purpose is that your code should be as flexible as possible. The more abstract the dependencies are, the easier it is to refactor them or create new implementations.
Depending on abstractions also make your code less likely to change if the dependency change.
External links
Summary
Feel free to leave a comment if something is unclear. It will not just help you, but all future readers.
Комментариев нет:
Отправить комментарий