AlignMinds Technologies logo

Principles of Writing SOLID Code: A Guide for Beginners

MODIFIED ON: November 29, 2022 / ALIGNMINDS TECHNOLOGIES / 0 COMMENTS

This blog post will help you to understand the basic principles of writing solid code.

Key principles of writing SOLID code

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov substitution principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

These are general principles that you want to always have in the back of your mind as you design your software. For the obvious reason, the first five are called SOLID. As in “we write SOLID code”. They are not always cut-and-dried; they are rarely 100% achievable. But active awareness of these issues will help you to avoid common coding errors.

1. Single Responsibility Principle

“A class should have a single responsibility”.

I prefer to think of ‘responsibility’ here as ‘purpose’ or ‘job to do’, but it’s called SRP. This is basically just another statement about modularity. Find the logical units and encapsulate them in a class. Don’t take classes that do some of this and some of that. You’ll get greater re-use of the component since the component wasn’t bundled with several other responsibilities the upstream consumer doesn’t want or need;

An alternative conceptualization of the SRP: A class should have a single reason to change.

This principle refers to the impact of a change of requirements on the code. For example, a class should either talk to the database, or format output for the UI, not both. Classes that deal with the database won’t need to be updated if the UI changes, and vice versa. When requirements change, modification is kept to just those spots directly affected, and these changes don’t have unanticipated consequences elsewhere in classes that whose responsibilities have leaked over into ours.

2. Open/Closed Principle

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”

Construct your classes so that requirements changes can be managed by adding code, not by modifying existing code. Once you have written code that works, you should ideally never touch it again, because if you don’t touch it, you can’t break it. If you’re writing nice modular code, you probably have objects being used by other objects all over the place (code reuse = good). If you now break that class, you break everybody who is using it (==bad).

This is a very simple little pizza ordering app. There is a TPizza class with an addTopping method and a computeCost method in this application. In this example, we’re interested in the TPizza class itself.

private void button1_Click(object sender, EventArgs e) {TPizza currPizza = new TPizza(); if (ckOlives.Checked)currPizza.addTopping(ETopping.Olives); if (ckPepperoni.Checked)currPizza.addTopping(ETopping.Pepperoni); if (ckMushrooms.Checked)currPizza.addTopping(ETopping.Mushrooms); int pizzaCost = currPizza.computeCost();lblCost.Text = pizzaCost.ToString();}

Here is the button code. Again, we’re not interested in the interface code, so this is just to show how TPizza is used. So, all very straightforward. Now, let’s look at TPizza class.

enum ETopping { Olives, Pepperoni, Mushrooms } class TPizza{ETopping[] toppingArray; int nToppings; public TPizza(){toppingArray = new ETopping[10]; nToppings = 0; } public void addTopping(ETopping newTopping){toppingArray[nToppings] = newTopping;nToppings++; }

So, it’s got a nice enum type and a nice array of toppings and an addTopping method and all is good.

Here is computeCost method.

public int computeCost() { int totalCost = 15; for (int i = 0; i < nToppings; i++) { switch (toppingArray[i]) { case ETopping.Olives: totalCost += 1; break; case ETopping.Pepperoni: totalCost += 2; break; case ETopping.Mushrooms: totalCost += 2; break; } } return totalCost; } }

This works great. But the problem arises when I want to add cheese topping. I can’t add cheese without modifying existing code in the TPizza class. This gives me a chance to break the TPizza class. It could be worse, of course. If there are other places in the class where we have used the same approach (printing out an invoice, sending instructions to the pizza oven, drawing a picture of the pizza) their code will also have to be changed.

How would you have written that architecture so that you could incorporate the change in requirements (adding cheese topping) by adding code, rather than changing existing code? (Answer=> Use a topping class, who knows its cost and type. Or, if you’re really feeling elaborate, a base topping class and descendants). Not that this failure is also a violation of the SRP. It should not be the job of the TPizza to keep track of the cost of individual toppings.

3. Liskov Substitution Principle

Subclasses should be substitutable for their base class. All members of an inheritance hierarchy should fulfil the same behavioural contract. If they don’t then your “is-a” abstraction is probably wrong.

The LSP helps us to avoid misusing inheritance and consequently running into the problems that result when this occurs. A user of a base class should continue to function properly if any derivative of the base class is passed to it. Failure to follow the LSP almost always leads to problems with the OCP, as you wiggle around coding in special cases to your class family.

void runWildLifeSimulator(TDuck d) { d.swim(); d.quack(); d.fly(); }

runWildLifeSimulator is what is called a ‘consumer’ function of TDuck. That is, it uses a TDuck instance. TDuck has made a contract – it will implement swim, quack and fly. Anybody who wants to be a TDuck needs to implement those, and it should make sense.

Surely, a Rubber Duck is a kind of duck. But because a Rubber Duck doesn’t fly and a TDuck does, this isn’t a good class hierarchy. Think about what you would have to do to the consumer function.

void runWildLifeSimulator(TDuck d) { d.swim(); d.quack(); if (TDuck.getType() != TRubberDucky) d.fly(); }

Why should a function that wants a TDuck has to know anything about THE DESCENDENTS of TDuck?  What if there are other violations in the class hierarchy? A switch statement? And of course, what happens if you need to change something about these ducks.

There is a workaround for this, of course. It is? (Let TRubberDuckie override fly to { do nothing }

There are workarounds for all violations of good design. If you hit the peg with a big enough hammer, it will go into the hole.  But these sorts of problems have been showed to produce rigidity and inflexibility and general goofiness in code. Try to avoid them.

4. Interface Segregation Principle

The dependency of one class to another one should depend on the smallest possible interface. Clients should not be forced to depend on methods they do not use. This one is very closely related to the SRP: don’t stuff everything into one big garbage multipurpose class. Changes to code are isolated to those classes that are logically affected.

interface Imessage { public bool SendSms(String message); public bool SendEmail(String message); }

In this case, all the classes that inherit this interface are forced to write methods for sending email and SMS. If some clients are interested only in emails, the issue arises.

The solution is,

interface Isms { public bool SendSms(String message); } interface Iemail { public bool SendEmail(String message); } interface Imessage : Isms { public bool SendSms(String message); }

5. Dependency Inversion Principle

Program to the most abstract class possible. High-level modules should not depend on (concrete) low-level modules.

Consequences of ignoring these core principles

These core OOAD (Object-oriented analysis and design) principles will help you to write SOLID code. But, what are the consequences of ignoring these principles?

As a result of ignoring these principles, the system will be,

1. Rigid

Changing one part of the code causes or requires a change to many other parts of the code.

2. Fragile

Changes in one part of the code break other parts of the code.

3. Immobile

The components/parts of the code cannot be easily reused, because they are tangled.

 –  Albin Antony