SOLID Principles

SOLID Principles

In object-oriented computer programming, the term SOLID Principles is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable. Good programmers always strive to improve the code, making it more maintainable, easy to comprehend and extensible. These kinds of improvements are generally carried out based on intuition and personal experiences in the past. However, wouldn’t it be great if every developer had some common set of rules to follow to make the code better.

The term SOLID Principles was coined as an acronym for the set of Principles applicable when programming using Object Oriented languages. They were developed by Robert C Martin aka Uncle Bob.  He was introduced in around 2000’s and stands for the 5 main concepts of object-oriented coding and software design. When the five rules are adhered to, it makes the code better.

S – Single responsibility principle

O – Open/closed principle

L – Liskov substitution principle

I – Interface segregation principle

D – Dependency inversion principle

Note that the SOLID Principles are the starting point for good code. It does not take away the fact that the code needs to be continuously refactored to keep it in good shape.

The idea behind these principles is that when each is observed, the developer is more likely to create a system that is easier to maintain and extend. In the following sections, we’ll discuss each of these points in turn.

Single Responsibility

This principle basically states that each class should have single responsibility for one area of functionality provided by the solution and that all required code should be encapsulated by that class.

Take the example below. We have a Message interface and class that implements it. The class is responsible for only one thing, any new features or methods that get added to this interface or class must purely related to the “messages”.

public interface IMessage
{
   IList<String> ToAddresses { getset; }
   string MessageBody { getset; }
   bool Send();
}

public class SmsMessage : IMessage
{
   public IList<String> ToAddresses { getset; }
   public string MessageBody { getset; }
   public bool Send()
   {
      //Do the real work here
      return true;
   }
 }
 
  

Open Closed

The Open Closed principle states that software entities, classes, methods etc. should be open for extension but closed for modification. Modification to the entities behavior can be achieved by extending its features but not modifying its source code. One way to adhere to this principle is by using inheritance as it allows you to take a class, inherit from it and extend the existing functionality where you need to.

Imagine our Message interface had to be used in a different type of sending messages. We may wish to keep some existing functionality but have different implementations of the ProcessApplication method which determines if a person wants to send email or SMS message. Look code below

public interface IMessage
{
   IList<String> ToAddresses { getset; }
   string MessageBody { getset; }
   bool Send();
}

public class SmsMessage : IMessage
{
    public IList<String> ToAddresses { getset; }
    public string MessageBody { getset; }
    public bool Send()
    {
       //Do the real work here
       return true;
    }
 }

 public class SmtpMessage :  IMessage
 {
     public IList<String> ToAddresses { getset; }
     public string MessageBody { getset; }
     public bool Send()
     {
        //Do the real work here
        return true;
     }
  }
 
  

Liskov Substitution

This principle is named after Barbara Liskov who originally talked about the problem in 1998. She states that: “you should be able to use any derived class in place of a parent class and have it behave in the same manner without modification “

It ensures that derived classes do not affect the behavior of the classes they are inheriting from. Or put another way, that any derived class can take the place of its parent class.

Here is a real-world example to bring this concept to life. Look code below:

public class Ellipse
{
    public double MajorAxis { get; set; }
    public double MinorAxis { get; set; }

    public virtual void SetMajorAxis(double majorAxis)
    {
        MajorAxis = majorAxis;
    }

    public virtual void SetMinorAxis(double minorAxis)
    {
        MinorAxis = minorAxis;
    }

    public virtual double Area()
    {
        return MajorAxis * MinorAxis * Math.PI;
    }
}
 
  

We know from high school geometry that a circle is just a special case for an ellipse, so we create a Circle class that inherits from Ellipse, but SetMajorAxis sets both axes (because in a circle, the major and minor axes must always be the same, which is just the radius):

public class Circle : Ellipse
{
    public override void SetMajorAxis(double majorAxis)
    {
        base.SetMajorAxis(majorAxis);
        this.MinorAxis = majorAxis; //In a cirle, each axis is identical
    }
}
 
  

Interface Segregation

This principle states: “that clients should not be forced to depend on interfaces they do not use”. This principle is about breaking down monster interfaces into more specialized fine-grained ones. Imagine there were 10 more methods in the IMessage interface and we didn’t have direct access to the interface source code. We may not want to implement every single method either. Our only choice would be to implement the methods we’re interested in and ignore the methods that aren’t relevant to our class. It’s not ideal but it would work. You could throw a NotImplementedException for guidance in terms of the methods we didn’t need to implement but the interface explicitly mandated. If you’re a Microsoft.NET developer, you will be aware of the Membership Provider. This was great when integrated with SQL Server and gave you lots of user administration methods such as Change Password, DeleteUser etc. But say you wanted to roll your own implementations of the Membership Provider. You were forced to implement almost 30 methods! You may not want to do that though, and only need to change one or two.

Dependency Inversion

This principle consists of two points:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

This principle is about reducing the dependencies amongst classes in an application. You can think of it as low-level objects providing coarse-grained interfaces that high-level objects can use, without the high-level objects having to know or care about the specific implementations provided by low-level objects. Examples of this may be system auditing, notification mechanisms or database layers. Imagine our IMessage had to perform an audit of each user interaction in the system, either to the file system or database. A class called AuditManager resides in the business logic layer and is responsible for invoking the audit mechanism via the method LogMessage(string message). In most well-architected systems you’d have a business logic layer to house complex rules.

The Message solution contains the following interface:

public interface ILogger
{

    bool LogMessage(string message);

}

 
  

And the following concrete classes implement the ILogger interface:

public class FileLogger : ILogger
{

    public bool LogMessage(string message)
    {
       return true;
    }
}

public class DBLogger : ILogger
{

     public bool LogMessage(string message)
     {
       return true;
     }
}

 
  

Now here is where it gets interesting. We know that AuditManager needs to invoke LogMessage(“Customer Processed”) but we aren’t interested in the implementation of how it achieves this under the hood. Prior to adopting SOLID Principles, the AuditManager may have directly called something like .LogMessageToFile() or .LogMessageToDatabase().

By adopting the dependency inversion principle, the AuditManager can be written like this:

public class AuditManager
{

     ILogger log;

     public AuditManager(ILogger logger)
     {

        log = logger;

     }

     public LogMessage(string message)
     {

        log.LogMessage(message);
     }

 }

 
  

So, what’s going on here?

The AuditManagers constructor accepts any class that implements the ILogger interface. This is called constructor injection.

Depending on the type being passed into the constructor (DBLogger or FileLogger in our case), the application will write audit records to the database or file system.

It means that whenever.LogMessage() is called by AuditManager, it doesn’t care how the low-level audit operation is being dealt with. It’s only concern is to invoke LogMessage() and let the low level (concrete) logging classes deal with writing to the file system or database.

Conclusion

In this article, we’ve run through the SOLID Principles in turn and some examples. Whilst they aren’t a “silver bullet”, by implementing them you can ensure that your application’s code base will be easier to maintain and support. The SOLID Principles are guidelines that can help you create maintainable and extendable classes and systems. As Uncle Bob says: “They are not laws. They are not perfect truths”. Nevertheless, they have helped me a lot to create clean code, and I think as a developer you should at least know them so you can decide when to apply them and when not.

What’s been your experience with SOLID Principles?







要查看或添加评论,请登录

社区洞察

其他会员也浏览了