What SOLID is and how its principles are implemented in Dart.

Denis
4 min readNov 24, 2023

SOLID is an acronym representing five principles developed by Robert Martin (or Uncle Bob, as he is also called). These principles are fundamental to object-oriented programming and design.

  1. Single Responsibility Principle (SRP) — This principle states that a class should have only one reason to change. Everything within a class should be focused on implementing a single task. If a class is responsible for multiple tasks, it should be divided into several classes.
  2. Open/Closed Principle (OCP) — The software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, a programmer should be able to add new functionality without altering existing code.
  3. Liskov Substitution Principle (LSP) — Functions using pointers to a base class should be able to use objects of a derived class without knowing it. In short, the behavior of derived classes should not contradict the behavior defined by the base class.
  4. Interface Segregation Principle (ISP) — Clients should not be forced to depend on interfaces they do not use. This principle aims to eliminate the shortcomings of monolithic interfaces.
  5. Dependency Inversion Principle (DIP) — Dependencies in a system should be based on abstractions. High-level modules should not depend on low-level modules. Both types of modules should depend on abstractions.

Examples of Usage in Dart

  1. Single Responsibility Principle (SRP)

Let’s consider an example of SRP violation:

class Order {
void calculateTotalSum() { /*...*/ }
void getItems() { /*...*/ }
void getItemCount() { /*...*/ }
void addItem(Item item) { /*...*/ }
void deleteItem(Item item) { /*...*/ }

void printOrder() { /*...*/ }
void showOrder() { /*...*/ }

void load() { /*...*/ }
void save() { /*...*/ }
void update() { /*...*/ }
void delete() { /*...*/ }
}

In this example, the Order class handles various responsibilities, violating the single responsibility principle. According to SRP, this should be separated into three different classes:

class Order {
void calculateTotalSum() { /*...*/ }
void getItems() { /*...*/ }
void getItemCount() { /*...*/ }
void addItem(Item item) { /*...*/ }
void deleteItem(Item item) { /*...*/ }
}

class OrderRepository {
void load() { /*...*/ }
void save() { /*...*/ }
void update() { /*...*/ }
void delete() { /*...*/ }
}

class OrderView {
void printOrder(Order order) { /*...*/ }
void showOrder(Order order) { /*...*/ }
}

Now, each class has a single responsibility, making the code more maintainable and reducing the risk of errors.

SRP is one of the most important SOLID principles and general software design principles. It helps maintain code cleanliness, improves maintainability, and makes the system more flexible to changes.

2. Open/Closed Principle (OCP)

Let’s say we have an AreaCalculator class that calculates the area of different shapes:

class Rectangle {
double height;
double width;
}

class AreaCalculator {
double calculateRectangleArea(Rectangle rectangle) {
return rectangle.height * rectangle.width;
}
}

If we want to add a new shape, such as a circle, we would have to modify AreaCalculator. Instead, we should create a common interface Shape and implement it for each shape:

abstract class Shape {
double calculateArea();
}

class Rectangle implements Shape {
double height;
double width;

@override
double calculateArea() {
return height * width;
}
}

class Circle implements Shape {
double radius;

@override
double calculateArea() {
return 3.14 * radius * radius;
}
}

class AreaCalculator {
double calculateArea(Shape shape) {
return shape.calculateArea();
}
}

3. Liskov Substitution Principle (LSP)

Suppose we have a Bird class that can fly:

class Bird {
void fly() {
// Flight Realization
}
}
class Duck extends Bird {}

But not all birds can fly (e.g., penguins). If we create a Penguin class that inherits from Bird but cannot fly, it would violate LSP. Instead, let's create a Flyable interface and apply it only to birds that can fly:

abstract class Bird {}

abstract class Flyable {
void fly();
}

class Duck extends Bird implements Flyable {
@override
void fly() {
// implementation for flight
}
}

class Penguin extends Bird {} // Penguins cannot fly, so they don't implement Flyable

4. Interface Segregation Principle (ISP)

If we have an interface with many methods, and a class implementing that interface does not use all of its methods, it violates ISP:

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

class HumanWorker implements Worker {
@override
void work() {
// implementation for work
}

@override
void eat() {
// implementation for eat
}
}

class RobotWorker implements Worker {
@override
void work() {
// implementation for work
}

@override
void eat() {
// Robots don't eat, but they are forced to implement this method
}
}

To fix this, we can split Worker into multiple interfaces:

abstract class Worker {
void work();
}

abstract class Eater {
void eat();
}

class HumanWorker implements Worker, Eater {
@override
void work() {
// implementation for work
}

@override
void eat() {
// implementation for eat
}
}

class RobotWorker implements Worker {
@override
void work() {
// implementation for work
}
}

5. Dependency Inversion Principle (DIP)

Instead of classes depending on concrete implementations, they should depend on abstractions:

class MySQLConnection {
void connect() {
// Implementation of connection to MySQL
}
}

class DatabaseManager {
final MySQLConnection connection;

DatabaseManager(this.connection);

void connect() {
connection.connect();
}
}

If we want to change the database to PostgreSQL, we would have to modify DatabaseManager. Instead, let's create an abstract class or interface DatabaseConnection:

abstract class DatabaseConnection {
void connect();
}

class MySQLConnection implements DatabaseConnection {
@override
void connect() {
// Implementation of connection to MySQL
}
}

class PostgreSQLConnection implements DatabaseConnection {
@override
void connect() {
// Implementation of connection to PostgreSQL
}
}

class DatabaseManager {
final DatabaseConnection connection;

DatabaseManager(this.connection);

void connect() {
connection.connect();
}
}

These are simple examples, but they illustrate how to apply SOLID principles when designing and writing Dart code.

--

--