DEV Community

Cover image for SOLID Principles: They're Rock-Solid for Good Reason!
Lucas Porfirio
Lucas Porfirio

Posted on

SOLID Principles: They're Rock-Solid for Good Reason!

Just started with object-oriented programming and feeling a bit lost about SOLID? No worries, in this article, I'll explain it to you and provide examples of how to use it in your code development.

What is SOLID?

In object-oriented programming, SOLID is an acronym for five design principles aimed at enhancing the understanding, development, and maintenance of software.

By applying this set of principles, you should notice a reduction in bugs, improved code quality, the production of more organized code, decreased coupling, enhanced refactoring, and encouragement of code reuse. Let's get to them.

1. S - Single Responsibility Principle

SRP

SRP - Single Responsibility Principle

This one is really simple, but super important: one class should have one, and only one, reason to change.

No more creating classes with multiple functionalities and responsibilities, huh? You've probably encountered or even created a class that does a bit of everything, a so-called God Class. It might seem fine at the moment, but when you need to make changes to the logic of that class, problems are sure to arise.

God class: In OOP, this is a class that do or knows too much things.



class ProfileManager {
  authenticateUser(username: string, password: string): boolean {
    // Authenticate logic
  }

  showUserProfile(username: string): UserProfile {
    // Show user profile logic
  }

  updateUserProfile(username: string): UserProfile {
    // Update user profile logic
  }

  setUserPermissions(username: string): void {
    // Set permission logic
  }
}


Enter fullscreen mode Exit fullscreen mode

This ProfileManager class is violating the SRP principle by performing FOUR distinct tasks. It is validating and updating data, doing the presentation, and to top it off, it's setting the permissions, all at the same time.

Issues this can cause

  • Lack of cohesion - a class shouldn't take on responsibilities that aren't its own;
  • Too much information in one place - your class will end up with many dependencies and difficulties for changes;
  • Challenges in implementing automated tests - it's hard to mock such a class.

Now, applying SRP to the ProfileManager class, let's see the improvement this principle can bring:



class AuthenticationManager {
  authenticateUser(username: string, password: string): boolean {
    // Authenticate logic
  }
}

class UserProfileManager {
  showUserProfile(username: string): UserProfile {
    // Show user profile logic
  }

  updateUserProfile(username: string): UserProfile {
    // Update user profile logic
  }
}

class PermissionManager {
  setUserPermissions(username: string): void {
    // Set permission logic
  }
}


Enter fullscreen mode Exit fullscreen mode

You might be wondering, can I apply this only to classes? The answer is: NOT AT ALL. You can (and should) apply it to methods and functions as well.



// ❌
function processTasks(taskList: Task[]): void {
  taskList.forEach((task) => {
    // Processing logic involving multiple responsibilities
    updateTaskStatus(task);
    displayTaskDetails(task);
    validateTaskCompletion(task);
    verifyTaskExistence(task);
  });
}

// ✅
function updateTaskStatus(task: Task): Task {
  // Logic for updating task status
  return { ...task, completed: true };
}

function displayTaskDetails(task: Task): void {
  // Logic for displaying task details
  console.log(`Task ID: ${task.id}, Description: ${task.description}`);
}

function validateTaskCompletion(task: Task): boolean {
  // Logic for validating task completion
  return task.completed;
}

function verifyTaskExistence(task: Task): boolean {
  // Logic for verifying task existence
  return tasks.some((t) => t.id === task.id);
}


Enter fullscreen mode Exit fullscreen mode

Beautiful, elegant, and organized code. This principle is the foundation for the others; by applying it, you should create high-quality, readable, and maintainable code.

2. O - Open-Closed Principle

OCP

OCP - Open-Closed Principle

Objects or entities should be open for extension but closed for modification. If you need to add functionality, it's better to extend rather than modify your source code.

Imagine that you need a class to calculate the area of some polygons.



class Circle {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Square {
  sideLength: number;

  constructor(sideLength: number) {
    this.sideLength = sideLength;
  }

  calculateArea(): number {
    return this.sideLength ** 2;
  }
}

class areaCalculator {
  totalArea(shapes: Shape[]): number {
    let total = 0;

    shapes.forEach((shape) => {
      if (shape instanceof Square) {
        total += (shape as any).calculateArea();
      } else {
        total += shape.area();
      }
    });

    return total;
  }
}


Enter fullscreen mode Exit fullscreen mode

The areaCalculator class is tasked with calculating the area of diferent polygons, each having its own area logic. If you, 'lil dev, needed to add new shapes, like triangles or rectangles, you'd find yourself altering this class to make the changes, right? That's where you run into a problem, violating the Open-Closed Principle.

What solution comes to mind? Probably adding another method to the class and done, problem solved 🤩. Not quite, young Padawan 😓, that's the problem!

Modifying an existing class to add new behavior carries a serious risk of introducing bugs into something that was already working.

Remember: OCP insists that a class should be closed for modification and open for extension.

See the beauty that comes with refactoring the code:



interface Shape {
  area(): number;
}

class Circle implements Shape {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Square implements Shape {
  sideLength: number;

  constructor(sideLength: number) {
    this.sideLength = sideLength;
  }

  area(): number {
    return this.sideLength ** 2;
  }
}

class AreaCalculator {
  totalArea(shapes: Shape[]): number {
    let total = 0;

    shapes.forEach((shape) => {
      total += shape.area();
    });

    return total;
  }
}


Enter fullscreen mode Exit fullscreen mode

See the AreaCalculator class: it no longer needs to know which methods to call to register the class. It can correctly call the area method by calling the contract imposed by the interface, and that's the only thing it needs.

As long as they implement the Shape interface, all runs fine.

Separate extensible behavior behind an interface and invert dependencies.

Uncle Bob

  • Open for extension: You can add new functionality or behavior to the class without changing its source code.
  • Closed for modification: If your class already has a functionality or behavior that works fine, don't change its source code to add something new.

3. L - Liskov Substitution Principle

LSP

LSP - Liskov Substitution Principle

The Liskov Substitution Principle says that a derived class must be substitutable for its base class.

This principle, introduced by Barbara Liskov in 1987, can be a bit complicated to understand by reading her explanation. Still, no worries, I'll provide another explanation and an example to help you understand.

If for each object o1 of type S there is an object o2 of type T such that, for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.

Barbara Liskov, 1987

You got it, right? Nah, probably not. Yeah, I didn't understand it the first time I read it (nor the next hundred times), but hold on, there's another explanation:

If S is a subtype of T, then objects of type T in a program can be replaced by objects of type S without altering the properties of this program.

Wikipedia

If you're more of a visual learner, don't worry, here's an example:



class Person {
  speakName() {
    return "I am a person!";
  }
}

class Child extends Person {
  speakName() {
    return "I am a child!";
  }
}

const person = new Person();
const child = new Child();

function printName(message: string) {
  console.log(message);
}

printName(person.speakName()); // I am a person!
printName(child.speakName()); // I am a child!


Enter fullscreen mode Exit fullscreen mode

The parent class and the derived class are passed as parameters, and the code continues to work as expected. Magic? Yeah, it's the magic of our friend Barb.

Examples of violations:

  • Overriding/implementing a method that does nothing;
  • Returning values of different types from the base class.
  • Throwing an unexpected exception;

4. I - Interface Segregation Principle

ISP

ISP - Interface Segregation Principle

This one says that a class should not be forced to implement interfaces and methods it does not use. It's better to create more specific interfaces than a big and generic one.

In the following example, an Book interface is created to abstract book behaviors, and then classes implement this interface:



interface Book {
  read(): void;
  download(): void;
}

class OnlineBook implements Book {
  read(): void {
    // does something
  }

  download(): void {
    // does something
  }
}

class PhysicalBook implements Book {
  read(): void {
    // does something
  }

  download(): void {
    // This implementation doesn't make sense for a book
    // it violates the Interface Segregation Principle
  }
}


Enter fullscreen mode Exit fullscreen mode

The generic Book interface is forcing the PhysicalBook class to have a behavior that makes no sense (or are we in the Matrix to download physical books?) and violates both the ISP and LSP principles.

Solving this problem using ISP:



interface Readable {
  read(): void;
}

interface Downloadable {
  download(): void;
}

class OnlineBook implements Readable, Downloadable {
  read(): void {
    // does something
  }

  download(): void {
    // does something
  }
}

class PhysicalBook implements Readable {
  read(): void {
    // does something
  }
}


Enter fullscreen mode Exit fullscreen mode

Now it's waaay better. We removed the download() method from the Book interface and added it to a derived interface, Downloadable. This way, the behavior is isolated correctly within our context, and we still respect the Interface Segregation Principle.

5. D - Dependency Inversion Principle

DIP

DIP - Dependency Inversion Principle

This one goes like this: Depend on abstractions and not on implementations.

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

Uncle Bob

Now I'll show a simple code to illustrate DIP. In this example, there's a service that gets the user from the database. First, let's create a concrete class that connects with the database:



// Low-level module
class MySQLDatabase {
  getUserData(id: number): string {
    // Logic to fetch user data from MySQL database
  }
}


Enter fullscreen mode Exit fullscreen mode

Now, let's create a service class that depends on the concrete implementation:



// High-level module
class UserService {
  private database: MySQLDatabase;

  constructor() {
    this.database = new MySQLDatabase();
  }

  getUser(id: number): string {
    return this.database.getUserData(id);
  }
}


Enter fullscreen mode Exit fullscreen mode

In the above example, UserService directly depends on the concrete implementation of MySQLDatabase. This violates DIP since the high-level class UserService is directly dependent on a low-level class.

If we want to switch to a different database system (e.g., PostgreSQL), we need to modify the UserService class, which is AWFUL!

Let's fix this code using DIP. Instead of depending on concrete implementations, the high-level class UserService should depend on abstractions. Let's create a Database interface as an abstraction:



// Abstract interface (abstraction) for the low-level module
interface Database {
  getUserData(id: number): string;
}


Enter fullscreen mode Exit fullscreen mode

Now, the concrete implementations MySQLDatabase and PostgreSQLDatabase should implement this interface:



class MySQLDatabase implements Database {
  getUserData(id: number): string {
    // Logic to fetch user data from MySQL database
  }
}

// Another low-level module implementing the Database interface
class PostgreSQLDatabase implements Database {
  getUserData(id: number): string {
    // Logic to fetch user data from PostgreSQL database
  }
}


Enter fullscreen mode Exit fullscreen mode

Finally, the UserService class can depend on the Database abstraction:



class UserService {
  private database: Database;

  constructor(database: Database) {
    this.database = database;
  }

  getUser(id: number): string {
    return this.database.getUserData(id);
  }
}


Enter fullscreen mode Exit fullscreen mode

This way, the UserService class depends on the Database abstraction, not on concrete implementations, fulfilling the Dependency Inversion Principle.

Conclusion

By adopting these principles, developers can create systems more resilient to changes, making maintenance easier and improving code quality over time.

The entirety of this article is derived from various other articles, my personal notes, and dozens of online videos that I came across while delving into the realms of Object-Oriented Programming (OOP) 🤣. The code snippets utilized in the examples were created based on my interpretation and comprehension of these principles. I really wish, my lil padawan, that I have contributed to advance your understanding and progress in your studies.

I really hope you liked this article, and don't forget to follow!

Note: Images taken from this article

Top comments (35)

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop • Edited

Love the article and the illustrations ! The last one is definitely my favourite since it is so much easier to test with that approach !
By the way you can add the language just after the quotes to highlight the code ☺️

Collapse
 
lukeskw profile image
Lucas Porfirio

Learning everyday 😅. Thanks!

Collapse
 
ruudzaki profile image
Kostiantyn Bilous

Good one! I just comment to Liskov's, that when your variable is a Type or a Collection of Types of the base class, in your caller class you shouldn't care to which objects (base or derived) it references, as their behaviour should be substitutable.

Collapse
 
thorstenhirsch profile image
Thorsten Hirsch

Awesome! Love your illustrations - it can be very hard to come up with a good idea to visualise abstract concepts.

Collapse
 
jamimeson profile image
jamimeson

Nice article, thanks for contributing to the community. One recommendation though, please make it more clear who the images belong to. My advice would be to include Ugonna Thelma‘s name explicitly at the top of the page, rather than at the bottom in a link titled “this article”.

Collapse
 
adam_b profile image
Adam Braimah

Are the images actually licensed for use by Ugonna Thelma? If not, I'd go further and say they should be removed from the article altogether.

Collapse
 
prafulla-codes profile image
Prafulla Raichurkar

This is so good!, learnt a-lot from this :D

Collapse
 
peerreynders profile image
peerreynders • Edited

FYI:

CUPID—the back story - Dan North & Associates Limited

“If you had to offer some principles for modern software development, which would you choose?” At a recent Extreme Tuesday Club (XTC) virtual meet-up, we were discussing whether the SOLID principles are outdated. A while ago I gave a tongue-in-cheek talk on the topic, so ahead of the meet-up one of the organisers asked what principles I would replace SOLID with since I disagreed with them. I have been thinking about this for some time and I proposed five of my own, which form the acronym CUPID.

favicon dannorth.net

CUPID—for joyful coding - Dan North & Associates Limited

What started as lighthearted iconoclasm, poking at the bear of SOLID, has developed into something more concrete and tangible. If I do not think the SOLID principles are useful these days, then what would I replace them with? Can any set of principles hold for all software? What do we even mean by principles?

favicon dannorth.net

… i.e. it's important to be just as familiar with the criticisms/limitations while always being mindful of the path of the expert beginner.

“Best Practices” are often “Advice; Lacking Context”.

Collapse
 
josefjelinek profile image
Josef Jelinek

I cannot agree more with the above. Presenting something like SOLID without (relatively narrow) context, when it is useful while ignoring many contexts where it can be misleading to less experienced developers, is pretty irresponsible.

Following SOLID is sometimes a solution to your problem, sometimes, it is just introducing additional problems to solve, irrelevant to the solution for the original problem.

Collapse
 
vedangit profile image
Vedangi Thokal • Edited

Superrrr helpful article! Thank you so much

Collapse
 
keyurparalkar profile image
Keyur Paralkar

I loved the explanation so much, that I am going to bookmark this post and implement it. Thanks for this.

Collapse
 
papylataupe profile image
Papy-La-Taupe

That's a great read !

It raises some questions for me tho. I learned that in a dynamic web project in javaEE, we should create a manager calling every Object-related method, like "UserManager" will call UserDAO, who will call UserDAOJdbcImpl who will then implement every user methods, hence the whole CRUD.

But the article explains that i should have a unique manager, dao and daoimpl for each CRUD line.

I get the clarity but wich SOLID model should i chose then ?
And following on that, if the sole purpose of my servlet>manager>dao>daojdbc is to deal only one method, why not direct directly go to servlet>daojdbc ? It May not be pretty but its more logic no ?

Collapse
 
peerreynders profile image
peerreynders • Edited

if the sole purpose of my servlet>manager>dao>daojdbc is to deal only one method, why not direct directly go to servlet>daojdbc?

Congratulations.

You've just re-enacted the birth of Spring, circa 2003.

Expert One-on-One J2EE Design and Development | Wiley

What is this book about? The results of using J2EE in practice are often disappointing: applications are often slow, unduly complex, and take too long to develop. Rod Johnson believes that the problem lies not in J2EE itself, but in that it is often used badly. Many J2EE publications advocate approaches that, while fine in theory, often fail in reality, or deliver no real business value. Expert One-on-One: J2EE Design and Development aims to demystify J2EE development. Using a practical focus, it shows how to use J2EE technologies to reduce, rather than increase, complexity. Rod draws on his experience of designing successful high-volume J2EE applications and salvaging failing projects, as well as intimate knowledge of the J2EE specifications, to offer a real-world, how-to guide on how you too can make J2EE work in practice. It will help you to solve common problems with J2EE and avoid the expensive mistakes often made in J2EE projects. It will guide you through the complexity of the J2EE services and APIs to enable you to build the simplest possible solution, on time and on budget. Rod takes a practical, pragmatic approach, questioning J2EE orthodoxy where it has failed to deliver results in practice and instead suggesting effective, proven approaches. What does this book cover? In this book, you will learn When to use a distributed architecture When and how to use EJB How to develop an efficient data access strategy How to design a clean and maintainable web interface How to design J2EE applications for performance Who is this book for? This book would be of value to most enterprise developers. Although some of the discussion (for example, on performance and scalability) would be most relevant to architects and lead developers, the practical focus would make it useful to anyone with some familiarity with J2EE. Because of the complete design-deployment coverage, a less advanced developer could work through the book along with a more introductory text, and successfully build and understand the sample application. This comprehensive coverage would also be useful to developers in smaller organisations, who might be called upon to fill several normally distinct roles. What is special about this book? Wondering what differentiates this book from others like it in the market? Take a look: It does not just discuss technology, but stress its practical application. The book is driven from the need to solve common tasks, rather than by the elements of J2EE. It discuss risks in J2EE development It takes the reader through the entire design, development and build process of a non-trivial application. This wouldnt be compressed into one or two chapters, like the Java Pet Store, but would be a realistic example comparable to the complexity of applications readers would need to build. At each point in the design, alternative choices would be discussed. This would be important both where theres a real problem with the obvious alternative, and where the obvious alternatives are perhaps equally valid. It emphasizes the use of OO design and design patterns in J2EE, without becoming a theoretical book

favicon wiley.com
Collapse
 
papylataupe profile image
Papy-La-Taupe

Well i didn't touch java Spring yet but that's an interesting exponation, i'll be sûre ro check that, thanks 😊

Thread Thread
 
peerreynders profile image
peerreynders

Well that was over 20 years ago and I haven't been keeping track largely because it was my sense that it was becoming exactly what it was initially trying to avoid.

Collapse
 
simmol profile image
Pavlin Angelov

Not work with Java, but had enough experience in WEB to understand the pattern :)

Since I wrote way too many things and it became a bit dense and there is a big risk of confusing you more than helping, let me try to give you one thing to think about.

Don't design/write by patterns (Solid or the Big Four or any other), refactor into one of them, when you have a specific issue to solve.
Start with the simples solution that would solve your problem and go from there :)

I highly recommend reading refactoring.com/ from Martin Fallower - The original book was written with JAVA examples, and the newer edition uses Javascript (because it does not matter the language).

But the article explains that I should have a unique manager, dao and daoimpl for each CRUD line.

The idea of each layer is for flexibility - Managers decide which DAO (Data access object?! I would guess) to call based on their internal logic, which in turn uses your DAO (internal data representation object) which is persisted using Library specific
object, based on which framework/library for DB you choose to use.

This has nothing to do with SOLID, to be honest - SOLID should (If applicable for solving the problem), be applied on the BUSINESS logic, not the plumbing :)

Half of your chain is just implementation details of how you manage your DB object persistence. And gives you the flexibility to change one part without touching the core business logic.

The only thing in this chain that should be reviewed in the light of SOLID is the Manager (how does the manager make its decisions), everything else is Framework and tech library choices dependent. Not that it can't be done differently, but it's not coming out of the principles in the article themselves.

Some of these things start to make way more sense by just writing more code.
Write your proposed version servlet>daojdbc, then try to make it so servlet can be reused in different contexts and save another object (how would you manage that) ?

With enough changes done to the system and trying to follow the pain (of changing and retesting the same things over and over again) you will probably come to the same conclusion that separating Servlet from Manager from daojdbc objects makes sense :) in some cases.

The truth is these things don't make sense for a simple TODO app that you will never maintain or change ever again. They only matter if your problem is complex enough and a need arises to change it.

So create your servlet -> daojdbc solution if it makes sense for the requirements for your application now. Just be ready to change it once new requirement comes along ;)

Collapse
 
papylataupe profile image
Papy-La-Taupe

Thank you for taking the time writing such a complete answer .

I read it all of course and i think this quote summarize it quite efficiently, and I full agréé :

Don't design/write by patterns (Solid or the Big Four or any other), refactor into one of them, when you have a specific issue to solve.
Start with the simples solution that would solve your problem and go from there :)

Before reading this I was actually trying to explain the DAO/MVC2 process to a fellow classmate, and I ended up telling her "do your jsp, then your servlet, then you jdbcimpl. Then write all your code in it, and when it starts looking downright confusing, only then add your factory, manager, etc etc. By then you will have understood WHY you were doing it."

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

Very nice and well written 🍻

Some comments may only be visible to logged-in visitors. Sign in to view all comments.