├── .gitignore ├── assets ├── Capture.png ├── image.PNG ├── umlsc.PNG ├── framework.PNG ├── CommaParser.PNG ├── IF_Refactor.PNG ├── OnlineCart.png ├── code_smell.png ├── delegation.png ├── inheritance.PNG ├── interception.PNG ├── modification.png ├── Account_class.PNG ├── CurrentAccount.png ├── First_Refactor.PNG ├── ParserHierachy.PNG ├── TradeProcessclass.PNG ├── decoraror_pattern.PNG ├── methods_refactor.PNG ├── strategypattern.PNG ├── Decorator_refactor.PNG ├── process_trades (2).png ├── decoraror_pattern_refactor.PNG ├── Refactored_TradeProcessClass.PNG └── Robert_C._Martin_surrounded_by_computers.jpg ├── authors.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | #Add any resource that you don't want pushed to the repository here 👇🏻: 2 | - 3 | - 4 | - 5 | - 6 | - 7 | -------------------------------------------------------------------------------- /assets/Capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/Capture.png -------------------------------------------------------------------------------- /assets/image.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/image.PNG -------------------------------------------------------------------------------- /assets/umlsc.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/umlsc.PNG -------------------------------------------------------------------------------- /assets/framework.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/framework.PNG -------------------------------------------------------------------------------- /assets/CommaParser.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/CommaParser.PNG -------------------------------------------------------------------------------- /assets/IF_Refactor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/IF_Refactor.PNG -------------------------------------------------------------------------------- /assets/OnlineCart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/OnlineCart.png -------------------------------------------------------------------------------- /assets/code_smell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/code_smell.png -------------------------------------------------------------------------------- /assets/delegation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/delegation.png -------------------------------------------------------------------------------- /assets/inheritance.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/inheritance.PNG -------------------------------------------------------------------------------- /assets/interception.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/interception.PNG -------------------------------------------------------------------------------- /assets/modification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/modification.png -------------------------------------------------------------------------------- /assets/Account_class.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/Account_class.PNG -------------------------------------------------------------------------------- /assets/CurrentAccount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/CurrentAccount.png -------------------------------------------------------------------------------- /assets/First_Refactor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/First_Refactor.PNG -------------------------------------------------------------------------------- /assets/ParserHierachy.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/ParserHierachy.PNG -------------------------------------------------------------------------------- /assets/TradeProcessclass.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/TradeProcessclass.PNG -------------------------------------------------------------------------------- /assets/decoraror_pattern.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/decoraror_pattern.PNG -------------------------------------------------------------------------------- /assets/methods_refactor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/methods_refactor.PNG -------------------------------------------------------------------------------- /assets/strategypattern.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/strategypattern.PNG -------------------------------------------------------------------------------- /assets/Decorator_refactor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/Decorator_refactor.PNG -------------------------------------------------------------------------------- /assets/process_trades (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/process_trades (2).png -------------------------------------------------------------------------------- /assets/decoraror_pattern_refactor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/decoraror_pattern_refactor.PNG -------------------------------------------------------------------------------- /assets/Refactored_TradeProcessClass.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/Refactored_TradeProcessClass.PNG -------------------------------------------------------------------------------- /assets/Robert_C._Martin_surrounded_by_computers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuxDevHQ/LuxDevHQ-Writing-Clean-Code-Python-Edition/HEAD/assets/Robert_C._Martin_surrounded_by_computers.jpg -------------------------------------------------------------------------------- /authors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "username": "Harun Mbaabu", 4 | "name": "Harun Mbaabu", 5 | "twitter": "Harun Mbaabu", 6 | "picture": "Not Available", 7 | "bio": "Not Available" 8 | }, 9 | { 10 | "username": "Vincent Kasozo", 11 | "name": "Vincent Kasozo", 12 | "twitter": "_Vincent Kasozo", 13 | "picture": "Not Available", 14 | "bio": "Software engineer." 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean Python Code 2 | 3 | ## Table of Contents 4 | 5 | ![Uncle Bob surrounded with computers](assets/Robert_C._Martin_surrounded_by_computers.jpg) 6 | 7 | 1. [Introduction](#introduction) 8 | 9 | 2. [Naming Things](#naming-Things) 10 | 11 | - [Use intention revealing name](#Use-intention-revealing-name) 12 | - [Meaningful Distinctions](#Meaningful-Distinctions) 13 | - [Avoid Disinformation](#Avoid-Disinformation) 14 | - [Pronounceable Names](#Pronounceable-names) 15 | - [Search-able Names](#Searchable-Names) 16 | - [Don't be cute](#Don't-be-cute) 17 | - [Avoid Encodings](#Avoid-Encodings) 18 | - [Hungarian Notation](#Hungarian-Notation) 19 | - [Member Prefixes](#Member-Prefixes) 20 | - [Interfaces & Implementations](#Interfaces-&-Implementations) 21 | - [Gratuitous Context](#Gratuitous-Context) 22 | - [Avoid Mental Mapping](#Avoid-Mental-Mapping) 23 | - [Class Names](#Class-Names) 24 | - [Types of Objects](#Types-of-Objects) 25 | - [Simple Superclass Name]() 26 | - [Qualified Subclass Name]() 27 | - [Method Names]() 28 | - [Pick One Word per Concept]() 29 | - [Don’t Pun]() 30 | - [Use Solution Domain Names]() 31 | - [Use Problem Domain Names]() 32 | - [Add Meaningful Context]() 33 | 34 | 3. [Functions](#functions) 35 | - [Small](#Small) 36 | - [Do One Thing](#Do-One-Thing) 37 | - [One level of Abstraction](#One-level-of-Abstraction) 38 | - [Avoid Conditionals](#Avoid-Conditionals) 39 | - [Use Descriptive Names](#Use-Descriptive-Names) 40 | - [Function Arguments](#Function-Arguments) 41 | - [Avoid Side Effects](#Avoid-Side-Effects) 42 | - [Pure Functions](#Pure-Functions) 43 | - [Niladic Functions](#Niladic-Functions) 44 | - [Argument Mutation](#Argument-Mutation) 45 | - [Exceptions](#Exceptions) 46 | - [I/O](#I/O) 47 | - [Command Query Separation](#Command-Query-Separation) 48 | - [Don't Repeat Yourself](#Don't-Repeat-Yourself) 49 | 4. [Objects and Data Structures](#objects-and-data-structures) 50 | 5. [Classes](#classes) 51 | 6. [SOLID Principles](#SOLID-Principles) 52 | - [Single Responsibility Principle](#Single-Responsibility-Principle) 53 | - [Open/Close Principle](#Open/Closed-Principle) 54 | - [Liskov Substitution Principle](#Liskov-Substitution) 55 | - [Interface Segregation Principle](#Interface-Segregation) 56 | - [Dependency Inversion Principle](#Dependency-Inversion-Principle) 57 | 7. [Testing](#testing) 58 | 8. [Concurrency](#concurrency) 59 | 9. [Error Handling](#error-handling) 60 | 10. [Formatting](#formatting) 61 | 11. [Comments](#comments) 62 | 12. [Translation](#translation) 63 | 64 | Software engineering principles, from Robert C. Martin's book 65 | [_Clean Code_](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882), 66 | adapted for Python. This is not a style guide. It's a guide to producing readable, reusable, and refactorable software in Python. 67 | 68 | ## Introduction 69 | 70 | --- 71 | 72 | ## Naming Things 73 | 74 | --- 75 | 76 | Modern software is so complex that no one can understand all parts of a non-trivial project alone. The only way humans tame details is through abstractions. With abstraction, we focus on the essential and forget about the non-essential at that particular time. You remember the way you learned body biology?? You focused on one system at a time, digestive, nervous, cardiovascular e.t.c and ignored the rest. That is abstraction at work. 77 | 78 | A variable name is an abstraction over memory, a function name is an abstraction over logic, a class name is an abstraction over a packet of data and the logic that operates on that data. 79 | 80 | The most fundamental abstraction in writing software is **naming**. Naming things is just one part of the story, using good names is a skill that unfortunately, is not owned by most programmers and that is why we have come up with so many refactorings concerned with naming things. 81 | 82 | Good names bring order to the chaotic environment of crafting software and hence, we better be good at this skill so that we can enjoy our craft. 83 | 84 | **[⬆ back to top](#table-of-contents)** 85 | 86 | ### **Use intention revealing names** 87 | 88 | --- 89 | 90 | This rule enforces that programmers should make their code read like well written prose by naming parts
91 | of their code perfectly. With such good naming, a programmer will never need to resort to comments or unnecessary
doc strings. 92 | Below is a code snippet from a software system. Would you make sense of it without any explanation? 93 | 94 | **Bad** :angry: 95 | 96 | ```python 97 | from typing import List 98 | 99 | def f(a : List[List[int]])->List[List[int]]: 100 | return [i for i in a if i[1] == 0] 101 | ``` 102 | 103 | It would be ashaming that someone would fail to understand such a simple function. What could have gone wrong?? 104 | 105 | The problem is simple.This code is littered with **mysterious names**. We have to agree that this is code and not a detective novel. Code should be clear and precise. 106 | 107 | What this code does is so trivial. It takes in a collection of orders and returns the pending orders. Let's pause for a moment and appreciate the extreme over engineering in this solution. 108 | The programmer assumes that each order is coded as a list of `ints` (`List[int]`) and that the second element is the order status. He decides that 0 means pending and 1 means cleared. 109 | 110 | Notice the first problem... that snippet doesn't contain knowledge about the domain. This is a design smell known as a **missing abstraction**. We are missing the Order abstraction. 111 | 112 | > **Missing Abstraction**
113 | > This smell arises when clumps of data are used instead creating a class or an interface 114 | 115 | We have a thorny problem right now, we lack meaningful domain abstractions. One of the ways of solving the missing abstraction smell is to **map domain entities**. So lets create an abstraction called Order. 116 | 117 | ```python 118 | from typing import List 119 | 120 | class Order: 121 | def __init__(self, order_id : int, order_status : int) -> None: 122 | self._order_id = order_id 123 | self._order_status = order_status 124 | 125 | def is_pending(self) -> bool: 126 | return self._order_status == 0: 127 | 128 | #more code goes here 129 | ``` 130 | 131 | > We could also have used the **namedtuple** in the python standard library but we won't be too functional that early. Let us stick with OOP for now. **namedtuples** contain only data and not data with code that acts on it. 132 | 133 | Let us now refactor our entity names and use our newly created abstraction too. We arrive at the following code snippet. 134 | 135 | **Better: :smiley:** 136 | 137 | ```python 138 | from typing import List 139 | 140 | Orders = List[Order] 141 | 142 | def get_pending_orders(orders : Orders)-> Orders: 143 | return [order for order in orders if order.is_pending()] 144 | ``` 145 | 146 | This function reads like well written prose. 147 | 148 | Notice that the `get_pending_orders()` function delegates the logic of finding the order status to the Order class. This is because the Order class knows its internal representation more than anyone else, so it better implement this logic. This is known as the **Most Qualified Rule** in OOP. 149 | 150 | > **Most Qualified Rule**
151 | > Work should be assigned to the class that knows best how to do it. 152 | 153 | > We are using the listcomp for a reason. Listcomps are examples of **iterative expressions**. They serve one role and that is creating lists. On the other hand, for-loops are **iterative commands** and thus accomplish a myriad of tasks. Pick the right tool for the job. 154 | 155 | Never allow client code know your implementation details. In fact the ACM A.M Laureate Babra Liskov says it soundly in her book [Program development in Java. Abstraction, Specification and OOD](https://book4you.org/book/1164544/93467d). The **Iterator design pattern** is one way of solving that problem. 156 | 157 | Here is another example of a misleading variable name. 158 | 159 | **Bad** :angry: 160 | 161 | ```python 162 | student_list= {'kasozi','vincent', 'bob'} 163 | ``` 164 | 165 | This variable name is so misleading. 166 | 167 | - It contains noise. why the list suffix? 168 | - It is lying to us. Lists are not the same as sets. They may all be collections but they are not the same at all. 169 | 170 | To prove that lists are not sets, below is a code snippet that returns the methods in the List class that aren't in the Set class. 171 | 172 | ```python 173 | sorted(set(dir(list())) - set(dir(set()))) 174 | ``` 175 | 176 | Once it has executed, `append()` is one of the returned functions implying that sets don't support `append()` but instead support `add()`. So you write the code below, your code breaks. 177 | 178 | > Sets are not sequences like lists. In fact, they are unordered collections and so adding the `append()` method to the set class would be misleading. `append()` means we are adding at the end which may not be the case with sets. 179 | 180 | **Bad** :angry: 181 | 182 | ```python 183 | student_list= {'kasozi','vincent', 'bob'} 184 | student_list.append('martin') #It breaks!! 185 | ``` 186 | 187 | It is better to use a different variable name is neutral to the data structure being used. 188 | In this case, once you decide to change data structure used, your variable won't destroy the semantics of your code. 189 | 190 | **Good** :smiley: 191 | 192 | ```python 193 | students = {'kasozi', 'vincent', 'bob'} 194 | ``` 195 | 196 | > You can not achieve good naming with a bad design. You can see that mapping domain entities into our code has made our codebase use natural names. 197 | 198 | **[⬆ back to top](#table-of-contents)** 199 | 200 | ## Meaningful-Distinctions 201 | 202 | **[⬆ back to top](#table-of-contents)** 203 | 204 | ## Avoid-Disinformation 205 | 206 | **[⬆ back to top](#table-of-contents)** 207 | 208 | ## Pronounceable-names 209 | 210 | --- 211 | 212 | When naming things in your code, it is much better to use names that are easy to pronounce by programmers. This enables developers discuss the code without the need to sound silly as they mention the names. If you a polyglot in natural langauges, it is much better to use the language common to most developers when naming your entities. 213 | 214 | **Bad** :angry: 215 | 216 | ```python 217 | from typing import List 218 | import math 219 | 220 | def sqrs(first_n : int)-> List[int]: 221 | if first_n > 0: 222 | return [int(math.pow(i, 2)) for i in xrange(first_n)] 223 | return [] 224 | 225 | lstsqrs = sqrs(5) 226 | ``` 227 | 228 | How can a human pronounce sqrs and lstsqrs? This is a serious problem. Let's correct it. 229 | 230 | **Good** :smiley: 231 | 232 | ```python 233 | from typing import List 234 | import math 235 | 236 | def generate_squares(first_n : int)-> List[int]: 237 | if first_n > 0: 238 | return [int(math.pow(i, 2)) for i in xrange(first_n)] 239 | return [] 240 | 241 | squares = generate_squares(5) 242 | ``` 243 | 244 | **[⬆ back to top](#table-of-contents)** 245 | 246 | ### Searchable-Names 247 | 248 | For example, you`re looking for some part of code where you calculate something and remember that was about work days in week 249 | 250 | **Bad** :angry: 251 | 252 | ```python 253 | for i in range(0,34): 254 | s += (t[i]*4)/5 255 | ``` 256 | 257 | 258 | What is easier to find ```5``` or ```WORK_DAYS_PER_WEEK```? 259 | 260 | 261 | It`s normally to name local variable as one char in short functions but if you can avoid it 262 | 263 | **Good** :smiley: 264 | 265 | ```python 266 | from typing import Final 267 | 268 | 269 | real_days_per_ideal_day: int = 4 270 | WORK_DAYS_PER_WEEK: Final[int] = 5 271 | sum: int = 0 272 | for i in range(0,number_of_tasks): 273 | real_task_days: int = task_estimate[i] * real_days_per_ideal_day 274 | real_task_weeks: int = real_task_days / WORK_DAYS_PER_WEEK 275 | sum: int += real_task_weeks 276 | ``` 277 | 278 | **[⬆ back to top](#table-of-contents)** 279 | 280 | ### Don't-be-cute 281 | 282 | **[⬆ back to top](#table-of-contents)** 283 | 284 | ### Avoid-Encodings 285 | 286 | **[⬆ back to top](#table-of-contents)** 287 | 288 | #### Hungarian-Notation 289 | 290 | #### Member-Prefixes 291 | 292 | #### Interfaces-&-Implementations 293 | 294 | ### Gratuitous-Context 295 | 296 | **[⬆ back to top](#table-of-contents)** 297 | 298 | ### Avoid-Mental-Mapping 299 | 300 | **[⬆ back to top](#table-of-contents)** 301 | 302 | ## **Functions** 303 | 304 | ### **Small** 305 | 306 | **[⬆ back to top](#table-of-contents)** 307 | 308 | ### Do-One-Thing 309 | 310 | **[⬆ back to top](#table-of-contents)** 311 | 312 | ### One-level-of-Abstraction 313 | 314 | **[⬆ back to top](#table-of-contents)** 315 | 316 | ### Avoid Conditionals 317 | 318 | --- 319 | 320 | Let us meet Joe. Joe is a junior web developer who works at a certain company in Nairobi. Joe's company has got a new client who wants Joe's company to build him an application to manage his bank. 321 | 322 | The client specifies that this application will manage user bank accounts. Joe organizes a meeting with the client and they agree to meet so that Joe can collect the client's business needs. Let us watch Joe as he puts his OOP programming skills to work. 323 | 324 | After their meeting, they agree that the user account will be able to accomplish the behaviour specified in the figure below. 325 | 326 | ![Account class](assets/Account_class.PNG) 327 | 328 | The code below provides the implementation details of this class. 329 | 330 | ```python 331 | class Account: 332 | def __init__(self, acc_number: str, amount: float, name: str): 333 | self.acc_number = acc_number 334 | self.amount = amount 335 | self.name = name 336 | 337 | def get_balance(self) -> float: 338 | return self.amount 339 | 340 | def __eq__(self, other: Account) -> bool: 341 | if isinstance(other, Account): 342 | return self.acc_number == other.acc_number 343 | 344 | def deposit(self, amount: float) -> bool: 345 | if amount > 0: 346 | self.amount += amount 347 | 348 | def withdraw(self, amount: float) -> None: 349 | if (amount > 0) and (amount <= self.amount): 350 | self.amount -= amount 351 | 352 | def _has_enough_collateral(self, loan: float) -> bool: 353 | if loan < self.amount / 2: 354 | return True 355 | 356 | def __str__(self) -> str: 357 | return f'Account acc number : {self.acc_number} amount : {self.amount}' 358 | 359 | def add_interest(self) -> None: 360 | self.deposit(0.1 * self.amount) 361 | 362 | def get_loan(self, amount : float) -> bool: 363 | if self._has_enough_collateral(amount): 364 | return True 365 | else: 366 | return False 367 | ``` 368 | 369 | The application is a success and after a month, the client comes back to Joe asking for more features. The client says that he now wants the application to work with more than one type of account. The application should now process SavingsAccount and CheckingAccount accounts. The difference between them is outlined below. 370 | 371 | - When authorizing a loan, a checking account needs a 372 | balance of two thirds the loan amount, whereas savings accounts require only one half the loan amount. 373 | 374 | - The bank gives periodic interest to savings accounts but not checking accounts. 375 | 376 | - The representation of an account will return 377 | “Savings Account” or “Checking Account,” as appropriate. 378 | 379 | Joe rolls up his sleeves and starts to make modifications to the original Account class to introduce the new features. Below is his approach. 380 | 381 | **Bad** :angry: 382 | 383 | ```python 384 | class Account: 385 | def __init__(self, acc_number: str, amount: float, name: str, type : int): 386 | self.acc_number = acc_number 387 | self.amount = amount 388 | self.name = name 389 | self.type = type 390 | 391 | def _has_enough_collateral(self, loan: float) -> bool: 392 | if self.type == 1: 393 | return self.amount >= loan / 2; 394 | elif selt.type == 2: 395 | return self.amount >= 2 * loan / 3; 396 | else: 397 | return False 398 | 399 | def __str__(self) -> str: 400 | if self.type == 1: 401 | return ' SavingsAccount' 402 | elif self.type == 2: 403 | return 'CheckingAccount' 404 | else: 405 | return 'InvalidAccount' 406 | 407 | def add_interest(self) -> None: 408 | if self.type == 1: self.deposit(0.1 * self.amount) 409 | 410 | 411 | def get_loan(self, amount : float) -> bool: 412 | True if self._has_enough_collateral(amount) else False 413 | 414 | #... other methods 415 | ``` 416 | 417 | > **Note:** We have only shown the methods that changed. 418 | 419 | With this implementation, Joe is happy and he ships the app into production since it works as the client had wanted. But something has really gone wrong here. 420 | 421 | ![conditionals](assets/code_smell.png) 422 | 423 | The problem are these conditionals here. They work for now but they will cause a maintenance nightmare very soon. What will happen if the client comes back asking Joe to add more account types? Joe will have to open this class and add more IFs. What happens of the client asks him to delete some of the account types? He will open the same class and edit all Ifs again. 424 | 425 | This class is now violating the **Single Responsibility Principle** and the **Open Closed Principle**. The class has more than one reason to change and still, it is not closed for modification and 426 | these IFs may also run slow. 427 | 428 | This smell is called the **Missing Hierarchy** smell. 429 | 430 | > **Missing Hierarchy**
431 | > This smell arises when a code segment uses conditional logic (typically in conjunction 432 | > with “tagged types”) to explicitly manage variation in behavior where a hierarchy 433 | > could have been created and used to encapsulate those variations. 434 | 435 | To solve this problem, we will need to introduce an hierarchy of account types. 436 | We will achieve this by creating a super abstract class Account and implement all the common methods but mark the account specific methods abstract. 437 | Different account types can then inherit from this base class. 438 | 439 | ![IF_refactor](assets/IF_Refactor.PNG) 440 | 441 | With this new approach, account specific methods will be implemented by subclasses and note that we will throw away those annoying IFs and replace them with polymorphism hence the **Replace Conditionals with Polymorphism** rule. 442 | 443 | Below are the implementation of Account, SavingsAccount and CheckingAccount. 444 | 445 | ![abstract methods](assets/Capture.png) 446 | 447 | **SavingsAccount class** 448 | 449 | **Good** :smiley: 450 | 451 | ```python 452 | class SavingAccount(Account): 453 | def __init__(self, acc_number: str, amount: float, name: str): 454 | Account.__init__(self, acc_number, amount, name) 455 | 456 | def __eq__(self, other: SavingsAccount) -> bool: 457 | if isinstance(other, SavingsAccount): 458 | return self.acc_number == other.acc_number 459 | 460 | 461 | def _has_enough_collateral(self, loan: float) -> bool: 462 | return self.amount >= loan / 2; 463 | 464 | def get_loan(self, amount : float) -> bool: 465 | return _has_enough_collateral(float) 466 | 467 | def __str__(self) -> str: 468 | return f'Saving Account acc number : {self.acc_number}' 469 | 470 | def add_interest(self) -> None: 471 | self.deposit(0.1 * self.amount) 472 | ``` 473 | 474 | **CheckingAccount class** 475 | 476 | **Good** :smiley: 477 | 478 | ```python 479 | class CheckingAccount(Account): 480 | def __init__(self, acc_number: str, amount: float, name: str): 481 | Account.__init__(self, acc_number, amount, name) 482 | 483 | def __eq__(self, other: SavingsAccount) -> bool: 484 | if isinstance(other, CheckingAccount): 485 | return self.acc_number == other.acc_number 486 | 487 | 488 | def has_enough_collateral(self, loan: float) -> bool: 489 | return self.amount >= 2 * loan / 3; 490 | 491 | def get_loan(self, amount : float) -> bool: 492 | return _has_enough_collateral(float) 493 | 494 | def __str__(self) -> str: 495 | return f'Checking Account acc number : {self.acc_number}' 496 | 497 | #empty method. 498 | def add_interest(self) -> None: 499 | pass 500 | ``` 501 | 502 | Notice that each branch of the original annoying `if else` is now implemented in its class. Now if the client comes back and asks Joe to add a fixed deposit account, Joe will just create a new class called FixedDeposit and it will inherit from that abstract Account class. With this design, note that : 503 | 504 | - To add new functionality, we add more classes and ignore all existing classes. This is the **Open Closed Principle.** 505 | 506 | Note that the CheckingAccount class leaves the add_interest method empty. This is a code smell known as the **Rebellious Hierarchy** design smell and we shall fix it later when we get to the **Interface Segregation Principle**. 507 | 508 | > **REBELLIOUS HIERARCHY**
509 | > This smell arises when a subtype rejects the methods provided by its supertype(s). 510 | > In this smell, a supertype and its subtypes conceptually share an IS-A relationship, 511 | > but some methods defined in subtypes violate this relationship. For example, for 512 | > a method defined by a supertype, its overridden method in the subtype could: 513 | > 514 | > - throw an exception rejecting any calls to the method 515 | > - provide an empty (or NOP i.e., NO Operation) method 516 | > - provide a method definition that just prints “should not implement” message 517 | > - return an error value to the caller indicating that the method is unsupported. 518 | 519 | After a year, Joe's client comes back and asks Joe to add a Current account. Guess what Joe does?? You guessed right, he just creates a new class for this new account and inherits from Account class as shown in the figure below. 520 | ![CurrentAccount](assets/CurrentAccount.png) 521 | 522 | **[⬆ back to top](#table-of-contents)** 523 | 524 | ### Use-Descriptive-Names 525 | 526 | **[⬆ back to top](#table-of-contents)** 527 | 528 | ### Function-Arguments 529 | 530 | **[⬆ back to top](#table-of-contents)** 531 | 532 | ### Avoid-Side-Effects 533 | 534 | #### **Pure Functions** 535 | 536 | --- 537 | 538 | What on earth is a pure function?? Well, adequately put, a pure function is one without side effects. 539 | Side effects are invisible inputs and outputs from functions. In pure Functional programming,functions behave like mathematical functions. Mathematical functions are transparent-- they will always return the same output when given the same input. Their output only depends on their inputs. 540 | 541 | Below are examples of functions with side effects: 542 | 543 | #### 1. **Niladic-Functions** 544 | 545 | --- 546 | 547 | **Bad** :angry: 548 | 549 | ```python 550 | class Customer: 551 | def __init__(self, first_name : str)-> None: 552 | self.first_name = first_name 553 | 554 | #This method is impure, it depends on global state. 555 | def get_name(self): 556 | return self.first_name 557 | 558 | #more code here 559 | ``` 560 | 561 | Niladic functions have this tendency to depend on some invisible input especially if such a function is member function of a class. Since all class members share the same class variables, most methods aren't pure at all. Class variable values will always depend on which method was called last. In a nutshell, most niladic functions depend on some **global state** in this case `self.first_name`. 562 | 563 | The same can be said to functions that return None. These too aren't pure functions. If a function doesn't return, then it is doing something that is affecting global state. **Such functions can not be composed in fluent APIs.** The sort method of the list class has side effects, it changes the list in place whereas the sorted builtin function has not side effects because it returns a new list. 564 | 565 | > `sort()` and `reverse()` are now discouraged and instead using the built-in `reversed()` and `sorted()` are encouraged. 566 | 567 | **Bad** :angry: 568 | 569 | ```python 570 | names = ['Kasozi', 'Martin', 'Newton', 'Grady'] 571 | 572 | #wrong: sorted_names now contains None 573 | sorted_names = names.sort() 574 | 575 | #correct: sorted_names now contain the sorted list 576 | sorted_names = sorted(names) 577 | ``` 578 | 579 | > **static methods**
580 | > One way to solve this problem is to use static methods inside a class. Static methods know nothing about the class data and hence their outputs only depend on their inputs. 581 | 582 | #### 2. **Argument Mutation** 583 | 584 | --- 585 | 586 | Functions that mutate their input arguments aren't pure functions. This becomes more pronounced when we run on multiple cores. More than one function may be reading from the same variable and each function can be context switched from the CPU at any time. If it was not yet done with editing the variable, others will read garbage. 587 | 588 | **Bad** :angry: 589 | 590 | ```python 591 | from typing import List 592 | 593 | Marks = List[int] 594 | 595 | marks = [43, 78, 56, 90, 23] 596 | 597 | def sort_marks(marks : Marks) -> None: 598 | marks.sort() 599 | 600 | def calculate_average(marks : Marks) -> float: 601 | return sum(marks)/float(len(marks)) 602 | ``` 603 | 604 | From the above code snippet, we have two functions that both read the same list. `sort_marks()` mutates its input argument and this is not good. Now imagine a scenario when `calculate_average_mark()` was running and before it completed, it was context switched and `sort_marks()` allowed to run. 605 | 606 | sort_marks will update the list in place and change the order of elements in the list, by the time `calculate_average_average()` will run again, it will be reading garbage. 607 | 608 | **Good :smiley:** 609 | 610 | ```python 611 | from typing import List 612 | 613 | Marks = List[int] 614 | 615 | marks = [43, 78, 56, 90, 23] 616 | 617 | #sort_marks now returns a new list and uses the sorted function 618 | 619 | #Mutates input argument 620 | def sort_marks(marks : Marks) -> Marks: 621 | return sorted(marks) 622 | 623 | # Doesn't mutate input argument 624 | def find_average_mark(marks : Marks) -> float: 625 | return sum(marks)/len(marks) 626 | ``` 627 | 628 | This problem can also be solved by using immutable data structures. 629 | 630 | > Function purity is also vital for unit-testing. Impure functions are hard to test especially if the side effect has to do with I/O. Unlike mutation, you can’t avoid side effects related to I/O; whereas mutation is an implementation detail, I/O is usually a requirement. 631 | 632 | #### 3. **Exceptions** 633 | 634 | --- 635 | 636 | Some function signatures are more expressive than others, by which I mean that they give us 637 | more information about what the function is doing, what inputs are permissible, and what outputs we can expect. The signature `() → ()`, for example, gives us no information at all: it may print some text, increment a counter, launch a spaceship... who knows! On the other hand, consider this signature: 638 | 639 | `(List[int], (int → bool)) → List[int]` 640 | 641 | Take a minute and see if you can guess what a function with this signature does. Of course, you 642 | can’t really know for sure without seeing the actual implementation, but you can make an 643 | educated guess. The function returns a list of `ints` as input; it also takes a list of `ints`, as well as a 644 | second argument, which is a function from int to `bool`: a predicate on int. 645 | 646 | But is not honest enough. What happens if we pass in an empty list?? This function may throw an exception. 647 | 648 | > Exceptions are hidden outputs from functions and functions that use exceptions have side effects. 649 | 650 | **Bad** :angry: 651 | 652 | ```python 653 | def find_quotient(first : int, second : int)-> float: 654 | try: 655 | return first/second 656 | except ZeroDivisionError: 657 | return None 658 | ``` 659 | 660 | What is wrong with such a function? In its signature, it claims to return a float but we can see that sometimes it fails. Such a function is not honest and such functions should be avoided. 661 | 662 | > Functiona languages handle errors using other means like Monads and Options. Not with exceptions. 663 | 664 | #### 4. **I/O** 665 | 666 | --- 667 | 668 | Functions that perform input/output aren't pure too. Why? This is because they return different outputs when given the same input argument. Let me explain more about this. Imagine a function that takes in an URL and returns HTML, if the HTML is changed, the function will return a different output but it is still taking in the same URL. Remember mathematical functions don't behave like this. 669 | 670 | **Bad** :angry: 671 | 672 | ```python 673 | def read_HTML(url : str)-> str: 674 | try: 675 | with open(url) as file: 676 | data = file.read() 677 | data = file.read() 678 | return data 679 | except FileNotFoundError: 680 | print('File Not found') 681 | ``` 682 | 683 | This function is plagued with more than one problem. 684 | 685 | - Its signature is not honest. It claims that the function returns a string and takes in a string but from the implementation, we see it can fail. 686 | - This function is performing IO. IO operations produce side effects and thus this function is not pure. 687 | 688 | > You can build pure functions in python with the help of the **operator** and **functools** modules. There is a package **fn.py** to support functional programming in Python 2 and 3. According 689 | > to its author, Alexey Kachayev, fn.py provides “implementation of missing features to 690 | > enjoy FP” in Python. It includes a @recur.tco decorator that implements tail-call optimization 691 | > for unlimited recursion in Python, among many other functions, data structures, 692 | > and recipes. 693 | 694 | ### Command-Query-Separation 695 | 696 | --- 697 | 698 | ### Don't Repeat Yourself (DRY) 699 | 700 | --- 701 | 702 | Let us imagine that we are working on a banking application. We all know that such an application will manipulate bank account objects among other things. 703 | Let us assume that at the start of the project, we have only two types of accounts to work with; 704 | 705 | - Savings Account 706 | - Checking Account 707 | 708 | We roll up our sleeves and put our OOP knowledge to test. We craft two classes to model both and Savings and Checking accounts. 709 | 710 | **SavingsAccount class** 711 | 712 | **Bad** :angry: 713 | 714 | ```python 715 | class SavingsAccount: 716 | def __init__(self, acc_number: str, amount: float, name: str): 717 | self.acc_number = acc_number 718 | self.amount = amount 719 | self.name = name 720 | 721 | def get_balance(self) -> float: 722 | return self.amount 723 | 724 | def __eq__(self, other: SavingsAccount) -> bool: 725 | if isinstance(other, SavingsAccount): 726 | return self.acc_number == other.acc_number 727 | 728 | def deposit(self, amount: float) -> bool: 729 | if amount > 0: 730 | self.amount += amount 731 | 732 | def withdraw(self, amount: float) -> None: 733 | if (amount > 0) and (amount <= self.amount): 734 | self.amount -= amount 735 | 736 | def has_enough_collateral(self, loan: float) -> bool: 737 | if loan < self.amount / 2: 738 | return True 739 | 740 | def __str__(self) -> str: 741 | return f'Saving Account acc number : {self.acc_number}' 742 | 743 | def add_interest(self) -> None: 744 | self.deposit(0.1 * self.amount) 745 | ``` 746 | 747 | **CheckingAccount class** 748 | 749 | **Bad** :angry: 750 | 751 | ```python 752 | class CheckingAccount: 753 | def __init__(self, acc_number: str, amount: float, name: str): 754 | self.acc_number = acc_number 755 | self.amount = amount 756 | self.name = name 757 | 758 | def get_balance(self) -> float: 759 | return self.amount 760 | 761 | def __eq__(self, other: SavingsAccount) -> bool: 762 | if isinstance(other, SavingsAccount): 763 | return self.acc_number == other.acc_number 764 | 765 | def deposit(self, amount: float) -> bool: 766 | if amount > 0: 767 | self.amount += amount 768 | 769 | def withdraw(self, amount: float) -> None: 770 | if (amount > 0) and (amount <= self.amount): 771 | self.amount -= amount 772 | 773 | def has_enough_collateral(self, loan: float) -> bool: 774 | if loan < self.amount / 5: 775 | return True 776 | 777 | def __str__(self) -> str: 778 | return f'Checking Account acc number : {self.acc_number}' 779 | 780 | def add_interest(self) -> None: 781 | self.deposit(0.5 * self.amount) 782 | ``` 783 | 784 | The table describes all the methods added to both classes. 785 | 786 | | method | description | 787 | | ------------------------- | --------------------------------------------------- | 788 | | `get_balance()` | returns the account balance | 789 | | `__str__()` | returns the string representation of account object | 790 | | `add_interest()` | adds a given interest to a given account | 791 | | `has_enough_collateral()` | checks if the account can be granted a loan | 792 | | `withdraw()` | withdraws a given amount from the account | 793 | | `deposit()` | deposits an amount to the account | 794 | | `__eq__()` | checks if 2 accounts are the same | 795 | 796 | The Unified Modeling Language (UML) class diagrams of both classes are shown below. Notice the duplication in method names. 797 | 798 | ![](assets/umlsc.PNG) 799 | 800 | If you look more closely, both these classes contain the same methods and to make it worse, most of these methods contain exactly the same code. This is a **bad** practice and it leads to a maintenance nightmare. Identical code is littered in more than one place and so if we ever make changes to one of the copies, we have to change all the others. 801 | 802 | There is a software principle that helps in solving such a problem and this principle is known as **DRY** for Don't Repeat Yourself. 803 | 804 | > The “Don’t Repeat Yourself” Rule
805 | > A piece of code should exist in exactly one place. 806 | 807 | It is evident from our bad design that we have two classes that both claim to do same thing really well and so we just violated the **Most Qualified Rule**. In most cases, such scenarios arise due to failing to identify similarities between objects in a system. 808 | 809 | To solve this problem, we will use inheritance. We will define a new abstract class called BankAccount and we will implement all the method containing the similar logic in this abstract class. Then we will leave the different methods to be implemented by subclasses of BankAccount. 810 | 811 | Below is the UML diagram for our new design. 812 | 813 | ![Inheritance Hierachy of SavingsAccount and CheckingAccount](assets/inheritance.PNG) 814 | 815 | **BankAccount** class 816 | 817 | **Good** :smiley: 818 | 819 | ```python 820 | from abc import ABC, abstractmethod 821 | 822 | class BankAccount(ABC): 823 | def __init__(self, acc_number: str, amount: float, name: str): 824 | self.acc_number = acc_number 825 | self.amount = amount 826 | self.name = name 827 | 828 | def get_balance(self) -> float: 829 | return self.amount 830 | 831 | def deposit(self, amount: float) -> bool: 832 | if amount > 0: 833 | self.amount += amount 834 | 835 | def withdraw(self, amount: float) -> None: 836 | if (amount > 0) and (amount <= self.amount): 837 | self.amount -= amount 838 | 839 | @abstractmethod 840 | def __eq__(self, other: SavingsAccount) -> bool: 841 | pass 842 | 843 | @abstractmethod 844 | def has_enough_collateral(self, loan: float) -> bool: 845 | pass 846 | 847 | @abstractmethod 848 | def __str__(self) -> str: 849 | pass 850 | 851 | @abstractmethod 852 | def add_interest(self) -> None: 853 | pass 854 | ``` 855 | 856 | > **Note :** In the BankAccount abstract class, the methods `__eq__()`, `has_enough_collateral()`, `__str__()` and `add_interest()` are abstract and so it is the responsible of subclasses to implement them. 857 | 858 | **SavingsAccount class** 859 | 860 | **Good** :smiley: 861 | 862 | ```python 863 | class SavingAccount(BankAccount): 864 | def __init__(self, acc_number: str, amount: float, name: str): 865 | BankAccount.__init__(self, acc_number, amount, name) 866 | 867 | def __eq__(self, other: SavingsAccount) -> bool: 868 | if isinstance(other, SavingsAccount): 869 | return self.acc_number == other.acc_number 870 | 871 | 872 | def has_enough_collateral(self, loan: float) -> bool: 873 | if loan < self.amount / 2: 874 | return True 875 | 876 | 877 | def __str__(self) -> str: 878 | return f'Saving Account acc number : {self.acc_number}' 879 | 880 | 881 | def add_interest(self) -> None: 882 | self.deposit(0.1 * self.amount) 883 | ``` 884 | 885 | **CheckingAccount class** 886 | 887 | **Good** :smiley: 888 | 889 | ```python 890 | class CheckingAccount(BankAccount): 891 | def __init__(self, acc_number: str, amount: float, name: str): 892 | BankAccount.__init__(self, acc_number, amount, name) 893 | 894 | def __eq__(self, other: SavingsAccount) -> bool: 895 | if isinstance(other, CheckingAccount): 896 | return self.acc_number == other.acc_number 897 | 898 | 899 | def has_enough_collateral(self, loan: float) -> bool: 900 | if loan < self.amount / 5: 901 | return True 902 | 903 | 904 | def __str__(self) -> str: 905 | return f'Checking Account acc number : {self.acc_number}' 906 | 907 | 908 | def add_interest(self) -> None: 909 | self.deposit(0.5 * self.amount) 910 | ``` 911 | 912 | With this new design, if we ever want to modify the methods common to both classes, we only edit them in the abstract class. This simplifies our codebase maintenance. In fact, this was of organizing code is so ideal for implementing the **Replace Ifs with Polymorphism (RIP)** principle as we shall see later. 913 | 914 | ## **SOLID Principles** 915 | 916 | --- 917 | 918 | ### Single Responsibility Principle 919 | 920 | --- 921 | 922 | The single responsibility principle (SRP) instructs developers to write code that has one and only one 923 | reason to change. If a class has more than one reason to change, it has more than one responsibility. 924 | Classes with more than a single responsibility should be broken down into smaller classes, each of 925 | which should have only one responsibility and reason to change. 926 | 927 | It is difficult to overstate the importance of delegating to abstractions. It is the lynchpin of adaptive 928 | code and, without it, developers would struggle to adapt to changing requirements in the way 929 | that Scrum and other Agile processes demand. 930 | 931 | Let us meet Vincent. Vincent is a developer and he loves his job really a lot. Vincent loves to keep learning and he buys books that talk about software but he is always busy that he fails to read them. Vincent has a new client that wants an application developed for him. 932 | 933 | **_The client wants a program that reads trade records from a file, parse them, log any errors, process the records and them save them to a database._** 934 | 935 | The data is stored in the following format. The first 3 capitals are the source currency code, the next 3 capitals are the destination currency code. The first integer is the lot and the last float is the price. 936 | 937 | ```markdown 938 | UGAUSD,2,45.3 939 | UGAUSD,7,76.4 940 | UGAEUR,7,76.4 941 | HJDSGS,1,76.3 942 | ygfuhf,tj,89 943 | ``` 944 | 945 | With these requirements, Vincent works out a first prototype of this application and tests to see if it works as the client wanted. Below is the class code. 946 | 947 | ![TradeProcessor](assets/TradeProcessclass.PNG) 948 | 949 | ```python 950 | from typing import List 951 | from sqlalchemy import create_engine, Column, Integer, String, Float 952 | from sqlalchemy.orm import sessionmaker 953 | from base import Base 954 | 955 | class TradeProcessor(object): 956 | @staticmethod 957 | def process_trades(filename): 958 | lines: List[str] = [] 959 | with open(filename) as ft: 960 | for line in ft: lines.append(line) 961 | trades: List[TradeRecord] = [] 962 | 963 | for index, line in enumerate(lines): 964 | fields = line.split(',') 965 | if len(fields) != 3: 966 | print(f'Line {index} malformed. Only {len(fields)} field(s) found.') 967 | continue 968 | if len(fields[0]) != 6: 969 | print(f'Trade currencies on line {index} malformed: "{fields[0]}"') 970 | continue 971 | trade_amount = 0 972 | try: 973 | trade_amount = float(fields[1]) 974 | except ValueError: 975 | print(f"WARN: Trade amount on line {index} not a valid integer: '{fields[1]}'") 976 | 977 | trade_price = 0 978 | try: 979 | trade_price = float(fields[2]) 980 | except ValueError: 981 | print(f"WARN: Trade price on line {index} not a valid decimal:'{fields[2]}'") 982 | 983 | print(trade_amount) 984 | sourceCurrencyCode = fields[0][:3] 985 | destinationCurrencyCode = fields[0][3:] 986 | trade = TradeRecord(source=sourceCurrencyCode, dest=destinationCurrencyCode, 987 | lots=trade_amount, amount=trade_price) 988 | trades.append(trade) 989 | 990 | engine = create_engine('postgresql://postgres:u2402/598@localhost:5432/python') 991 | Session = sessionmaker(bind=engine) 992 | Base.metadata.create_all(engine) 993 | session = Session() 994 | for trade in trades: 995 | session.add(trade) 996 | session.commit() 997 | session.close() 998 | ``` 999 | 1000 | > **Note :** In this example we used the SqlAlchemy ORM for persistence but we could have used any DB APIs out there. 1001 | 1002 | Below is the code for the TradeRecord class that SqlAlchemy uses to persist our data. 1003 | 1004 | ```python 1005 | class TradeRecord(Base): 1006 | __tablename__ = 'TradeRecord' 1007 | id = Column(Integer, primary_key=True) 1008 | source_curreny = Column(String) 1009 | dest_currency = Column(String) 1010 | lots = Column(Integer) 1011 | amount = Column(Float) 1012 | 1013 | def __init__(self, source, dest, lots, amount): 1014 | self.source_curreny = source 1015 | self.dest_currency = dest 1016 | self.lots = lots 1017 | self.amount = amount 1018 | ``` 1019 | 1020 | If you look closely at the TradeProcessor class, it is the best example of a class that has a ton of responsibilities to change. The method `process_trades` is a hidden class within itself. It is doing more than one thing as listed below. 1021 | 1022 | 1. It reads every line from a File object, storing each line in a list of strings. 1023 | 2. It parses out individual fields from each line and stores them in a more structured list of 1024 | Trade­Record instances. 1025 | 3. The parsing includes some validation and some logging to the console. 1026 | 4. Each TradeRecord is then stored to a database. 1027 | 1028 | We can see that the responsibilities of the TradeProcessor are : 1029 | 1030 | 1. Reading files 1031 | 2. Parsing strings 1032 | 3. Validating string fields 1033 | 4. Logging 1034 | 5. Database insertion. 1035 | 1036 | The single responsibility principle states that this class, like all others, 1037 | should only have a single reason to change. However, the reality of the TradeProcessor is that it will 1038 | change under the following circumstances: 1039 | 1040 | - When the client decides not to use a file for input but instead read the trades from a remote call to a web service. 1041 | - When the format of the input data changes, perhaps with the addition of an extra field indicating the broker for the transaction. 1042 | - When the validation rules of the input data change. 1043 | - When the way in which you log warnings, errors, and information changes. If you are using a 1044 | hosted web service, writing to the console would not be a viable option. 1045 | - When the database changes in some way for example the client decides not to store the data in a relational database and opt for document storage, or the database is moved behind a web service that 1046 | you must call. 1047 | 1048 | For each of these changes, this class would have to be modified. Furthermore, unless you maintain 1049 | a variety of versions, there is no possibility of adapting the TradeProcessor so that it is able to read 1050 | from a different input source, for example. Imagine the maintenance headache when you are asked to 1051 | add the ability to store the trades in a web service!!! 1052 | 1053 | ### **Refactoring towards the SRP** 1054 | 1055 | --- 1056 | 1057 | We are going to achieve this in two steps. 1058 | 1059 | 1. Refactor for Clarity 1060 | 2. Refactor for Adaptability 1061 | 1062 | ### **Refactor for clarity** 1063 | 1064 | --- 1065 | 1066 | The first thing we are going to do is to break down the monstrous `process_trades()` method into smaller more specialized methods that do only one thing. Here we go. 1067 | If you look closely, the `process_trades()` method is doing 3 things: 1068 | 1069 | 1. Reading data from the file. 1070 | 2. Parsing and Logging and 1071 | 3. Storing to the data. 1072 | 1073 | ![process_trades_refactor]() 1074 | 1075 | So we can see from a very high level refactor it to something like below 1076 | 1077 | ```python 1078 | @staticmethod 1079 | def process_trades(filename): 1080 | lines: List[str] = TradeProcessor.__read_trade_data(filename) 1081 | trades: List[TradeRecord] = TradeProcessor.__parse_trades(lines) 1082 | TradeProcessor.__store_trades(trades) 1083 | ``` 1084 | 1085 | > Notice how these 4 smaller methods are easier to test than the original monolith!! 1086 | 1087 | ![TradeProcessor_class refactor](assets/methods_refactor.PNG) 1088 | Now let us look into the implementations of these new more focused methods. 1089 | 1090 | #### read_trade_data() 1091 | 1092 | --- 1093 | 1094 | ```python 1095 | @staticmethod 1096 | def __read_trade_data(filename: str) -> List[str]: 1097 | lines: List[str] 1098 | lines = [line for line in open(filename)] 1099 | return lines 1100 | ``` 1101 | 1102 | This method takes in the name of the file to read, it uses a list comprehension to enumerate over the read lines and returns a list of strings. Really simple!!!. 1103 | 1104 | #### parse_trades 1105 | 1106 | --- 1107 | 1108 | ```python 1109 | @staticmethod 1110 | def __parse_trades(trade_data: List[str]) -> List[TradeRecord]: 1111 | trades: List[TradeRecord] = [] 1112 | for index, line in enumerate(trade_data): 1113 | fields: List[str] = line.split(',') 1114 | if not TradeProcessor.__validate_trade_data(fields, index + 1): 1115 | continue 1116 | trade = TradeProcessor.__create_trade_record(fields) 1117 | trades.append(trade) 1118 | return trades 1119 | ``` 1120 | 1121 | This method takes in a list of strings produced by the `read_trade_data()` methods and tries to parse according to a given structure. **methods should do only one thing** and hence the `parse_trades()` method delegates to two other methods to accomplish its task. 1122 | 1123 | 1. The `validate_trade_data()` method. This is responsible for validating the read string to check if it follows a given format. 1124 | 2. The `create_trade_record()` method. This takes in a list of validated strings and uses them to create a `TradeRecord` object to persist to the database. 1125 | 1126 | Let us work on the implements of these two new methods. 1127 | 1128 | #### validate_trade_data() 1129 | 1130 | --- 1131 | 1132 | ```python 1133 | @staticmethod 1134 | def __validate_trade_data(fields: List[str], index: int) -> bool: 1135 | if len(fields) != 3: 1136 | TradeProcessor.__log_message(f'WARN: Line {index} malformed. Only {len(fields)} field(s) found.') 1137 | return False 1138 | if len(fields[0]) != 6: 1139 | TradeProcessor.__log_message(f'WARN: Trade currencies on line {index} malformed: {fields[0]}') 1140 | return False 1141 | try: 1142 | trade_amount = float(fields[1]) 1143 | except ValueError: 1144 | TradeProcessor.__log_message(f"WARN: Trade amount on line {index} not a valid integer: '{fields[1]}'") 1145 | return False 1146 | try: 1147 | trade_price = float(fields[2]) 1148 | except ValueError: 1149 | TradeProcessor.__log_message(f'WARN: Trade price on line {index} not a valid decimal:{fields[2]}') 1150 | return False 1151 | return True 1152 | ``` 1153 | 1154 | This method should be self explanatory since it is a refactor from the original `process_trades()` method. One thing has changed in it. The method no longer does the logging by itself. It delegates the logging to another method called `log_message()`. We shall see the advantage of this later. 1155 | 1156 | Below is the implementation of the `log_message()` method. 1157 | 1158 | ```python 1159 | @staticmethod 1160 | def __log_message(message: str) -> None: 1161 | print(message) 1162 | ``` 1163 | 1164 | #### create_trade_record() 1165 | 1166 | --- 1167 | 1168 | ```python 1169 | @staticmethod 1170 | def __create_trade_record(fields: List[str]) -> TradeRecord: 1171 | in_curr = slice(0, 3); 1172 | out_curr = slice(3, None) 1173 | source_curr_code = fields[0][in_curr] 1174 | dest_curr_code = fields[0][out_curr] 1175 | trade_amount = int(fields[1]) 1176 | trade_price = float(fields[2]) 1177 | 1178 | trade_record = TradeRecord(source_curr_code, dest_curr_code, 1179 | trade_amount, trade_price) 1180 | return trade_record 1181 | ``` 1182 | 1183 | This is also straight forward. The reason why we use slice objects here is to make our code readable. The last method we will look at is the `store_trades()` which persists our data to a database. 1184 | 1185 | #### store_trades() 1186 | 1187 | --- 1188 | 1189 | ```python 1190 | @staticmethod 1191 | def __store_trades(trades: List[TradeRecord]) -> None: 1192 | engine = create_engine('postgresql://postgres:54875/501@localhost:5432/python') 1193 | Session = sessionmaker(bind=engine) 1194 | Base.metadata.create_all(engine) 1195 | session = Session() 1196 | for trade in trades: 1197 | session.add(trade) 1198 | session.commit() 1199 | session.close() 1200 | TradeProcessor.__log_message(f'{len(trades)} trades processed') 1201 | 1202 | ``` 1203 | 1204 | This method uses an ORM known as SQLAlchemy to persist our data. ORMs write the SQL for us behind the scene and this increases the flexibility of our application. 1205 | 1206 | > This method is far from ideal, notice that it hard codes the connection strings and this very bad. There are tones of github repositories with exposed database connection strings. It would be better to read the connection string from a configuration file and add the configure file to gitignore. 1207 | 1208 | At the moment, our class that had only one big method now has a bunch of methods as shown in the following code snippet and UML class diagram: 1209 | 1210 | ```python 1211 | 1212 | class TradeProcessor(object): 1213 | @staticmethod 1214 | def process_trades(filename): 1215 | lines: List[str] = TradeProcessor.read_trade_data(filename) 1216 | trades: List[TradeRecord] = TradeProcessor.parse_trades(lines) 1217 | TradeProcessor.store_trades(trades) 1218 | 1219 | @staticmethod 1220 | def __read_trade_data(filename: str) -> List[str]: 1221 | lines: List[str] 1222 | lines = [line for line in open(filename)] 1223 | return lines 1224 | 1225 | @staticmethod 1226 | def __log_message(message: str) -> None: 1227 | print(message) 1228 | 1229 | @staticmethod 1230 | def __validate_trade_data(fields: List[str], index: int) -> bool: 1231 | if len(fields) != 3: 1232 | TradeProcessor.log_message(f'Line {index} malformed. Only {len(fields)} field(s) found.') 1233 | return False 1234 | if len(fields[0]) != 6: 1235 | TradeProcessor.log_message(f'Trade currencies on line {index} malformed: {fields[0]}') 1236 | return False 1237 | try: 1238 | trade_amount = float(fields[1]) 1239 | except ValueError: 1240 | TradeProcessor.log_message(f"Trade amount on line {index} not a valid integer: '{fields[1]}'") 1241 | return False 1242 | try: 1243 | trade_price = float(fields[2]) 1244 | except ValueError: 1245 | TradeProcessor.log_message(f'Trade price on line {index} not a valid decimal:{fields[2]}') 1246 | return False 1247 | return True 1248 | 1249 | @staticmethod 1250 | def __create_trade_record(fields: List[str]) -> TradeRecord: 1251 | in_curr = slice(0, 3); 1252 | out_curr = slice(3, None) 1253 | source_curr_code = fields[0][in_curr] 1254 | dest_curr_code = fields[0][out_curr] 1255 | trade_amount = int(fields[1]) 1256 | trade_price = float(fields[2]) 1257 | 1258 | trade_record = TradeRecord(source_curr_code, dest_curr_code, 1259 | trade_amount, trade_price) 1260 | return trade_record 1261 | 1262 | @staticmethod 1263 | def __parse_trades(trade_data: List[str]) -> List[TradeRecord]: 1264 | trades: List[TradeRecord] = [] 1265 | for index, line in enumerate(trade_data): 1266 | fields: List[str] = line.split(',') 1267 | if not TradeProcessor.validate_trade_data(fields, index + 1): 1268 | continue 1269 | trade = TradeProcessor.create_trade_record(fields) 1270 | trades.append(trade) 1271 | return trades 1272 | 1273 | @staticmethod 1274 | def __store_trades(trades: List[TradeRecord]) -> None: 1275 | engine = create_engine('postgresql://postgres:u2402/501@localhost:5432/python') 1276 | Session = sessionmaker(bind=engine) 1277 | Base.metadata.create_all(engine) 1278 | session = Session() 1279 | for trade in trades: 1280 | session.add(trade) 1281 | session.commit() 1282 | session.close() 1283 | TradeProcessor.log_message(f'{len(trades)} trades processed') 1284 | 1285 | 1286 | ``` 1287 | 1288 | ![Refactored TradeProcessor class](assets/Refactored_TradeProcessClass.PNG) 1289 | 1290 | Looking back at this refactor, it is a clear improvement on the original implementation. However, what have you really achieved? Although the new ProcessTrades method is indisputably smaller 1291 | than the monolithic original, and the code is definitely more readable, you have gained very little by way of adaptability. You can change the implementation of the LogMessage method so that it, for example, writes to a file instead of to the console, but that involves a change to the TradeProcessor class, which is precisely what you wanted to avoid. 1292 | 1293 | This refactor has been an important stepping stone on the path to truly separating the responsibilities of this class. It has been a **refactor for clarity**, not for adaptability. The next task is to split each responsibility into different classes and place them behind interfaces. What you need is true abstraction to achieve useful adaptability. 1294 | 1295 | ### **Refactoring for adaptability** 1296 | 1297 | --- 1298 | 1299 | In the previous refactor, we broke down the `process_trades()` method into smaller more focused methods. But still, that didn't solve our problem, our class was still doing lots of things. In this section, we are going to distribute the different responsibilities across classes. 1300 | 1301 | From the previous section, we agreed that our class was serving 3 main responsibilities, Data reading, Data parsing and data storage. So we will start with taking out the code that does that into other classes. 1302 | 1303 | We are going to create 3 abstract classes that will be used by the TradeProcessor class as shown in the following UML diagram. 1304 | 1305 | ![](assets/First_Refactor.PNG) 1306 | 1307 | In the above UML diagram, the TradeProcessor class now has private polymorphic hidden fields that it uses to accomplish its tasks. Since we already created smaller specific methods, we know which method goes to which abstraction. Below are the implementations of the new abstract classes. 1308 | 1309 | ```python 1310 | class DataProvider(ABC): 1311 | @abstractmethod 1312 | def read_trade_data(self): 1313 | pass 1314 | 1315 | 1316 | class TradeDataParser(ABC): 1317 | @abstractmethod 1318 | def parse_trade_data(self, lines: List[str]) -> List[TradeRecord]: 1319 | pass 1320 | 1321 | 1322 | class TradeRepository(ABC): 1323 | @abstractmethod 1324 | def persist_trade_data(self, trade_data: List[TradeRecord]) -> None: 1325 | pass 1326 | ``` 1327 | 1328 | Notice that all of them are abstract classes with abstract methods and so can't be directly instantiated. We shall then have implementors of these abstract classes to use with the `TradeProcess()` class. 1329 | 1330 | Below is the new implementation of the TradeProcessor class. 1331 | 1332 | ```python 1333 | class TradeProcessor(object): 1334 | def __init__(self, provider: DataProvider, parser: TradeDataParser, 1335 | persister: TradeRepository) -> None: 1336 | self._provider = provider 1337 | self._parser = parser 1338 | self._persister = persister 1339 | 1340 | def process_trades(self): 1341 | lines = self._provider.read_trade_data() 1342 | trades = self._parser.parse_trade_data(lines) 1343 | self._persister.persist_trade_data(trades) 1344 | ``` 1345 | 1346 | We are now doing it the object oriented way, we are having objects encapsulating computations (wait for the **strategy pattern** later). The objects that do the real work are injected into the TradeProcessor class when it is being instantiated. This is an example of **dependency inversion** which is implemented by the **dependency injection** pattern. More on this later. 1347 | 1348 | The class is now significantly different from its previous incarnation. It no longer contains the 1349 | implementation details for the whole process but instead contains the blueprint for the process. 1350 | The class models the process of transferring trade data from one format to another. This is its only 1351 | responsibility, its only concern, and the only reason that this class should change. If the process itself 1352 | changes, this class will change to reflect it. But if you decide you no longer want to retrieve data from 1353 | a file, log on to the console, or store the trades in a database, this class remains as is. 1354 | 1355 | > The more observant readers may be asking where the objects injected into the TradeProcessor class come from. Well, they come from a dependency injection container. One thing that the **Single Responsibility Principle** gives rise to are lots of small classes. To assemble such small classes to work well can be a hard thing to do, and that is when dependency injection containers come to the resucue. 1356 | 1357 | Since the `TradeProcessor` class now just models the workflow of converting between trade data formats, it no longer cares about where the data comes from, how it is parsed, validated and where it is stored. This means we can have different implementations of the 1358 | 1359 | `DataProvider` abstraction 1360 | 1361 | - Relational Database 1362 | - Text Files 1363 | - NoSql Databases 1364 | - Web services 1365 | - e.t.c 1366 | 1367 | `TradeDataParser` abstraction 1368 | 1369 | - CommaParser 1370 | - TabParser 1371 | - ColonParser 1372 | 1373 | `TradeRepository` abstraction 1374 | 1375 | - Relational Database 1376 | - Text Files 1377 | - NoSql Databases 1378 | - Web services 1379 | - e.t.c 1380 | 1381 | The UML below shows some of the classes implementing the above abstract classes. Notice that we can swap between any of the different implementations and `TradeProcessor` will not even know. This is what software engineers call **loose coupling**. 1382 | 1383 | ![Comma_parser](assets/CommaParser.PNG) 1384 | 1385 | From the above diagram, we are confident that once a new storage mechanism pops up, we just roll up a class to implement the new functionality, we make sure that the class inherits from the right base class. We then inject this new class instance in `TradeProcessor`. This is the **Open Closed Principle** as we will see in the next section. 1386 | 1387 | If you look so closely at the above diagram, you can notice that as new requirements pop up, we get a class **big bang**. We shall solve this problem later when we look at **decorators**. 1388 | 1389 | So far, we have solved 3 problems. These are: 1390 | 1391 | - What happens if we need to use another data source. 1392 | - What happens if we need to store the data to a different storage. 1393 | - What happens when the business requirements call for a new parsing strategy. 1394 | 1395 | **What happens if new business rules come up that need new validation rules?** 1396 | 1397 | Remember that the original 1398 | `parse_trades()` method delegated responsibility for validation and for mapping. You can repeat the 1399 | process of refactoring so that the `CommaParser` class does not have more than one responsibility. At the moment, `CommaParser` is implemented as shown below 1400 | 1401 | ```python 1402 | 1403 | class CommaParser(TradeDataParser): 1404 | def parse_trade_data(self, trade_data : List[str]) -> List[TradeRecord]: 1405 | trades: List[TradeRecord] = [] 1406 | for index, line in enumerate(trade_data): 1407 | fields: List[str] = line.split(',') 1408 | if not CommaParser.__validate_trade_data(fields, index + 1): 1409 | continue 1410 | trade = CommaParser.__create_trade_record(fields) 1411 | trades.append(trade) 1412 | return trades 1413 | 1414 | @staticmethod 1415 | def __log_message(message: str) -> None: 1416 | print(message) 1417 | 1418 | def __create_trade_record(self,fields: List[str]) -> TradeRecord: 1419 | in_curr = slice(0, 3); 1420 | out_curr = slice(3, None) 1421 | source_curr_code = fields[0][in_curr] 1422 | dest_curr_code = fields[0][out_curr] 1423 | trade_amount = int(fields[1]) 1424 | trade_price = float(fields[2]) 1425 | 1426 | trade_record = TradeRecord(source_curr_code, dest_curr_code, 1427 | trade_amount, trade_price) 1428 | return trade_record 1429 | 1430 | def __validate_trade_data(self, fields: List[str], index: int) -> bool: 1431 | if len(fields) != 3: 1432 | CommaParser.__log_message(f'Line {index} malformed. Only {len(fields)} field(s) found.') 1433 | return False 1434 | if len(fields[0]) != 6: 1435 | CommaParser.__log_message(f'Trade currencies on line {index} malformed: {fields[0]}') 1436 | return False 1437 | try: 1438 | trade_amount = float(fields[1]) 1439 | except ValueError: 1440 | CommaParser.__log_message(f"Trade amount on line {index} not a valid integer: '{fields[1]}'") 1441 | return False 1442 | try: 1443 | trade_price = float(fields[2]) 1444 | except ValueError: 1445 | CommaParser.__log_message(f'Trade price on line {index} not a valid float:{fields[2]}') 1446 | return False 1447 | return True 1448 | ``` 1449 | 1450 | We can see that the current implementation of `CommaParser` is not ideal. The class is having more than one responsibility to change. So we can refactor out the two methods `__validate_trade_data()` and `__create_trade_record()` into new classes since they both change for different reasons. 1451 | 1452 | We will create 2 new abstractions -- `TradeMapper` (responsible for mapping validated fields into `TradeRecord` instances) and `TradeValidator` (responsible for validating the input data before creating `TradeRecord` instances). 1453 | 1454 | Our new design is shown in the following UML diagram. 1455 | 1456 | ![ParserHierachy](assets/ParserHierachy.PNG) 1457 | 1458 | This is a flexible design in such a way that if the parsing rules change, i.e. text is separated by tab and not ',', we just implement `TradeDataParser` in a new class.Incase the data validation rules change too, we just roll up a new class inheriting from `TradeValidator`. 1459 | 1460 | Below are the implementations of the new abstractions. Note that the interface for `TradeDataParser` has changed and now takes in instances of `TradeValidator` and `TradeMapper` to help it accomplish it's task 1461 | 1462 | ```python 1463 | 1464 | class TradeMapper(ABC): 1465 | @abstractmethod 1466 | def create_trade_record(self, fields: List[str]) -> TradeRecord: 1467 | pass 1468 | 1469 | 1470 | class TradeValidator(ABC): 1471 | @abstractmethod 1472 | def validate_trade_data(self, fields: List[str], index: int) -> bool: 1473 | pass 1474 | 1475 | 1476 | class TradeDataParser(ABC): 1477 | @abstractmethod 1478 | def parse_trade_data(self, trade_data: List[str]) -> TradeRecord: 1479 | pass 1480 | ``` 1481 | 1482 | And then here is the new implementation of the CommaParser in terms of these new abstractions. 1483 | 1484 | ![Delegation](assets/delegation.png) 1485 | 1486 | Pay attention to the green rectangles. In the constructor, two dependencies are injected in `mapper` and `validator` and these two are used by CommaParser to parse the input assuming the string components are separated by commas hence the `split(',')`. Other parsers would implement it differently. 1487 | 1488 | Below are possible implementations of the `TradeMapper` and `TradeValidator` abstractions. 1489 | 1490 | ```python 1491 | class SimpleTradeMapper(TradeMapper): 1492 | def create_trade_record(self, fields: List[str]) -> TradeRecord: 1493 | in_curr = slice(0, 3); 1494 | out_curr = slice(3, None) 1495 | source_curr_code = fields[0][in_curr] 1496 | dest_curr_code = fields[0][out_curr] 1497 | trade_amount = int(fields[1]) 1498 | trade_price = float(fields[2]) 1499 | 1500 | trade_record = TradeRecord(source_curr_code, dest_curr_code, 1501 | trade_amount, trade_price) 1502 | return trade_record 1503 | ``` 1504 | 1505 | ```python 1506 | 1507 | class SimpleValidator(TradeValidator): 1508 | @staticmethod 1509 | def __log_message(message: str) -> None: 1510 | print(message) 1511 | 1512 | def validate_trade_data(self, fields: List[str], index: int) -> bool: 1513 | if len(fields) != 3: 1514 | SimpleValidator.__log_message(f'Line {index} malformed. Only {len(fields)} field(s) found.') 1515 | return False 1516 | if len(fields[0]) != 6: 1517 | SimpleValidator.__log_message(f'Trade currencies on line {index} malformed: {fields[0]}') 1518 | return False 1519 | try: 1520 | trade_amount = float(fields[1]) 1521 | except ValueError: 1522 | SimpleValidator.__log_message(f"Trade amount on line {index} not a valid integer: '{fields[1]}'") 1523 | return False 1524 | try: 1525 | trade_price = float(fields[2]) 1526 | except ValueError: 1527 | SimpleValidator.__log_message(f'Trade price on line {index} not a valid float:{fields[2]}') 1528 | return False 1529 | return True 1530 | 1531 | ``` 1532 | 1533 | We are almost there but still we are having a smell in our design. We would love to be able to log to different destinations -- console, text file or even a database. But if you look closely at the implementations of `TradeRepository` and `TradeValidator`, the logger is hard coded and it always logs to the console. 1534 | 1535 | We have to solve this problem before we run out of business. We are going to refactor this function into its abstraction. The following snippet reveals the snippet for this change. 1536 | 1537 | ```python 1538 | from abc import ABC, abstractmethod 1539 | 1540 | class TradeLogger(ABC): 1541 | @abstractmethod 1542 | def log_message(self, message): 1543 | pass 1544 | 1545 | 1546 | class SimpleValidator(TradeValidator): 1547 | def __init__(self, logger: TradeLogger)->None: 1548 | if instance(logger, TradeLogger): 1549 | self._logger = logger 1550 | else: 1551 | raise AssertionError('Bad Argument') 1552 | 1553 | def validate_trade_data(self, fields: List[str], index: int) -> bool: 1554 | if len(fields) != 3: 1555 | self._logger.log_message(f'Line {index} malformed. Only {len(fields)} field(s) found.') 1556 | return False 1557 | if len(fields[0]) != 6: 1558 | self._logger.log_message(f'Trade currencies on line {index} malformed: {fields[0]}') 1559 | return False 1560 | try: 1561 | trade_amount = float(fields[1]) 1562 | except ValueError: 1563 | self._logger.log_message(f"Trade amount on line {index} not a valid integer: '{fields[1]}'") 1564 | return False 1565 | try: 1566 | trade_price = float(fields[2]) 1567 | except ValueError: 1568 | self._logger.log_message(f'Trade price on line {index} not a valid float:{fields[2]}') 1569 | return False 1570 | return True 1571 | ``` 1572 | 1573 | After all these refactorings, we finally have a collection of abstractions that work together to solve the simple problem we posed at the beginning of this chapter. 1574 | 1575 | The figure below shows the design of the abstractions. 1576 | 1577 | ![framework](assets/framework.PNG) 1578 | 1579 | > **Note** that none of these are concrete classes and so they can not be instantiated. To use the `TradeProcessor` class, you will need concrete implementations of all these abstractions and then you will have to wire them together to accomplish a task. **Dependency Injection** containers do this wiring. 1580 | 1581 | From a monolith, we have created a miniature framework for converting trade data between formats. Congratulations!!!!!. 1582 | 1583 | ## Open/Closed Principle(OCP) 1584 | 1585 | --- 1586 | 1587 | We will now go to the next principle on my list of the SOLID principles of Object Oriented software design--**The Open/Closed Principle**. This principle states that **A software artifact should be closed for modification but open for extension**. 1588 | 1589 | At first, this definition seems to be a paradox. How can a software module be closed for modification but open for extension?? Well, we shall see how achieve this goal with the principles we shall discuss in this section. 1590 | 1591 | This term was first coined in 1988 by **Bertran Meyer** in his book **Object-Oriented Software Construction (Prentice Hall)**. The modern definition of this principle was offered by Martin Roberts and goes as follows 1592 | 1593 | > **Open for extension** : This means that the behavior of the module can be extended. 1594 | > As the requirements of the application change, we are able to extend the module 1595 | > with new behaviors that satisfy those changes. In other words, we are able to 1596 | > change what the module does.

> **Closed for modification** : Extending the behavior of a module does not result 1597 | > in changes to the source or binary code of the module. The binary executable 1598 | > version of the module, whether in a linkable library, a DLL, or a Java .jar, remains untouched. 1599 | 1600 | There are 2 exceptions to this rule. Code can be edited if : 1601 | 1602 | 1. Fixing bugs. 1603 | 1604 | If a module contains a bug, we can either choose to write a new similar module without the bugs but this would be an overkill solution. So we tend to prefer fixing the buggy module to writing a new one. 1605 | 1606 | 2. Client awareness. 1607 | 1608 | Another situation where it is possible to edit the source code of a module is when the changes don't affect the client of the module.This places an 1609 | emphasis on how coupled the software modules are, at all levels of granularity: between classes and 1610 | classes, assemblies and assemblies, and subsystems and subsystems. 1611 | 1612 | If a change in one class forces a change in another, the two are said to be tightly coupled. Conversely, if a class can change in isolation without forcing other classes to change, the participating 1613 | classes are loosely coupled. At all times and at all levels, loose coupling is preferable. Maintaining 1614 | loose coupling limits the impact that the OCP has if you allow modifications to existing code that 1615 | does not force further changes to clients 1616 | 1617 | To illustrate the OCP rule, we are going to use the following techniques 1618 | 1619 | 1. Strategy pattern 1620 | 1. Decorator design pattern 1621 | 1622 | ### Strategy design pattern. 1623 | 1624 | --- 1625 | 1626 | > **Strategy Pattern**
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it. 1627 | 1628 | This definition seems abstract enough but we are going to try explaininig it in the following example. 1629 | Consider the following class that is part of an e-commerce application. The class contains a method that selects the which payment to choose for settling a payment as shown below. 1630 | 1631 | **Bad** :angry: 1632 | 1633 | ```python 1634 | class OnlineCart: 1635 | def check_out(self, payment_type: str) -> None: 1636 | if payment_type == 'creditCard': 1637 | self.process_credit_card_payment() 1638 | elif payment_type == 'payPal': 1639 | self.process_paypal_payment() 1640 | elif payment_type == 'GoogleCheckout': 1641 | self.process_google_payment() 1642 | elif payment_type == 'AmazonPayments': 1643 | self.process_amazon_payment() 1644 | else: 1645 | pass 1646 | 1647 | def process_credit_card_payment(self): 1648 | print('paying with credit card...') 1649 | 1650 | def process_paypal_payment(self): 1651 | print('Paying with paypal...') 1652 | 1653 | def process_google_payment(self): 1654 | print('Paying with google check out') 1655 | 1656 | def process_amazon_payment(self): 1657 | print('Paying with amazon ...') 1658 | ``` 1659 | 1660 | The above class is neither extendable nor flexible. If a new payment method comes up, the conditional logic will have to be changed and a new method added to the class. This class violets the OCP rule and thus needs to be refactored. 1661 | 1662 | There are many ways to solve this simple problem but I will stick with the original solution proposed by the GoF programmers. We will use the **strategy pattern**. We will model each payment method as a class and we will use composition and inject in the payment strategy at run-time. 1663 | 1664 | **Good** :smile: 1665 | 1666 | ```python 1667 | from abc import ABC, abstractmethod 1668 | 1669 | class Payment(ABC): 1670 | def __init__(self, payment_id: str): 1671 | self.id = payment_id 1672 | 1673 | @abstractmethod 1674 | def pay(self): 1675 | pass 1676 | 1677 | ``` 1678 | 1679 | This is the interface that all payment strategies are supposed to implement. We are going to use to create a family of payment strategies. 1680 | 1681 | **Good** :smile: 1682 | 1683 | ```python 1684 | class OnlineCart: 1685 | def __init__(self, payment: Payment) -> None: 1686 | if isinstance(payment, Payment): 1687 | self.payment = payment 1688 | else: 1689 | raise AssertionError('Bad argument') 1690 | 1691 | def check_out(self): 1692 | self.payment.pay() 1693 | 1694 | 1695 | ``` 1696 | 1697 | The `OnLineCart` class no longer contains the conditional logic and all the corresponding methods have been pulled out. They will be implemented by the corresponding payment strategies as the following code snippet reveals. 1698 | 1699 | **Good** :smile: 1700 | 1701 | ```python 1702 | 1703 | class CreditCard(Payment): 1704 | def __init__(self, *, card_number: str) -> None: 1705 | Payment.__init__(self, card_number) 1706 | 1707 | def pay(self) -> None: 1708 | print(f'Payment made with card number {self.id}') 1709 | 1710 | 1711 | class Paypal(Payment): 1712 | def __init__(self, *, paypal_id: str) -> None: 1713 | Payment.__init__(self, paypal_id) 1714 | 1715 | def pay(self) -> None: 1716 | print(f'Payment made with paypal id {self.id}') 1717 | 1718 | 1719 | class GoogleCheckOut(Payment): 1720 | def __init__(self, *, google_checkout: str) -> None: 1721 | Payment.__init__(self, google_checkout) 1722 | 1723 | def pay(self) -> None: 1724 | print(f'Payment made with google checkout with id {self.id}') 1725 | 1726 | 1727 | class AmazonPayment(Payment): 1728 | def __init__(self, *, amazon_payment: str) -> None: 1729 | Payment.__init__(self, amazon_payment) 1730 | 1731 | def pay(self) -> None: 1732 | print(f'Payment made with amazon services using id {self.id}') 1733 | 1734 | 1735 | ``` 1736 | 1737 | To use the `OnlineCart` class, we inject in the payment strategy to use for making the payment as shown in the following snippet. 1738 | 1739 | ```python 1740 | # we are paying using paypal 1741 | paypal : Payment = PayPal(paypal_id='ERTWF342T) 1742 | cart : OnlineCart = OnlineCart(paypal) 1743 | cart.check_out() 1744 | ``` 1745 | 1746 | Note that the `OnlineCart` class no longer cares about which payment method is being used, it delegates that responsibility to the wrapped object. `OnlineCart` is now open for extension (we can change its behavior by passing in different objects) but it is closed for modification (we don't change its source code to add new functionality). 1747 | 1748 | ![Strategy_Pattern](assets/strategypattern.PNG) 1749 | 1750 | From the above UML class diagram, we can notice that once a new payment method shows up, we just create a new class for that method, inherit from `Payment` and inject it in `OnlineCart`. This code is flexible and extendable. 1751 | 1752 | > **Note**: The same design could be achieved with lambda expressions though at times the logic in the respective strategiess may be complex enough that it is implemented in more than one function. This is why i decided to use this rather verbose method. 1753 | 1754 | ### The Decorator design pattern 1755 | 1756 | --- 1757 | 1758 | > **Decorator Pattern**
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to sub-classing for extending functionality. 1759 | 1760 | The decorator design pattern was first proposed in 1994 in the seminal work of the Gang of Four book. It is a technique of adding capabilities to a class without changing its source code. 1761 | We are going to view this under various examples. 1762 | 1763 | We are going to continue with our example in the previous section. Consider that we want to add some logging information after making the payment. There are many ways to solve this problem and one of them is to edit the `OnlineCart` class to add logging features as shown below; 1764 | 1765 | ![code_smell](assets/modification.png) 1766 | 1767 | This is a **code smell**. Notice that we have modified the class, this is violation of the Open/Closed principle. There is even a more serious problem than this one. In this case we are logging to the console, what will happen if we want to log to a database or to a text file? We will have to constantly open this class and modify it, this is serious violation of the OCP rule. 1768 | 1769 | One solution to this problem is the **decorator pattern**. Decorators are just classes that wrapper other classes. The **wrapped classes** have exactly the same interface as the **wrapper classes**. We achieve this by using both **composition** and **inheritance** as the following UML diagram reveals. 1770 | 1771 | ![decorator_pattern](assets/decoraror_pattern.PNG) 1772 | 1773 | In this case, the `Component` is an interface that is both supported by `ConcreteComponent` and `Decorator` this means that both `ConcreteComponent` and `Decorator` can be swapped without breaking existing code. 1774 | 1775 | Notice also that the `Decorator` contains a `Component` inside it implying that it delegates some of its tasks to the wrapped component. 1776 | 1777 | Let us use this technique to add logging capabilities to the `OnlineCart` class without having to modify it. 1778 | 1779 | Looking very closely at the code for `OnlineCart`, notice that the `Payment` object is being injected during the instantiation of the class and so the `OnlineCart` class doesn't control what type of payment it receives (remember `Payment` is polymorphic). 1780 | 1781 | ![DI](assets/OnlineCart.png) 1782 | 1783 | This means we can inject in anything that is similar to `Payment`. That is what the **Decorator pattern** is based on. **Dependency Injection** is the prerequisite to achieving all this flexibility. We shall cover dependency injection fully under the **Dependency Inversion Principle (DIP)**. 1784 | 1785 | The following diagram shows the idea behind the decorator pattern. 1786 | ![Interception](assets/interception.PNG) 1787 | 1788 | In the first row, the `OnlineCart` class is directly depending on the `Payment` abstraction. In the second row, the `OnlineCart` class no longer depends directly on the `Payment` abstraction, there has been some redirection. 1789 | 1790 | Ok!! time for some code. 1791 | 1792 | Below is the code for our abstract decorator class. 1793 | 1794 | ```python 1795 | class Decorator(Payment, ABC): 1796 | def __init__(self, payment: Payment) -> None: 1797 | if isinstance(payment, Payment): 1798 | self.payment = payment 1799 | else: 1800 | raise AssertionError('Bad argument') 1801 | 1802 | @abstractmethod 1803 | def pay(self): 1804 | pass 1805 | ``` 1806 | 1807 | Pay close attention to this class. 1808 | 1809 | 1. It uses multiple inheritance : This is because we need the class to both be abstract and still inherit from the `Payment` class. 1810 | 2. It takes in a `Payment` dependency and inherits from `Payment`. This is typical of decorator classes. 1811 | 3. The `pay()` method is abstract since concrete decorators will have to define their implementations. 1812 | 1813 | We can now implement our `Logging` decorator that adds logging capabilities to the `OnlineCart` class without modifying it. Let's go!!! 1814 | 1815 | ```python 1816 | class ConsoleLoggingDecorator(Decorator): 1817 | def __init__(self, payment: Payment): 1818 | Decorator.__init__(self, payment) 1819 | 1820 | def pay(self): 1821 | self.payment.pay() 1822 | print(f'Logging Payment made with id {self.payment.id}') 1823 | ``` 1824 | 1825 | Simple!!! This is the console logging decorator because it logs on the console using `print`. We can create a file logging decorator that logs into a text file as shown below. 1826 | 1827 | ```python 1828 | class FileLoggingDecorator(Decorator): 1829 | def __init__(self, *, payment: Payment, filename: str): 1830 | Decorator.__init__(self, payment) 1831 | self.filename = filename 1832 | 1833 | def pay(self): 1834 | self.payment.pay() 1835 | _file = open(self.filename, 'a') 1836 | _file.writelines(f'{self.payment.id} payment logged\n') 1837 | _file.close() 1838 | ``` 1839 | 1840 | The snippet shows the code that sets up the `OnlineCart` class to use the `ConsoleLoggingDecorator` and then the `FileLoggingDecorator` 1841 | 1842 | ```python 1843 | #using the console logger 1844 | credit_card: Payment = CreditCard(card_number='RTGW@#') 1845 | decorator: FileLoggingDecorator = ConsoleLoggingDecorator(payment=credit_card) 1846 | cart: OnlineCart = OnlineCart(decorator) 1847 | cart.check_out() 1848 | 1849 | #using the file logger 1850 | credit_card: Payment = CreditCard(card_number='RTGW@#') 1851 | decorator: FileLoggingDecorator = FileLoggingDecorator(filename='dta.txt', payment=paypal) 1852 | cart: OnlineCart = OnlineCart(decorator) 1853 | cart.check_out() 1854 | ``` 1855 | 1856 | **Note**: We first create a bare payment object and then wrap it in a decorator which we then inject into `OnlineCart` class. The decorator adds some capabilities (in this case logging) to the payment object. 1857 | 1858 | We can add any functionality to the `OnlineCart` class without modifying it. Capabilities like Profiling, Laziness, Immutability, e.t.c. 1859 | 1860 | The following UML class diagram shows our work up to to this point in time. 1861 | 1862 | ![Decorator_refactor](assets/Decorator_refactor.PNG) 1863 | 1864 | We can add more organization to the decorator hierachy by using the **Template pattern**. That will be a story for the next time. 1865 | 1866 | ## Liskov Substitution Principle 1867 | --------------------------------------------------------------------------------