Hello, dart and flutter developers! Today’s topic is SOLID principles in Dart. This is not about adding new language features; rather, it is about improving code quality and maintainability. These best practices are intended to be used in any object-oriented language, not just Dart.
What are the fundamental principles of Flutter?
The acronym SOLID stands for five well-known design principles:
- Single Responsibility.
- Open-Closed.
- Liskov Substitution.
- Interface Segregation.
- and Dependency Inversion.
All of which will be discussed further below. They are very popular among developers and are widely accepted as good practices.
What issue did these S.O.L.I.D principles solve?
- Jumping multiple methods with the same name and spending a significant amount of time to fix a minor bug.
- Rereading the code several times to find the section that needs to be changed.
It’s difficult to understand what the method does.
Essentially, we spend far more time understanding than we do writing code.
SOLID Principal in dart – flutter
Single Responsibility principle
This principle states that a class should only change for one reason. In other words, you should design classes with a single “responsibility” to make them easier to maintain and more difficult to break.
As the name implies, it is solely responsible. A class should only be responsible for one thing, which means it should only change for one reason.
Let’s start with a bad practice in which maintenance is unpleasant. Because everything is in one place: validation, getRequest, and painting.
class Shapes { List < String > cache = List < > (); // Calculations double squareArea(double l) { /* ... */ } double circleArea(double r) { /* ... */ } double triangleArea(double b, double h) { /* ... */ } // Paint to the screen void paintSquare(Canvas c) { /* ... */ } void paintCircle(Canvas c) { /* ... */ } void paintTriangle(Canvas c) { /* ... */ } // GET requests String wikiArticle(String figure) { /* ... */ } void _cacheElements(String text) { /* ... */ } }
Because it handles internet requests, work, and calculations all in one place, This class completely destroys the SRP. This class will change frequently in the future: whenever a method requires maintenance, you’ll have to change some code, which may break other parts of the class. What do you think about this?
// Calculations and logic abstract class Shape { double area(); } class Square extends Shape {} class Circle extends Shape {} class Rectangle extends Shape {} // UI painting class ShapePainter { void paintSquare(Canvas c) { /* ... */ } void paintCircle(Canvas c) { /* ... */ } void paintTriangle(Canvas c) { /* ... */ } } // Networking class ShapesOnline { String wikiArticle(String figure) { /* ... */ } void _cacheElements(String text) { /* ... */ } }
Open-Closed Principle
You should be able to extend the behavior of a (class, module, function, etc.) without modifying it.
According to the open-closed principle, a good programmer should add new behaviors without changing the existing source code. This concept is notoriously described as “classes should be open for extension and closed for changes”.
// Looks good but wait! Customer wants a revision in the last second and wants a third number to add. How we can solve this problem without breaking the app?int sum(int a, int b) { int sum(int a, int b) { return a + b; } // Breaks old functionality int sum(int a, int b, int c) { return a + b + c; } // There is no problem for old codes because this one wont be affected by new codes int sum(int a, int b, [int c = 0]) { return a + b + c; }
Liskov Substitution Principle
This principle suggests that “parent classes should be easily substituted with their child classes without blowing up the application”. To explain this, consider the following example.
The architecture ensures that the subclass maintains the logic correctness of the code. Composition with interfaces is preferred over inheritance.
abstract class StorageService { void get(String key); void put(String key, String value); } class HiveService implements StorageService { @override void get(String path) {} @override void put(String key, String value) {} } class ObjectBoxService implements StorageService { @override void get(String key) {} @override void put(String key, String value) {} }
Interface Segregation Principle
This principle suggests that “many client specific interfaces are better than one general interface“. This is the first principle that applies to an interface; the other three principles apply to classes.
abstract class Teacher{ void work(); void sleep(); } class Human implements Teacher{ void work() => print("I do a lot of work"); void sleep() => print("I need 10 hours per night..."); } class Robot implements Teacher{ void work() => print("I always work"); void sleep() {} // ?? }
The issue here is that robots do not require sleep, so we are unsure how to implement that method. Worker represents an entity capable of performing work, and it is assumed that if you can work, you can also sleep. In our case, this assumption is not always correct, so we’ll divide the class:
abstract class Teacher{ void work(); } abstract class Sleeper { void sleep(); } class Human implements Teacher, Sleeper { void work() => print("I do a lot of work"); void sleep() => print("I need 10 hours per night..."); } class Robot implements Teacher{ void work() => print("I'm always iiii work as teacher"); }
Dependency Inversion Principle (DIP)
The issue here is that robots do not require sleep, so we are unsure how to implement that method. Worker represents an entity capable of performing work, and it is assumed that if you can work, you can also sleep. In our case, this assumption is not always correct, so we’ll divide the class:
Assume we have a Firebase-integrated app and want to migrate the database to a PhpDataBaseserver later. If we make them rely on an interface, there will be no problem; all we need to do is change the service.
As if you were changing the batteries in a toy. The toy is still an old toy, but the batteries are new.
abstract class DataApi { Future get(String path); } class FirebaseService implements DataApi { @override Future get(String path) async { return await Future.value('Data from Firebase'); } } class PhpDBService implements DataApi { @override Future get(String path) async { return await Future.value('Data from PhpDBService '); } }
Instead of use
abstract class DataService { FutureOr<String> fetch(); } class FirebaseService extends DataService { @override Future<String> fetch() async => await 'data'; } class LocaDataService extends DataService { @override String fetch() => 'data'; }
Conclusion
Avoid being trapped by Solid.
S.O.L.I.D. Principles are not rules, but rather principles.
When using this, always use common sense. Your goal is to make your code maintainable and easy to extend.
Avoid fragmenting your code excessively for the sake of SRP or S.O.L.I.D.
Thank you for reading the SOLID principles ….Have a fine day!!