SOLID is an acronym for the five object-oriented design principles. These principles enable developers to develop software that is easily extendable and maintainable. SOLID principles also ensure that software can easily adapt to changing requirements. Let’s look at each of the principles, one by one:
SRP states that every class must have only one reason to change it. In other words, each class must be responsible for a single purpose only. Consider the following code snippet, featuring a User
class with an update
method:
class User{void update(Database db, string msg){try{db.Add(msg); // Update the DB}catch (Exception ex) // Catch and log error.{File.WriteAllText("errors.txt", ex.ToString());}}}
The User
class is not only updating the database, its also handling logging of errors.
Consider the SRP compliant code below:
class User{private ErrorLogger error = new ErrorLogger();void update(Database db, string msg){try{db.Add(msg);}catch (Exception ex){error.handle(ex.ToString());}}}class ErrorLogger{void handle(string error){File.WriteAllText("errors.txt", error);}}
OCP states that classes must be open for extension, but closed for modification:
class TextMessage{void sendMsg(Database db, string msg){if (msg.StartsWith("@")){db.Tag(msg); // messages starting with @ are tags.}// if another message that starts with '#' needs,// to be handled, then another if condition will be needed.// The class will need to be heavily modified to// incorporate changing requirements.else{db.Add(msg); // otherwise they're just simple messages.}}}
In the above snippet, the TextMessage
class has a sendMsg
module. This module behaves differently when the message starts with @.
However, if another type of message, that starts with # needs to be handled, then the class will need to be modified by adding another if
statement.
Consider the code below; it uses inheritance to comply with the Open-Closed Principle:
class TextMessage{void sendMsg(Database db, string msg){db.Add(msg);}}// A class made specifically for tagging.// If a new feature needs to be incorporatedclass Tag : TextMessage // class Tag inherits TextMessage{override void sendMsg(Database db, string msg){db.Tag(msg);}}
LSP states that if is a subtype of , then objects of type may be replaced with objects of type . Consider the example below:
class Bird{void fly(){System.Console.WriteLine("I believe I can fly!");}}class Dove : Bird {}class Ostrich : Bird {}
The Dove
class is a subtype of Bird
; objects of Bird
can be replaced by objects of Dove
because doves can fly. However, the same cannot be done with objects of Ostrich
since they cannot fly. This is a violation of LSP.
Now consider the LSP compliant code below:
class Bird{// functions that all birds do}class FlyingBirds : Bird{void fly(){System.Console.WriteLine("I believe I can fly!");}}class Dove : FlyingBirds {}class Ostrich : Bird {}
Objects of FlyingBirds
are replaceable by Dove
, and objects of simple Bird
are replaceable by objects of Ostrich
; hence the code follows LSP.
ISP suggests that several specific client interfaces are better than having one general interface. In other words, it is better to have multiple interfaces instead of adding lots of functionality to one interface.
Consider the example below:
interface phoneTasks{void call();void text();void games();void torch();}class smartPhone : phoneTasks{public void call(){}public void text(){}public void games(){}public void torch(){}}class dumbPhone: phoneTasks{public void call(){}public void text(){}// But the dumb phone does not have games, or a torch.// This is an ISP violation.public void games(){}public void torch(){}}
The interface phoneTasks
has methods for tasks that a smartphone can perform; however, another phone may not have all of these features but is still implementing them.
Now consider the ISP compliant code below:
// Lots of specific interfaces is better than// one big general interface.interface phoneTasks{void call();void text();}interface Games{void games();}interface Torch{void torch();}class smartPhone : phoneTasks, Games, Torch{public void call(){}public void text(){}public void games(){}public void torch(){}}// dumbPhone is only implementing the features it has.// no unneccessary interfaces.class dumbPhone: phoneTasks{public void call(){}public void text(){}}
DIP states that:
Consider an example of a calculator class that is dependent on two low-level classes that define its features. Now, if more features are to be added, then the calculator class would need to be modified.
DIP dictates that high-level classes must not be dependent on low-level classes. In this case, the calculator class should not be bogged down with the details of the implementation of its features. The illustration below highlights this idea:
Free Resources