|
| 1 | +# **_`Solid Principles`_** |
| 2 | + |
| 3 | +1. [1. Single Responsibility Principle (SRP)](<#1.-Single-Responsibility-Principle-(SRP)>) |
| 4 | +1. [2. Open closed principle](#2.-Open-closed-principle) |
| 5 | +1. [3. Liskov Substitution Principle (LSP)](<#3.-Liskov-Substitution-Principle-(LSP)>) |
| 6 | +1. [4. Interface Segregation Principle (ISP)](<#4.-Interface-Segregation-Principle-(ISP)>) |
| 7 | +1. [5. Dependency Inversion Principle (DIP)](<#5.-Dependency-Inversion-Principle-(DIP)>) |
| 8 | + |
| 9 | +## 1. Single Responsibility Principle (SRP) |
| 10 | + |
| 11 | +- a class should only have a single responsibility |
| 12 | +- so that it could change for one reason and no more. |
| 13 | +- In other words, |
| 14 | + - you should create classes dealing with a single duty |
| 15 | + - so that they’re easier to maintain and harder to break. |
| 16 | + |
| 17 | +### ❌ Wrong |
| 18 | + |
| 19 | +```dart |
| 20 | +class Shapes { |
| 21 | + List<String> cache = List<>(); |
| 22 | + // Calculations |
| 23 | + double squareArea(double l) { /* ... */ } |
| 24 | + double circleArea(double r) { /* ... */ } |
| 25 | + double triangleArea(double b, double h) { /* ... */ } |
| 26 | + // Paint to the screen |
| 27 | + void paintSquare(Canvas c) { /* ... */ } |
| 28 | + void paintCircle(Canvas c) { /* ... */ } |
| 29 | + void paintTriangle(Canvas c) { /* ... */ } |
| 30 | + // GET requests |
| 31 | + String wikiArticle(String figure) { /* ... */ } |
| 32 | + void _cacheElements(String text) { /* ... */ } |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +### ✅ Right |
| 37 | + |
| 38 | +- class for each operation or logic |
| 39 | + |
| 40 | +```dart |
| 41 | +// Calculations and logic |
| 42 | +abstract class Shape { |
| 43 | + double area(); |
| 44 | +} |
| 45 | +class Square extends Shape {} |
| 46 | +class Circle extends Shape {} |
| 47 | +class Rectangle extends Shape {} |
| 48 | +// UI painting |
| 49 | +class ShapePainter {} |
| 50 | +// Networking |
| 51 | +class ShapesOnline {} |
| 52 | +``` |
| 53 | + |
| 54 | +- There are 3 separated classes focusing on a single task to accomplish: |
| 55 | + |
| 56 | + they are easier to read, test, maintain and understand. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +## 2. Open closed principle |
| 63 | + |
| 64 | +- in a good architecture |
| 65 | + - you should be able to add new behaviors |
| 66 | + - without modifying the existing source code. |
| 67 | +- This concept is notoriously described with the sentence: |
| 68 | + > **"software entities should be open for extensions but closed for modifications "**. |
| 69 | +
|
| 70 | +### ❌ Wrong |
| 71 | + |
| 72 | +```dart |
| 73 | +/// SRP class |
| 74 | +class Rectangle { |
| 75 | + final double width; |
| 76 | + final double height; |
| 77 | + Rectangle(this.width, this.height); |
| 78 | +} |
| 79 | +
|
| 80 | +/// SRP class |
| 81 | +class Circle { |
| 82 | + final double radius; |
| 83 | + Rectangle(this.radius); |
| 84 | + double get PI => 3.1415; |
| 85 | +} |
| 86 | +
|
| 87 | +/// problem is here |
| 88 | +class AreaCalculator { |
| 89 | + double calculate(Object shape) { |
| 90 | + if (shape is Rectangle) { |
| 91 | + // Smart cast |
| 92 | + return r.width * r.height; |
| 93 | + } else { |
| 94 | + final c = shape as Circle; |
| 95 | + return c.radius * c.radius * c.PI; |
| 96 | + } |
| 97 | + } |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +- Both Rectangle and Circle respect the SRP |
| 102 | +- The problem is inside AreaCalculator: |
| 103 | + - because if we added other shapes, |
| 104 | + - we would have to edit the code to **add more if conditions** |
| 105 | + |
| 106 | +### ✅ Right |
| 107 | + |
| 108 | +- replaced if => with interface |
| 109 | +- open for extensions >> open to add new shape |
| 110 | +- close for modification >> no written class or method will modified |
| 111 | + |
| 112 | +```dart |
| 113 | +// Use it as an interface |
| 114 | +abstract class Area { |
| 115 | + double computeArea(); |
| 116 | +} |
| 117 | +// Every class calculates the area by itself |
| 118 | +class Rectangle implements Area {} |
| 119 | +class Circle implements Area {} |
| 120 | +class Triangle implements Area {} |
| 121 | +class Rhombus implements Area {} |
| 122 | +class Trapezoid implements Area {} |
| 123 | +class AreaCalculator { |
| 124 | + double calculate(Area shape) { |
| 125 | + return shape.computeArea(); |
| 126 | + } |
| 127 | +} |
| 128 | +
|
| 129 | +``` |
| 130 | + |
| 131 | +- Thanks to the **interface**, |
| 132 | +- now we have the possibility to add or remove as many classes as we want |
| 133 | +- without changing AreaCalculator. |
| 134 | + |
| 135 | + - For example, if we added class Square implements Area it would automatically be "compatible" with the double calculate(...) method. |
| 136 | + |
| 137 | + - The gist of this principle is: depend on abstractions and not on implementations. |
| 138 | + - Thanks to abstract classes you work with abstractions and not with the concrete implementations: your code doesn’t rely on "predefined" entities. |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## 3. Liskov Substitution Principle (LSP) |
| 145 | + |
| 146 | +- that **`subclasses should be replaceable with superclasses`** |
| 147 | + |
| 148 | + - without altering the logical correctness of the program. |
| 149 | + |
| 150 | +- In practical terms, it means that a |
| 151 | + - subtype must guarantee the "usage conditions" of its supertype |
| 152 | + - plus something more it wants |
| 153 | + |
| 154 | +### ❌ Wrong |
| 155 | + |
| 156 | +```dart |
| 157 | +class Rectangle { |
| 158 | + double width; |
| 159 | + double height; |
| 160 | + Rectangle(this.width, this.height); |
| 161 | +} |
| 162 | +class Square extends Rectangle { |
| 163 | + Square(double length): super(length, length); |
| 164 | +} |
| 165 | +
|
| 166 | +/// Fail >>> Not Valid change width and height with different values of square |
| 167 | +void main() { |
| 168 | + Rectangle fail = Square(3); |
| 169 | + fail.width = 4; |
| 170 | + fail.height = 8; |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +- We have a big logic problem here. |
| 175 | + - A square must have 4 sides with the same length |
| 176 | + - but the rectangle doesn’t have this restriction. |
| 177 | +- at this point we have a square with 2 sides of length 4 and 2 sides of length 8 |
| 178 | + ... which is absolutely wrong! |
| 179 | + |
| 180 | +- This example also shows that: |
| 181 | + - inheriting from **abstract classes or interfaces**, |
| 182 | + - rather than **concrete classes**, |
| 183 | + - is a very good practice. Prefer composition (with interfaces) over inheritance. |
| 184 | + |
| 185 | +### ✅ Right |
| 186 | + |
| 187 | +- to solve this problem, |
| 188 | + - `simply make Rectangle and Square two independent classes`. |
| 189 | + - Breaking LSP does not occur if you depend from interfaces: |
| 190 | + - they don’t provide any logic implementation as it’s deferred to the actual classes. |
| 191 | + |
| 192 | +```dart |
| 193 | +abstract class Shape { |
| 194 | + double computeArea(); |
| 195 | +} |
| 196 | +class Rectangle implements Shape {} |
| 197 | +class Square extends Shape {} |
| 198 | +``` |
| 199 | + |
| 200 | +--- |
| 201 | + |
| 202 | +## 4. Interface Segregation Principle (ISP) |
| 203 | + |
| 204 | +- A client doesn’t have to be forced to implement a behavior it doesn’t need. |
| 205 | +- What turns out from this is: |
| 206 | + - you should create small interfaces with minimal methods. |
| 207 | + - Generally `it’s better having 8 interfaces with 1 method instead of 1 interface with 8 methods`. |
| 208 | + |
| 209 | +### ❌ Wrong |
| 210 | + |
| 211 | +```dart |
| 212 | +// Interfaces |
| 213 | +abstract class Worker { |
| 214 | + void work(); |
| 215 | + void sleep(); |
| 216 | +} |
| 217 | +class Human implements Worker { |
| 218 | + void work() => print("I do a lot of work"); |
| 219 | + void sleep() => print("I need 10 hours per night..."); |
| 220 | +} |
| 221 | +class Robot implements Worker { |
| 222 | + void work() => print("I always work"); |
| 223 | + void sleep() {} // ?? |
| 224 | +} |
| 225 | +
|
| 226 | +``` |
| 227 | + |
| 228 | +### ✅ Right |
| 229 | + |
| 230 | +This is definitely better because |
| 231 | + |
| 232 | +- there are no useless methods |
| 233 | +- and we’re free to decide which behaviors should the classes implement. |
| 234 | + |
| 235 | +```dart |
| 236 | +// Interfaces |
| 237 | +abstract class Worker { |
| 238 | + void work(); |
| 239 | +} |
| 240 | +abstract class Sleeper { |
| 241 | + void sleep(); |
| 242 | +} |
| 243 | +class Human implements Worker, Sleeper { |
| 244 | + void work() => print("I do a lot of work"); |
| 245 | + void sleep() => print("I need 10 hours per night..."); |
| 246 | +} |
| 247 | +class Robot implements Worker { |
| 248 | + void work() => print("I always work"); |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +--- |
| 253 | + |
| 254 | +## 5. Dependency Inversion Principle (DIP) |
| 255 | + |
| 256 | +`very important` |
| 257 | + |
| 258 | +- DIP states that we should code against abstractions and not implementations. |
| 259 | + |
| 260 | + - ✅ Extending an abstract class is good |
| 261 | + - ✅ and implement an interface is good |
| 262 | + |
| 263 | +- ❌ but descending from a concrete classed with no abstract methods is bad. |
| 264 | + |
| 265 | +<img src=".\assets\dipQuote.png" align="center"/> |
| 266 | + |
| 267 | +<table align="center"> |
| 268 | + <tr> |
| 269 | + <td> ❌ Wrong </td> |
| 270 | + <td> ✅ Right </td> |
| 271 | + </tr> |
| 272 | + <tr> |
| 273 | + <td><img src = "assets/wrong_dip.png"></td> |
| 274 | + <td><img src = "assets/right_dip.png"></td> |
| 275 | + </tr> |
| 276 | + <tr> |
| 277 | + <td> |
| 278 | + <p>in the high level (notification class) I created 2 objects of the 2 classes hotMail and gmail</p> |
| 279 | + </td> |
| 280 | + <td> |
| 281 | + <p>hotMail and gmail Implemented the IMessage interface so then in the high level (Notification class) I declared an object from the IMessage Interface</p> |
| 282 | + </td> |
| 283 | + </tr> |
| 284 | +</table> |
| 285 | + |
| 286 | +<img align="center" src = "assets/dip_explanation.png"> |
| 287 | + |
| 288 | +client = dependency = the thing that we injected (gmail and hotMail) |
| 289 | + |
| 290 | +### ❌ Wrong |
| 291 | + |
| 292 | +```dart |
| 293 | +/// Low Level |
| 294 | +class HotMail { |
| 295 | + send() {} |
| 296 | +} |
| 297 | +
|
| 298 | +class Gmail { |
| 299 | + send() {} |
| 300 | +} |
| 301 | +
|
| 302 | +/// high level |
| 303 | +class Notification { |
| 304 | + HotMail hotMail = HotMail(); |
| 305 | + Gmail gmail = Gmail(); |
| 306 | +
|
| 307 | + void sendGmail() => gmail.send(); |
| 308 | + void sendHotMail() => hotMail.send(); |
| 309 | +} |
| 310 | +
|
| 311 | +/// in Main |
| 312 | +void main(List<String> args) { |
| 313 | + Notification notification = Notification(); |
| 314 | + notification.sendGmail(); |
| 315 | + notification.sendHotMail(); |
| 316 | +} |
| 317 | +
|
| 318 | +``` |
| 319 | + |
| 320 | +### ✅ Right |
| 321 | + |
| 322 | +```dart |
| 323 | +
|
| 324 | +/// Low Level |
| 325 | +abstract class BaseMail { |
| 326 | + send(); |
| 327 | +} |
| 328 | +
|
| 329 | +class HotMail implements BaseMail { |
| 330 | + @override |
| 331 | + send() {} |
| 332 | +} |
| 333 | +
|
| 334 | +class Gmail implements BaseMail { |
| 335 | + @override |
| 336 | + send() {} |
| 337 | +} |
| 338 | +
|
| 339 | +/// high level |
| 340 | +class Notification { |
| 341 | + BaseMail baseMail; |
| 342 | +
|
| 343 | + Notification(this.baseMail); |
| 344 | +
|
| 345 | + void send() => baseMail.send(); |
| 346 | +} |
| 347 | +
|
| 348 | +/// in Main |
| 349 | +void main(List<String> args) { |
| 350 | + Notification notification = Notification(Gmail()); |
| 351 | + notification.send(); |
| 352 | +} |
| 353 | +``` |
| 354 | + |
| 355 | +--- |
| 356 | + |
| 357 | +- Dependency injection (DI) is a very famous way to implement the DIP. |
| 358 | +- Depending on abstractions gives the freedom to be independent from the implementation. |
| 359 | +- Look at this example: |
| 360 | + |
| 361 | +```dart |
| 362 | +// Use this as interface |
| 363 | +abstract class EncryptionAlgorithm { |
| 364 | +String encrypt(); // <-- abstraction |
| 365 | +} |
| 366 | +class AlgoAES implements EncryptionAlgorithm {} |
| 367 | +class AlgoRSA implements EncryptionAlgorithm {} |
| 368 | +class AlgoSHA implements EncryptionAlgorithm {} |
| 369 | +
|
| 370 | +class FileManager { |
| 371 | + void secureFile(EncryptionAlgorithm algo) { |
| 372 | + algo.encrypt(); |
| 373 | + } |
| 374 | +} |
| 375 | +``` |
| 376 | + |
| 377 | +- The `FileManager class` knows nothing about how algo works, it’s just aware that the encrypt() => Method secures a file. |
| 378 | + |
| 379 | +- This is essential for maintenance because we can call the method as we want: |
| 380 | + |
| 381 | +```dart |
| 382 | +void main(){ |
| 383 | + final fm = FileManager(...); |
| 384 | + fm.secureFile(AlgoAES()); |
| 385 | + fm.secureFile(AlgoRSA()); |
| 386 | +} |
| 387 | +``` |
| 388 | + |
| 389 | +If we added another encryption algorithm, it would be automatically compatible with secureFile |
| 390 | +as it is a subtype of EncryptionAlgorithm. |
| 391 | + |
| 392 | +**In this example, Done the 5 SOLID principles all together.** |
0 commit comments