DEV Community

Cover image for From Fish to Flying Dogs: Vivek’s Playful Guide to SOLID Principles with Tara at NonStop
Anagha GN
Anagha GN

Posted on

From Fish to Flying Dogs: Vivek’s Playful Guide to SOLID Principles with Tara at NonStop

Scene: Tara, a new Intern, has just joined NonStop IO Technologies Pvt. Ltd. She’s eager to learn about best coding practices. Vivek, the Project Manager, decides to explain the SOLID principles to her using the context of aquatic and terrestrial animals. They are sitting in the company lounge with a cup of coffee.

Vivek: (smiling) Welcome to NonStop IO Technologies, Tara! How’s your first day going?

Tara: (grinning) It’s been great so far, Vivek. Just trying to soak in all the new information. By the way, I’ve been hearing a lot about these SOLID principles. What’s all the hype about?

Vivek: (chuckling) Ah, the famous SOLID principles! They’re like the Avengers of software design — each one has its superpower to save us from bad code. Let’s start with the Single Responsibility Principle, or SRP. Imagine a Zoo class that handles feeding, entertaining, and displaying animals. It’s like asking a fish to swim, walk, and fly. Impossible, right?

Tara: (laughing) I’d love to see a flying fish, though!

Vivek: (laughs) True! But in coding terms, it’s a mess. Here’s an example of how the Zoo class violates SRP:

Violation Example:

class Zoo {
  void feedAnimals() {
    print('Feeding animals');
    // Code to feed animals
  }
  void entertainAnimals() {
    print('Entertaining animals');
    // Code to entertain animals
    Fish fish = Fish();
    fish.swim();
    Bird bird = Bird();
    bird.fly();
    bird.walk();
    Dog dog = Dog();
    dog.walk();
    dog.swim();
  }
void displayAnimals() {
    print('Displaying animals');
    // Code to display animals to visitors
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

The Zoo class handles multiple responsibilities: feeding animals, entertaining them, and displaying them. This violates SRP because changing one responsibility (e.g., how animals are entertained) would require modifying the Zoo class, making it more complex and error-prone.
Correction Example:

class Feeder {
  void feedAnimals() {
    print('Feeding animals');
    // Code to feed animals
  }
}
class Entertainer {
  void entertainAnimals() {
    print('Entertaining animals');
    // Code to entertain animals
    Fish fish = Fish();
    fish.swim();
    Bird bird = Bird();
    bird.fly();
    bird.walk();
    Dog dog = Dog();
    dog.walk();
    dog.swim();
  }
}
class Displayer {
  void displayAnimals() {
    print('Displaying animals');
    // Code to display animals to visitors
  }
}
Correction Explanation:

Each class (Feeder, Entertainer, Displayer) now has a single responsibility. This makes the code more modular and easier to maintain.
Tara: Got it! So each class has its responsibility. What’s next?

Vivek: Next up, OCP — the Open/Closed Principle. Classes should be open for extension but closed for modification. Here’s how our Zoo class violates OCP:

Violation Example:

class Zoo {
  void showAnimals() {
    Fish fish = Fish();
    Bird bird = Bird();
    Dog dog = Dog();
    fish.swim();
    bird.fly();
    bird.walk();
    dog.swim();
    dog.walk();
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

The Zoo class directly depends on specific implementations of Fish, Bird, and Dog. If you want to add a new animal, you’d have to modify the showAnimals method, which violates OCP.
Correction Example:

abstract class Swimmer {
  void swim();
}
abstract class Walker {
  void walk();
}
abstract class Flyer {
  void fly();
}
class Fish implements Swimmer {
  @override
  void swim() {
    print('Fish is swimming');
  }
}
class Bird implements Walker, Flyer {
  @override
  void fly() {
    print('Bird is flying');
  }
@override
  void walk() {
    print('Bird is walking');
  }
}
class Dog implements Walker, Swimmer {
  @override
  void swim() {
    print('Dog is swimming');
  }
@override
  void walk() {
    print('Dog is walking');
  }
}
class Zoo {
  final List<Swimmer> swimmers;
  final List<Walker> walkers;
  final List<Flyer> flyers;

Zoo(this.swimmers, this.walkers, this.flyers);

void showSwimmers() {
    for (var swimmer in swimmers) {
      swimmer.swim();
    }
  }
void showWalkers() {
    for (var walker in walkers) {
      walker.walk();
    }
  }
void showFlyers() {
    for (var flyer in flyers) {
      flyer.fly();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Correction Explanation:

By using interfaces (Swimmer, Walker, Flyer), the Zoo class can work with any new animal that implements these interfaces, adhering to OCP.
Tara: So, no tearing down walls for a new animal? Sounds like a relief for the animals and the developers!

Vivek: (grinning) Exactly! Now, LSP — Liskov Substitution Principle. This principle ensures that derived classes can substitute base classes without breaking the system. Here’s how it’s violated:

Violation Example:

abstract class Swimmer {
  void swim();
}

class Fish implements Swimmer {
  @override
  void swim() {
    print('Fish is swimming');
  }
}

class Dog implements Swimmer {
  @override
  void swim() {
    print('Dog is swimming');
  }

  void bark() {
    print('Dog is barking');
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

In this example, Dog is substituting for Swimmer, but it introduces additional behaviors (bark()) that Fish doesn’t have. This could cause issues when using a Swimmer in a context where only swimming is expected, as Dog introduces unexpected behavior.
*Correction Example:
*

abstract class Swimmer {
  void swim();
}

class Fish implements Swimmer {
  @override
  void swim() {
    print('Fish is swimming');
  }
}

class Dog implements Swimmer {
  @override
  void swim() {
    print('Dog is swimming');
  }
}
Enter fullscreen mode Exit fullscreen mode

Correction Explanation:

Both Fish and Dog implement the Swimmer interface correctly, focusing solely on swimming. This adheres to LSP as both classes can be used interchangeably wherever a Swimmer is expected without introducing unexpected behaviors.
This adjustment ensures that the Swimmer interface remains consistent and reliable, adhering to LSP by not introducing additional methods or behaviors that aren’t expected by the base interface.

Tara: (giggling) Unless it’s on some sort of superhero potion!

Vivek: (laughs) Exactly! For superheroes, we have a separate interface. Then, we have ISP — the Interface Segregation Principle. This principle means no client should be forced to depend on interfaces it doesn’t use. Here’s how it’s violated:

Violation Example:

abstract class Animal {
  void swim();
  void walk();
  void fly();
}
class Fish implements Animal {
  @override
  void swim() {
    print('Fish is swimming');
  }
@override
  void walk() {
    throw UnimplementedError(); // Fish can’t walk
  }
@override
  void fly() {
    throw UnimplementedError(); // Fish can’t fly
  }
}
class Bird implements Animal {
  @override
  void swim() {
    throw UnimplementedError(); // Bird can’t swim
  }
@override
  void walk() {
    print('Bird is walking');
  }
@override
  void fly() {
    print('Bird is flying');
  }
}
Explanation:

The Animal interface forces all animals to implement swim, walk, and fly, even if they don’t need those methods. This violates ISP because Fish and Bird are forced to implement methods that don’t apply to them.
Correction Example:

abstract class Swimmer {
  void swim();
}
abstract class Walker {
  void walk();
}
abstract class Flyer {
  void fly();
}
class Fish implements Swimmer {
  @override
  void swim() {
    print('Fish is swimming');
  }
}
class Bird implements Walker, Flyer {
  @override
  void fly() {
    print('Bird is flying');
  }
@override
  void walk() {
    print('Bird is walking');
  }
}
Enter fullscreen mode Exit fullscreen mode

Correction Explanation:

By using separate interfaces (Swimmer, Walker, Flyer), each animal class only implements the methods it needs. This adheres to ISP by ensuring that classes are not burdened with unnecessary methods.
Tara: (laughing) Or making a dog try to fly — though, a cape might help!

Vivek: (chuckling) True, true. And finally, we have DIP — Dependency Inversion Principle. High-level modules shouldn’t depend on low-level modules but on abstractions. Here’s how it’s violated:

Violation Example:

class Zoo {
  void showAnimals() {
    Fish fish = Fish();
    Bird bird = Bird();
    Dog dog = Dog();
    fish.swim();
    bird.fly();
    bird.walk();
    dog.swim();
    dog.walk();
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

In this design, the Zoo class depends on concrete implementations (Fish, Bird, Dog). If we want to change the behavior or add new animal types, we need to modify the Zoo class, which violates DIP.
Correction Example:

abstract class Swimmer {
  void swim();
}

abstract class Walker {
  void walk();
}

abstract class Flyer {
  void fly();
}

class Fish implements Swimmer {
  @override
  void swim() {
    print('Fish is swimming');
  }
}

class Bird implements Walker, Flyer {
  @override
  void fly() {
    print('Bird is flying');
  }

  @override
  void walk() {
    print('Bird is walking');
  }
}

class Dog implements Walker, Swimmer {
  @override
  void swim() {
    print('Dog is swimming');
  }

  @override
  void walk() {
    print('Dog is walking');
  }
}

class Zoo {
  final Swimmer swimmer;
  final Walker walker;
  final Flyer flyer;

  Zoo(this.swimmer, this.walker, this.flyer);

  void performShow() {
    swimmer.swim();
    walker.walk();
    flyer.fly();
  }
}

void main() {
  Zoo zoo = Zoo(Fish(), Dog(), Bird());
  zoo.performShow();
}
Enter fullscreen mode Exit fullscreen mode

Correction Explanation:

We define Swimmer, Walker, and Flyer interfaces as abstractions. Fish, Bird, and Dog implement these interfaces as low-level modules. The Zoo class depends on these abstractions (Swimmer, Walker, Flyer) rather than concrete implementations.
Now, the Zoo class is not tightly coupled to specific animal classes. We can easily swap out different implementations (e.g., replace Dog with a Cat that implements Walker) without changing the Zoo class. This design adheres to the Dependency Inversion Principle, promoting flexibility and maintainability.
Tara: So, the pet-sitter is prepared for anything — from fish to flying birds?

Vivek: (grinning) Exactly! It’s about being adaptable and prepared for change. And that, my dear intern, is a quick tour through the SOLID zoo!

Tara: (smiling) That was fun and enlightening! Now I can brag about understanding SOLID principles to my friends.

Vivek: (laughing) You sure can! And remember, this is just the beginning. At NonStop IO Technologies Pvt. Ltd., we’re all about quality code and having fun while learning. You’ll get the best mentoring and training here.

Tara: (grateful) I can already see that. Thanks for the awesome explanation, Vivek! I’m looking forward to learning more.

Vivek: (smiling) Anytime, Tara! Welcome aboard, and remember, we’re all in this together — whether you’re a dog, bird, or even a flying fish! 😊

Conclusion: Understanding and applying SOLID principles helps create a more flexible, maintainable, and scalable codebase. At NonStop IO Technologies Pvt. Ltd., we take pride in providing top-notch mentoring and training to ensure our developers can apply these principles effectively, making our software as robust and adaptable as a well-tended zoo.

Top comments (0)