Practical Insights On SOLID DRY KISS Explained In `Noob Vs Pro` Way

Practical Insights On SOLID DRY KISS Explained In `Noob Vs Pro` Way

Hello, fellow developers!

Today, we're diving deep into the world of SOLID, DRY, and KISS - three magical acronyms that can level up your coding skills. Buckle up, and let's get started!


The SOLID Pillars of Development

Let's start with SOLID. It's not just a word to describe something firm and rigid; in the world of software development, SOLID is a crucial acronym that encapsulates five key design principles. When applied correctly, these principles can significantly improve your ability to write modular, flexible, and robust code.

1. Single Responsibility Principle (SRP)

Imagine you have a class in your Flutter code, and it's doing everything - storing user data, saving it to a database, validating input. Hold on, isn't that too much for one class? That's where the Single Responsibility Principle comes into play, asserting that a class should have one, and only one, reason to change.

Take a look at the following 'Noob Way' of writing a user class:

class User {
  String name;
  String email;

  User({this.name, this.email});

  void saveUser() {
    // Database save logic
  }
}

Here, the User class is doing too much: it’s not just storing user data, it’s also responsible for saving it to the database. This violates SRP. Here's the 'Pro Way' to do it:

class User {
  String name;
  String email;

  User({this.name, this.email});
}

class UserRepository {
  void saveUser(User user) {
    // Database save logic
  }
}

By separating the data storage and persistence responsibilities into two distinct classes, our code becomes more modular and easier to manage.

2. Open-Closed Principle (OCP)

Imagine you have a logger class in your app that prints logs to the console. Later, you decide you also want to save logs to a file. What do you do? Modify the existing logger class? No, that would violate the Open-Closed Principle, which states that classes should be open for extension, but closed for modification.

Look at the following 'Noob Way' of writing a logger class:

class Logger {
  void log(String message) {
    print(message);
  }
}

If we want to modify this logger to also support file logging, we'd have to change the existing class. This isn't great for maintainability or flexibility. Instead, let's follow the 'Pro Way':

abstract class Logger {
  void log(String message);
}

class ConsoleLogger extends Logger {
  @override
  void log(String message) {
    print(message);
  }
}

With this structure, we can easily extend our logging system with a new class like FileLogger without having to modify the existing ConsoleLogger.

3. Liskov Substitution Principle (LSP)

Ever heard of the saying, "If it looks like a duck, quacks like a duck, but needs batteries – you probably have the wrong abstraction." That's Liskov Substitution Principle for you. It insists that subclasses must be substitutable for their base classes without causing any hiccups.

Consider the following 'Noob Way' of dealing with birds:

class Bird {
  void fly() {
    print('Flying');
  }
}

class Penguin extends Bird {}

At first glance, this might seem fine. But wait, penguins can't fly! So, having a Penguin class extend Bird, which can fly, doesn't make sense. Let's revise this the 'Pro Way':

abstract class Bird {}

abstract class FlyingBird extends Bird {
  void fly();
}

class Sparrow extends FlyingBird {
  @override
  void fly() {
    print('Flying');
  }
}

class Penguin extends Bird {}

Now that's better! We have our FlyingBird class for birds that can fly, and our Penguin class can just extend Bird without any flying-related confusion.

4. Interface Segregation Principle (ISP)

Let's say you have an interface Worker with methods work and eat. If you have a Robot class implementing Worker, it will be forced to implement eat method as well. But hang on, robots don't eat! This is where Interface Segregation Principle comes in, stating that no client should be forced to depend on interfaces it doesn't use.

Check out the following 'Noob Way' of implementing a worker interface:

abstract class Worker {
  void work();
  void eat();
}

class Robot implements Worker {
  @override
  void work() {
    print('Working');
  }

  @override
  void eat() {
    // Robots don't eat!
  }
}

That doesn't look right, does it? Here's the 'Pro Way':

abstract class Worker {
  void work();
}

abstract class Eater {
  void eat();
}

class Robot implements Worker {
  @override
  void work() {
    print('Working');
  }
}

class Human implements Worker, Eater {
  @override
  void work() {
    print('Working');
  }

  @override
  void eat() {
    print('Eating');
  }
}

This way, our Robot class can just implement Worker without having to worry about any eating-related functionalities.

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle encourages us to write code that depends upon abstractions, not upon concrete details. Essentially, it's all about the high level modules (those calling a function) not depending on the low level modules (those being called).

Here's the 'Noob Way' of creating a user repository:

class MySQLDatabase {
  void save(String data) {
    print('Saving data to MySQL Database');
  }
}

class UserRepository {
  final MySQLDatabase database;

  UserRepository(this.database);

  void saveUser(String userData) {
    database.save(userData);
  }
}

This UserRepository class is tightly coupled with the MySQLDatabase. But what if we want to switch to a different database system in the future? We'd have to rewrite our UserRepository! Instead, we should follow the 'Pro Way':

abstract class Database {
  void save(String data);
}

class MySQLDatabase implements Database {
  @override
  void save(String data) {
    print('Saving data to MySQL Database');
  }
}

class UserRepository {
  final Database database;

  UserRepository(this.database);

  void saveUser(String userData) {
    database.save(userData);
  }
}

In this scenario, our UserRepository depends on an abstract Database, allowing us to easily change the database implementation in the future without any major code overhaul.

That wraps up our overview of the SOLID principles.

Now let's understand DRY and KISS principles.


The DRY and KISS Principles - Power Tools for Development

Don't Repeat Yourself (DRY)

This one's a classic! 'Don't Repeat Yourself' or DRY, as we like to call it, is all about reducing repetition in your code. Duplication can make your code hard to maintain and unnecessarily long. Instead, aim for reusing parts of your code to make it cleaner and more efficient.

Consider this example:

Noob Way:

void printUserData(User user) {
  print('Name: ' + user.name);
  print('Email: ' + user.email);
}

void printAdminData(Admin admin) {
  print('Name: ' + admin.name);
  print('Email: ' + admin.email);
}

In this case, we're repeating the same printing logic twice. We're better off following the 'Pro Way':

Pro Way:

void printUserData(User user) {
  print('Name: ' + user.name);
  print('Email: ' + user.email);
}

void printAdminData(Admin admin) {
  printUserData(admin);
}

By reusing printUserData in printAdminData, we've eliminated duplication and made our code simpler and easier to maintain.

Keep It Simple, Stupid (KISS)

The KISS principle urges us to keep our code as simple and straightforward as possible. By avoiding unnecessary complexity, we make our code more understandable and easier to maintain.

Check out this 'Noob Way' example:

Noob Way:

List<int> getEvens(List<int> numbers) {
  return numbers.where((number) => number.isEven).toList();
}

List<int> getOdds(List<int> numbers) {
  return numbers.where((number) => number.isOdd).toList();
}

void printNumbers() {
  List<int> numbers = [1, 2, 3, 4, 5];
  print(getEvens(numbers));
  print(getOdds(numbers));
}

Here, we've created two additional functions getEvens and getOdds that are only used once. This unnecessarily complicates our code. Instead, we should stick to the 'Pro Way':

Pro Way:

void printNumbers() {
  List<int> numbers = [1, 2, 3, 4, 5];
  print(numbers.where((number) => number.isEven).toList());
  print(numbers.where((number) => number.isOdd).toList());
}

By doing everything within printNumbers, we've kept our code simple and straight-forward, just as the KISS principle recommends.


Conclusion

And that's a wrap on our overview of the SOLID, DRY, and KISS principles. Remember, the goal here is to write clean, efficient, and maintainable code. While these principles can guide us, don't forget the most important principle of all - write code that makes sense to you and your team. After all, you're the one who's going to be working with it!

Until next time, keep on coding!


Before we go...

Thanks for reading!

If you loved this, drop a like and consider following me :)

I share insights on flutter, open-source & software development to help you become a 10x developer.

Got any doubt or wanna chat? React out to me on twitter or linkedin.