This article is intended for those who have at least a basic idea of Object oriented programming. They know the difference between classes and objects and can talk about the basic pillars of object oriented programming i.e., Encapsulation, Abstraction, Polymorphism and Inheritance.
In the object oriented world we only see objects. Objects interact with each other. Classes, Objects, Inheritance, Polymorphism, Abstraction are common vocabulary we hear in our day-to-day careers.
In the modern software world every software developer uses object oriented language of some kind, but the question is, does he really know what object oriented programming means? Does he know that he is working as an object oriented programmer? If the answer is yes, is he really using the power of object oriented programming?
In this article we will go beyond the basic pillars of object oriented programming and talk about object oriented design.
It’s a process of planning a software system where objects will interact with each other to solve specific problems. The saying goes, "Proper Object oriented design makes a developer's life easy, whereas bad design makes it a disaster."
When anyone starts creating software architecture their intentions are good. They try to use their existing experience to create an elegant and clean design.
Over time software starts to rot. With every feature request or change software design alters its shape and eventually the simplest changes to application requires a lot of effort and, more importantly, creates higher chances for more bugs.
Software solves real life business problems and since business processes keep evolving, software keeps on changing.
Changes are an integral part of the software world. Obviously because clients are paying they will demand for what they are expecting. So we cannot blame "change" for the degradation of software design. It is our design which is at fault.
One of the biggest reasons for the damaging of software design is the introduction of unplanned dependencies into the system. Every part of the system is dependant on some other part and so changing one part will affect another part. If we are able to manage those dependencies we will easily maintain the software system and software quality too.
In the same way, object oriented design is filled with many principles which let us manage the problems with software design.
Mr. Robert Martin (commonly known as Uncle Bob) categorized them as
In this article we will talk about SOLID principles with practical example.
It’s an acronym of J five principles introduced by Mr. Robert Martin (commonly known as Uncle Bob) i.e., Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion. It’s said (Wikipedia) when all five principles are applied together intend to make it more likely that a programmer will create a system that is easy to maintain and extend over time. Let’s talk about every principle in detail
I work as a team leader for one of the software firms in India. In my spare time I do some writing, newspaper editing and other various projects. Basically, I have multiple responsibilities in my life.
When something bad happens at my work place, like when my boss scolds me for some mistake, I get distracted from my other work. Basically, if one thing goes bad, everything will mess up.
Before we talk about this principle I want you take a look at following class.
Every time one gets changed there is a chance that the other also gets changed because both are staying in the same home and both have same parent. We can’t control everything. So a single change leads to double testing (or maybe more).
SRP says "Every software module should have only one reason to change".
Now it’s up to us how we achieve this. One thing we can do is create three different classes
Employee
– Contains Properties (Data) EmployeeDB
– Does database operations EmplyeeReport
– Does report related tasks Collapse | Copy Code
public class Employee { public string EmployeeName { get; set; } public int EmployeeNo { get; set; } } public class EmployeeDB { public void Insert(Employee e) { //Database Logic written here } public Employee Select() { //Database Logic written here } } public class EmployeeReport { public void GenerateReport(Employee e) { //Set report formatting } }
Note: This principle also applies to methods. Every method should have a single responsibility.
The answer is YES. Now you might ask how it’s possible that
Well the answer for this question is simple. It’s context. Here, responsibility is related to the context in which we are speaking. When we say class responsibility it will be somewhat at higher level. For instance, the EmployeeDB
class will be responsible for employee operations related to the Database whereas the EmployeeReport
class will be responsible for employee operations related to reports.
When it comes to methods it will be at lower level. For instance look at following example:
Collapse | Copy Code
//Method with multiple responsibilities – violating SRP public void Insert(Employee e) { string StrConnectionString = ""; SqlConnection objCon = new SqlConnection(StrConnectionString); SqlParameter[] SomeParameters=null;//Create Parameter array from values SqlCommand objCommand = new SqlCommand("InertQuery", objCon); objCommand.Parameters.AddRange(SomeParameters); ObjCommand.ExecuteNonQuery(); } //Method with single responsibility – follow SRP public void Insert(Employee e) { SqlConnection objCon = GetConnection(); SqlParameter[] SomeParameters=GetParameters(); SqlCommand ObjCommand = GetCommand(objCon,"InertQuery",SomeParameters); ObjCommand.ExecuteNonQuery(); } private SqlCommand GetCommand(SqlConnection objCon, string InsertQuery, SqlParameter[] SomeParameters) { SqlCommand objCommand = new SqlCommand(InsertQuery, objCon); objCommand.Parameters.AddRange(SomeParameters); return objCommand; } private SqlParameter[] GetParaeters() { //Create Paramter array from values } private SqlConnection GetConnection() { string StrConnectionString = ""; return new SqlConnection(StrConnectionString); }
Testing is advantageous in and of itself, but that the code has become readable is an additional advantage. The more the code is readable the simpler it seems.
Let’s assume you want to add one more floor between the first and second floor in your two floor house. Do you think it is possible? Yes it is, but is it feasible? Here are some options:
Let's say the Select
method in the EmployeeDB
class is used by two clients/screens. One is made for normal employees, one is made for managers, and the Manager Screen needs a change in the method.
If I make changes in the Select
method to satisfy the new requirement, other UI will also be affected. Plus making changes in existing tested solution may result in unexpected errors.
It says, "Software modules should be closed for modifications but open for extensions." An orthogonal statement.
We will derive a new class called EmployeeManagerDB
from EmployeeDB
and override the Select
method as per the new requirement.
Collapse | Copy Code
public class EmployeeDB { public virtual Employee Select() { //Old Select Method } } public class EmployeeManagerDB : EmployeeDB { public override Employee Select() { //Select method as per Manager //UI requirement } }
Note: Now the design is considered good object oriented design if this change is anticipated at the time of the design and already has a provision within for extension (method made virtual). Now the UI code will look like:
Collapse | Copy Code
//Normal Screen EmployeeDB objEmpDb = new EmployeeDB(); Employee objEmp = objEmpDb.Select(); //Manager Screen EmployeeDB objEmpDb = new EmployeeManagerDB(); Employee objEmp = objEmpDb.Select();
If you are using .NET 3.5 or later then there is a second way called extension method that will let us add new methods to existing types without even touching them.
Note: There may be some more ways to achieve the desired result. As I said these are principles not commandments.
You might be wondering why we are defining it prior to examples and problem discussions. Simply put, I thought it will be more sensible here.
It says, "Subclasses should be substitutable for base classes." Don’t you think this statement is strange? If we can always write BaseClass b=new DerivedClass()
then why would such a principle be made?
A father is a real estate business man whereas his son wants to be cricketer.
A son can’t replace his father in spite of the fact that they belong to same family hierarchy.
Let’s talk about a very common example.
Normally when we talk about geometric shapes, we call a rectangle a base class for square. Let’s take a look at code snippet.
Collapse | Copy Code
public class Rectangle { public int Width { get; set; } public int Height { get; set; } } public class Square:Rectangle { //codes specific to //square will be added }
One can say,
Collapse | Copy Code
Rectangle o = new Rectangle(); o.Width = 5; o.Height = 6;
Perfect but as per LSP we should be able to replace Rectangle with square. Let’s try to do so.
Collapse | Copy Code
Rectangle o = new Square(); o.Width = 5; o.Height = 6;
What is the matter? Square cannot have different width and height.
What it means? It means we can’t replace base with derived. Means we are violating LSP.
Why don’t we make width and height virtual in Rectangle, and override them in Square?
Code snippet
Collapse | Copy Code
public class Square : Rectangle { public override int Width { get{return base.Width;} set { base.Height = value; base.Width = value; } } public override int Height { get{return base.Height;} set { base.Height = value; base.Width = value; } } }
We can’t because doing so we are violating LSP, as we are changing the behavior of Width and Height properties in derived class (for Rectangle height and width cannot be equal, if they are equal it’s cannot be Rectangle).
(It will not be a kind of replacement).
There should be an abstract class Shape which looks like:
Collapse | Copy Code
public abstract class Shape { public virtual int Width { get; set; } public virtual int Height { get; set; } }
Now there will be two concrete classes independent of each other, one rectangle and one square, both of which will be derived from Shape.
Now the developer can say:
Collapse | Copy Code
Shape o = new Rectangle(); o.Width = 5; o.Height = 6; Shape o = new Square(); o.Width = 5; //both height and width become 5 o.Height = 6; //both height and width become 6
Even after overriding in derived classes we are not changing the behavior of width and height, because when we talk about shape, there will not be any fixed rule for width and height. They may be equal, or may not be.
Let’s say you purchase a new desktop PC. You will find a couple of USB ports, some serial ports, a VGA port etc. If you open the cabinet you will see lots of slots on the motherboard used for connecting various parts with each other, mostly used by hardware engineers at the time of assembly.
Those internal slots will not be visible until you open the cabinet. In short, only the required interfaces are made available/visible to you. Imagine a situation where everything was external or internal. Then there is a greater chances of hardware failure (as if life wasn't hard enough for computer users).
Let’s say we will go to a shop to buy something (let’s say, for instance, to buy a cricket bat).
Now imagine a situation where the shopkeeper starts showing you the ball and stumps as well. It may be possible that we will get confused and may end up buying something we did not require. We may even forget why we were there in the first place.
Let’s say we want to develop a Report Management System. Now, the very first task is creating a business layer which will be used by three different UIs.
EmployeeUI
– Show reports related to currently logged in employee ManagerUI
– Show reports related to himself and the team for which he/manager belongs. AdminUI
– Show reports related to individual employee ,related to team and related to company like profit report. Collapse | Copy Code
public interface IReportBAL { void GeneratePFReport(); void GenerateESICReport(); void GenerateResourcePerformanceReport(); void GenerateProjectSchedule(); void GenerateProfitReport(); } public class ReportBAL : IReportBAL { public void GeneratePFReport() {/*...............*/} public void GenerateESICReport() {/*...............*/} public void GenerateResourcePerformanceReport() {/*...............*/} public void GenerateProjectSchedule() {/*...............*/} public void GenerateProfitReport() {/*...............*/} } public class EmployeeUI { public void DisplayUI() { IReportBAL objBal = new ReportBAL(); objBal.GenerateESICReport(); objBal.GeneratePFReport(); } } public class ManagerUI { public void DisplayUI() { IReportBAL objBal = new ReportBAL(); objBal.GenerateESICReport(); objBal.GeneratePFReport(); objBal.GenerateResourcePerformanceReport (); objBal.GenerateProjectSchedule (); } } public class AdminUI { public void DisplayUI() { IReportBAL objBal = new ReportBAL(); objBal.GenerateESICReport(); objBal.GeneratePFReport(); objBal.GenerateResourcePerformanceReport(); objBal.GenerateProjectSchedule(); objBal.GenerateProfitReport(); } }
Now in each UI when the developer types "objBal
" the following intellisense will be shown:
What is the problem?
The developer who is working on EmployeeUI
gets access to all the other methods as well, which may unnecessarily cause him/her confusion.
It states that "Clients should not be forced to implement interfaces they don’t use." It can also be stated as "Many client specific interfaces are better than one general purpose interface." In simple words, if your interface is fat, break it into multiple interfaces.
Collapse | Copy Code
public interface IEmployeeReportBAL { void GeneratePFReport(); void GenerateESICReport(); } public interface IManagerReportBAL : IEmployeeReportBAL { void GenerateResourcePerformanceReport(); void GenerateProjectSchedule(); } public interface IAdminReportBAL : IManagerReportBAL { void GenerateProfitReport(); } public class ReportBAL : IAdminReportBAL { public void GeneratePFReport() {/*...............*/} public void GenerateESICReport() {/*...............*/} public void GenerateResourcePerformanceReport() {/*...............*/} public void GenerateProjectSchedule() {/*...............*/} public void GenerateProfitReport() {/*...............*/} }
Collapse | Copy Code
public class EmployeeUI { public void DisplayUI() { IEmployeeReportBAL objBal = new ReportBAL(); objBal.GenerateESICReport(); objBal.GeneratePFReport(); } }
Collapse | Copy Code
public class ManagerUI { public void DisplayUI() { IManagerReportBAL objBal = new ReportBAL(); objBal.GenerateESICReport(); objBal.GeneratePFReport(); objBal.GenerateResourcePerformanceReport (); objBal.GenerateProjectSchedule (); } }
Collapse | Copy Code
public class AdminUI { public void DisplayUI() { IAdminReportBAL objBal = new ReportBAL(); objBal.GenerateESICReport(); objBal.GeneratePFReport(); objBal.GenerateResourcePerformanceReport(); objBal.GenerateProjectSchedule(); objBal.GenerateProfitReport(); } }
By following ISP we can make client see, what he is required to see.
Let’s talk about our desktop computers. Different parts such as RAM, a hard disk, and CD-ROM (etc.) are loosely connected to the motherboard. That means that, if, in future in any part stops working it can easily be replaced with a new one. Just imagine a situation where all parts were tightly coupled to each other, which means it would not be possible to remove any part from the motherboard. Then in that case if the RAM stops working we have to buy new motherboard which is going to be very expensive.
Look at the following code.
Collapse | Copy Code
public class CustomerBAL { public void Insert(Customer c) { try { //Insert logic } catch (Exception e) { FileLogger f = new FileLogger(); f.LogError(e); } } } public class FileLogger { public void LogError(Exception e) { //Log Error in a physical file } }
In the above code CustomerBAL
is directly dependent on the FileLogger
class which will log exceptions in physical file. Now let’s assume tomorrow management decides to log exceptions in the Event Viewer. Now what? Change existing code. Oh no! My God, that might create a new error!
It says, "High level modules should not depend upon low level modules. Rather, both should depend upon abstractions."
Collapse | Copy Code
public interface ILogger { void LogError(Exception e); } public class FileLogger:ILogger { public void LogError(Exception e) { //Log Error in a physical file } } public class EventViewerLogger : ILogger { public void LogError(Exception e) { //Log Error in a physical file } } public class CustomerBAL { private ILogger _objLogger; public CustomerBAL(ILogger objLogger) { _objLogger = objLogger; } public void Insert(Customer c) { try { //Insert logic } catch (Exception e) { _objLogger.LogError(e); } } }
As you can see the client depends on abtraction i.e, ILogger
which can be set to an instance of any derived class.
So now we've covered all five principles of SOLID. Thanks Uncle Bob.
Now the question is, are there more principles other than those categorized by Uncle Bob? The answer is yes, but we will not going to describe each and every thing in detail for now. But they are:
We can’t avoid changes. The only thing we can do is develop and design software in such a way that it is able to handle such changes.
Hope all of you enjoyed reading this article. Thank you for the patience.
For technical training related to various topics including ASP.NET, Design Patterns, WCF and MVC contact SukeshMarla[at]Gmail.com or at www.sukesh-marla.com
For more stuff like this click here. Subscribe to article updates or follow at twitter @SukeshMarla
See 400 above FAQ questions and answers in .NET, C#, ASP.NET, SQL, WCF, WPF, WWF, SharePoint, Design patterns, UML etc.
引用:http://www.codeproject.com/Articles/567768/Object-Oriented-Design-Principles