├── .gitignore ├── Chapter01 ├── README.md ├── invoices.json ├── main.py ├── performance_calculator.py ├── plays.json ├── statement.py ├── statement_data.py └── tests.py ├── Chapter04 ├── README.md ├── producer.py ├── province.py └── test_province.py ├── Chapter06 ├── 01-extract-function │ ├── example.py │ ├── invoice.py │ ├── order.py │ └── test_example.py ├── 02-inline-function │ ├── customer.py │ ├── driver.py │ ├── example1.py │ ├── example2.py │ ├── example3.py │ └── test_example.py ├── 03-extract-variable │ ├── order.py │ ├── price.py │ └── test_example.py ├── 04-inline-variable │ ├── order.py │ └── test_order.py ├── 05-change-function-declaration │ ├── address.py │ ├── book.py │ ├── circum.py │ ├── customer.py │ ├── in_new_england.py │ └── test_example.py ├── 06-encapsulate-variable │ ├── default_owner.py │ └── test_default_owner.py ├── 07-rename-variable │ ├── company_name.py │ ├── test_example.py │ └── title.py ├── 08-introduce-parameter-object │ ├── example.py │ ├── number_range.py │ ├── operating_plan.py │ └── test_example.py ├── 09-combine-functions-into-class │ ├── client1.py │ ├── client2.py │ ├── client3.py │ ├── dummy.py │ ├── reading.py │ └── test_client.py ├── 10-combine-functions-into-transform │ ├── client1.py │ ├── client2.py │ ├── client3.py │ ├── dummy.py │ ├── reading.py │ └── test_example.py ├── 11-split-phase │ ├── price_data.py │ ├── price_order.py │ ├── product.py │ ├── shipping_method.py │ └── test_price_order.py └── README.md ├── Chapter07 ├── 01-encapsulate-record │ ├── customer_data.json │ ├── customer_data.py │ ├── example1.py │ ├── example2.py │ ├── organization.py │ ├── test_example1.py │ └── test_example2.py ├── 02-encapsulate-collection │ ├── course.py │ ├── person.py │ └── test_example.py ├── 03-replace-primitive-with-object │ ├── example.py │ ├── order.py │ └── test_example.py ├── 04-replace-temp-with-query │ ├── order.py │ └── test_order.py ├── 05-extract-class │ ├── person.py │ └── test_person.py ├── 06-inline-class │ ├── shipment.py │ └── test_shipment.py ├── 07-hide-delegate │ ├── department.py │ ├── person.py │ └── test_person.py ├── 08-remove-middle-man │ ├── department.py │ ├── person.py │ └── test_person.py └── README.md ├── Chapter08 ├── 01-move-function │ ├── account.py │ ├── point.py │ ├── test_example.py │ └── track_summary.py ├── 02-move-field │ ├── account.py │ ├── customer.py │ └── test_customer.py ├── 03-move-statements-into-function │ ├── example.py │ ├── person.py │ ├── photo.py │ └── test_example.py ├── 04-move-statements-to-callers │ ├── example.py │ ├── person.py │ ├── photo.py │ └── test_example.py ├── 07-split-loop │ ├── example.py │ ├── person.py │ └── test_example.py ├── 08-replace-loop-with-pipeline │ ├── example.py │ └── test_example.py └── README.md ├── Chapter09 ├── 01-split-variable │ ├── example.py │ ├── scenario.py │ └── test_example.py ├── 02-rename-field │ ├── example.py │ ├── organization.py │ └── test_example.py ├── 03-replace-derived-variable-with-query │ ├── adjustment.py │ ├── production_plan.py │ ├── production_plan2.py │ └── test_example.py ├── 04-change-reference-to-value │ ├── person.py │ ├── telephone_number.py │ └── test_example.py ├── 05-change-value-to_reference │ ├── customer.py │ ├── order.py │ └── test_order.py └── README.md ├── Chapter10 ├── 01-decompose-conditional │ ├── example.py │ ├── plan.py │ └── test_example.py ├── 02-consolidate-conditional-expression │ ├── employee.py │ ├── example.py │ └── test_example.py ├── 03-replace-nested-conditional-with-guard-clauses │ ├── employee.py │ ├── example.py │ ├── instrument.py │ └── test_example.py ├── 04-replace-conditional-with-polymorphism │ ├── bird.py │ ├── test_bird.py │ ├── test_voyage_rating.py │ └── voyage_rating.py ├── 05-introduce-special-case │ ├── customer.py │ ├── example.py │ ├── my_site.py │ └── test_example.py ├── 06-introduce-assertion │ ├── customer.py │ └── test_customer.py ├── 07-replace-control-flag-with-break │ ├── example.py │ └── test_example.py └── README.md ├── Chapter11 ├── 01-seperate-query-from-modifier │ ├── client.py │ ├── example.py │ └── test_example.py ├── 02-parameterize-function │ ├── example.py │ └── test_example.py ├── 03-remove-flag-argument │ ├── example1 │ │ ├── example.py │ │ ├── order.py │ │ └── test_example.py │ └── example2 │ │ ├── example.py │ │ ├── order.py │ │ └── test_example.py ├── 04-preserve-whole-object │ ├── example1 │ │ ├── example.py │ │ ├── heating_plan.py │ │ ├── room.py │ │ ├── temperature_range.py │ │ └── test_example.py │ └── example2 │ │ ├── example.py │ │ ├── heating_plan.py │ │ ├── room.py │ │ ├── temperature_range.py │ │ └── test_example.py ├── 05-replace-parameter-with-query │ ├── order.py │ └── test_order.py ├── 06-replace-query-with-parameter │ ├── example.py │ ├── heating_plan.py │ ├── test_example.py │ └── thermostat.py ├── 07-remove-setting-method │ ├── person.py │ └── test_person.py ├── 08-replace-constructor-with-factory-function │ ├── employee.py │ └── test_employee.py ├── 09-replace-function-with-command │ ├── candidate.py │ ├── medical_exam.py │ ├── score.py │ ├── scoring_guide.py │ └── test_score.py ├── 10-replace-command-with-function │ ├── charge.py │ ├── customer.py │ ├── provider.py │ └── test_charge.py ├── 11-return-modified-value │ ├── ascent_calculator.py │ ├── point.py │ └── test_ascent_calculator.py ├── 12-replace-error-code-with-exception │ ├── country_data.py │ ├── error.py │ ├── example.py │ ├── order.py │ ├── shipping_rules.py │ └── test_example.py ├── 13-replace-exception-with-precheck │ ├── resource_pool.py │ └── test_resource.py └── README.md ├── Chapter12 ├── 01-pull-up-method │ ├── department.py │ ├── employee.py │ ├── party.py │ └── test_party.py ├── 03-pull-up-constructor-body │ ├── example1 │ │ ├── department.py │ │ ├── employee.py │ │ ├── party.py │ │ └── test_party.py │ └── example2 │ │ ├── employee.py │ │ └── manager.py ├── 06-replace-type-code-with-subclasses │ ├── example1 │ │ ├── employee.py │ │ └── test_employee.py │ └── example2 │ │ ├── employee.py │ │ └── test_employee.py ├── 07-remove-subclass │ ├── client.py │ ├── person.py │ └── test_client.py ├── 08-extract-superclass │ ├── department.py │ ├── employee.py │ ├── party.py │ └── test_example.py ├── 10-replace-subclass-with-delegate │ ├── example1 │ │ ├── booking.py │ │ ├── extras.py │ │ ├── show.py │ │ └── test_booking.py │ └── example2 │ │ ├── bird.py │ │ └── test_bird.py ├── 11-replace-superclass-with-delegate │ ├── catalog_item.py │ ├── scroll.py │ └── test_scroll.py └── README.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Chapter01/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 1: Refactoring: A First Example 2 | 3 | ## Contents 4 | 5 | 1. The Starting Point 6 | 2. Comments on the Starting Program 7 | 3. The First Step in Refactoring 8 | 4. Decomposing the statement Function 9 | 5. Status: Lots of Nested Functions 10 | 6. Splitting the Phases of Calculation and Formatting 11 | 7. Status: Separated into Two Files (and Phases) 12 | 8. Reorganizing the Calculations by Types 13 | 9. Status: Creating the Data with the Polymorphic 14 | 10. Calculator 15 | 11. Final Thoughts 16 | -------------------------------------------------------------------------------- /Chapter01/invoices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "customer": "BigCo", 4 | "performances": [ 5 | { 6 | "playID": "hamlet", 7 | "audience": 55 8 | }, 9 | { 10 | "playID": "as-like", 11 | "audience": 35 12 | }, 13 | { 14 | "playID": "othello", 15 | "audience": 40 16 | } 17 | ] 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /Chapter01/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from statement import statement 4 | 5 | 6 | def main(): 7 | invoice_doc = "" 8 | 9 | with open("plays.json") as play_f, open("invoices.json") as invoice_f: 10 | plays = json.load(play_f) 11 | invoices = json.load(invoice_f) 12 | 13 | for invoice in invoices: 14 | invoice_doc += statement(invoice, plays) 15 | 16 | print(invoice_doc, end="") 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /Chapter01/performance_calculator.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | class PerformanceCalculator: 5 | def __init__(self, performance: dict, play: dict): 6 | self.performance = performance 7 | self.play = play 8 | 9 | def get_amount(self) -> int: 10 | raise Exception("Use subclass") 11 | 12 | def get_volume_credits(self) -> int: 13 | return max(self.performance["audience"] - 30, 0) 14 | 15 | 16 | class TragedyCalculator(PerformanceCalculator): 17 | def get_amount(self) -> int: 18 | result = 40000 19 | if self.performance["audience"] > 30: 20 | result += 1000 * (self.performance["audience"] - 30) 21 | return result 22 | 23 | 24 | class ComedyCalculator(PerformanceCalculator): 25 | def get_amount(self) -> int: 26 | result = 30000 27 | if self.performance["audience"] > 20: 28 | result += 10000 + 500 * (self.performance["audience"] - 20) 29 | result += 300 * self.performance["audience"] 30 | return result 31 | 32 | def get_volume_credits(self) -> int: 33 | result = super().get_volume_credits() 34 | result += math.floor(self.performance["audience"] / 5) 35 | return result 36 | 37 | 38 | def create_performance_calculator( 39 | performance: dict, play: dict 40 | ) -> PerformanceCalculator: 41 | if play["type"] == "tragedy": 42 | return TragedyCalculator(performance, play) 43 | elif play["type"] == "comedy": 44 | return ComedyCalculator(performance, play) 45 | else: 46 | raise ValueError(f'Unknown genre: {play["type"]}') 47 | 48 | -------------------------------------------------------------------------------- /Chapter01/plays.json: -------------------------------------------------------------------------------- 1 | { 2 | "hamlet": { "name": "Hamlet", "type": "tragedy" }, 3 | "as-like": { "name": "As You Like It", "type": "comedy" }, 4 | "othello": { "name": "Othello", "type": "tragedy" } 5 | } 6 | -------------------------------------------------------------------------------- /Chapter01/statement.py: -------------------------------------------------------------------------------- 1 | from statement_data import create_statement_data 2 | 3 | 4 | def statement(invoice: dict, plays: dict) -> str: 5 | return render_plain_text(create_statement_data(invoice, plays)) 6 | 7 | 8 | def render_plain_text(data: dict) -> str: 9 | result = f"Invoice (Customer: {data['customer']})\n" 10 | 11 | for perf in data["performances"]: 12 | result += ( 13 | f"\t{perf['play']['name']}: " 14 | f"{get_usd(perf['amount'])} ({perf['audience']} Seats)\n" 15 | ) 16 | 17 | result += f"Total Amount: {get_usd(data['total_amount'])}\n" 18 | result += f"Volume Credits: {data['total_volume_credits']}\n" 19 | return result 20 | 21 | 22 | def html_statement(invoice: dict, plays: dict) -> str: 23 | return render_html(create_statement_data(invoice, plays)) 24 | 25 | 26 | def render_html(data: dict) -> str: 27 | result = f"
Play | Seats | Price |
---|---|---|
{perf['play']['name']} | " 34 | f"({perf['audience']} Seats) | " 35 | f"{get_usd(perf['amount'])} |
Total Amount: {get_usd(data['total_amount'])}
\n" 40 | result += ( 41 | f"Volume Credits: {data['total_volume_credits']}
\n" 42 | ) 43 | return result 44 | 45 | 46 | def get_usd(num: float) -> str: 47 | return "${:,.2f}".format(num / 100) 48 | 49 | -------------------------------------------------------------------------------- /Chapter01/statement_data.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from functools import reduce 3 | 4 | from performance_calculator import create_performance_calculator 5 | 6 | 7 | def create_statement_data(invoice: dict, plays: dict) -> dict: 8 | result = {} 9 | result["customer"] = invoice["customer"] 10 | result["performances"] = [ 11 | enrich_performance(perf, plays) for perf in invoice["performances"] 12 | ] 13 | result["total_amount"] = get_total_amount(result) 14 | result["total_volume_credits"] = get_total_volume_credits(result) 15 | return result 16 | 17 | 18 | def enrich_performance(performance: dict, plays: dict) -> dict: 19 | calculator = create_performance_calculator( 20 | performance, get_play_for(performance, plays) 21 | ) 22 | result = copy(performance) # Shallow copy 23 | result["play"] = calculator.play 24 | result["amount"] = calculator.get_amount() 25 | result["volume_credits"] = calculator.get_volume_credits() 26 | return result 27 | 28 | 29 | def get_play_for(performance: dict, plays: dict) -> dict: 30 | return plays[performance["playID"]] 31 | 32 | 33 | def get_total_amount(data: dict) -> int: 34 | return reduce(lambda total, p: total + p["amount"], data["performances"], 0) 35 | 36 | 37 | def get_total_volume_credits(data: dict) -> int: 38 | return reduce( 39 | lambda total, p: total + p["volume_credits"], data["performances"], 0 40 | ) 41 | -------------------------------------------------------------------------------- /Chapter01/tests.py: -------------------------------------------------------------------------------- 1 | from statement import statement, html_statement 2 | import pytest 3 | 4 | 5 | def test_statement(): 6 | plays = { 7 | "hamlet": {"name": "Hamlet", "type": "tragedy"}, 8 | "as-like": {"name": "As You Like It", "type": "comedy"}, 9 | "othello": {"name": "Othello", "type": "tragedy"}, 10 | } 11 | invoice = { 12 | "customer": "BigCo", 13 | "performances": [ 14 | {"playID": "hamlet", "audience": 55}, 15 | {"playID": "as-like", "audience": 35}, 16 | {"playID": "othello", "audience": 40}, 17 | ], 18 | } 19 | expected = ( 20 | "Invoice (Customer: BigCo)\n" 21 | "\tHamlet: $650.00 (55 Seats)\n" 22 | "\tAs You Like It: $580.00 (35 Seats)\n" 23 | "\tOthello: $500.00 (40 Seats)\n" 24 | "Total Amount: $1,730.00\n" 25 | "Volume Credits: 47\n" 26 | ) 27 | 28 | assert statement(invoice, plays) == expected 29 | 30 | 31 | def test_statement_unknown_genre(): 32 | plays = { 33 | "hamlet": {"name": "Hamlet", "type": "tragedy"}, 34 | "as-like": {"name": "As You Like It", "type": "comedy"}, 35 | "othello": {"name": "Othello", "type": "sci-fi"}, 36 | } 37 | invoice = { 38 | "customer": "BigCo", 39 | "performances": [ 40 | {"playID": "hamlet", "audience": 55}, 41 | {"playID": "as-like", "audience": 35}, 42 | {"playID": "othello", "audience": 40}, 43 | ], 44 | } 45 | with pytest.raises(ValueError): 46 | statement(invoice, plays) 47 | 48 | 49 | def test_html_statement(): 50 | plays = { 51 | "hamlet": {"name": "Hamlet", "type": "tragedy"}, 52 | "as-like": {"name": "As You Like It", "type": "comedy"}, 53 | "othello": {"name": "Othello", "type": "tragedy"}, 54 | } 55 | invoice = { 56 | "customer": "BigCo", 57 | "performances": [ 58 | {"playID": "hamlet", "audience": 55}, 59 | {"playID": "as-like", "audience": 35}, 60 | {"playID": "othello", "audience": 40}, 61 | ], 62 | } 63 | expected = ( 64 | "Play | Seats | Price |
---|---|---|
Hamlet | (55 Seats) | $650.00 |
As You Like It | (35 Seats) | $580.00 |
Othello | (40 Seats) | $500.00 |
Total Amount: $1,730.00
\n" 72 | "Volume Credits: 47
\n" 73 | ) 74 | assert html_statement(invoice, plays) == expected 75 | -------------------------------------------------------------------------------- /Chapter04/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 4: Building Tests 2 | 3 | ## Contents 4 | 5 | 1. The Value of Self-Testing Code 6 | 2. Sample Code to Test 7 | 3. A First Test 8 | 4. Add Another Test 9 | 5. Modifying the Fixture 10 | 6. Probing the Boundaries 11 | 7. Much More Than This 12 | -------------------------------------------------------------------------------- /Chapter04/producer.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | 4 | class Producer: 5 | def __init__(self, province, data): 6 | self._province = province 7 | self._cost = data["cost"] 8 | self._name = data["name"] 9 | self._production = data.get("production", 0) 10 | 11 | @property 12 | def name(self): 13 | return self._name 14 | 15 | @property 16 | def cost(self): 17 | return self._cost 18 | 19 | @cost.setter 20 | def cost(self, value): 21 | self._cost = int(value) 22 | 23 | @property 24 | def production(self): 25 | return self._production 26 | 27 | @production.setter 28 | def production(self, amount: Union[str, int]): 29 | if type(amount) == str: 30 | new_production = int(amount) if amount.isdigit() else 0 31 | else: 32 | new_production = amount 33 | self._province.total_production += new_production - self._production 34 | self._production = new_production 35 | -------------------------------------------------------------------------------- /Chapter04/province.py: -------------------------------------------------------------------------------- 1 | from producer import Producer 2 | 3 | 4 | class Province: 5 | def __init__(self, doc: dict): 6 | self._name = doc["name"] 7 | self._producers = [] 8 | self._total_production = 0 9 | self._demand = doc["demand"] 10 | self._price = doc["price"] 11 | for producer_doc in doc["producers"]: 12 | self.add_producer(Producer(self, producer_doc)) 13 | 14 | def add_producer(self, producer: Producer): 15 | self._producers.append(producer) 16 | self._total_production += producer.production 17 | 18 | @property 19 | def name(self): 20 | return self._name 21 | 22 | @property 23 | def producers(self): 24 | return list(self._producers) # Shallow copy 25 | 26 | @property 27 | def total_production(self): 28 | return self._total_production 29 | 30 | @total_production.setter 31 | def total_production(self, value): 32 | self._total_production = value 33 | 34 | @property 35 | def demand(self): 36 | return self._demand 37 | 38 | @demand.setter 39 | def demand(self, value): 40 | self._demand = int(value) 41 | 42 | @property 43 | def price(self): 44 | return self._price 45 | 46 | @price.setter 47 | def price(self, value): 48 | self.price = int(value) 49 | 50 | @property 51 | def shortfall(self): 52 | return self._demand - self.total_production 53 | 54 | @property 55 | def profit(self): 56 | return self.demand_value - self.demand_cost 57 | 58 | @property 59 | def demand_value(self): 60 | return self.satisfied_demand * self.price 61 | 62 | @property 63 | def satisfied_demand(self): 64 | return min(self._demand, self.total_production) 65 | 66 | @property 67 | def demand_cost(self): 68 | remaining_demand = self.demand 69 | result = 0 70 | for producer in sorted(self.producers, key=lambda p: p.cost): 71 | contribution = min(remaining_demand, producer.production) 72 | remaining_demand -= contribution 73 | result += contribution * producer.cost 74 | return result 75 | 76 | 77 | def sample_province_data() -> dict: 78 | return { 79 | "name": "Asia", 80 | "producers": [ 81 | {"name": "Byzantium", "cost": 10, "production": 9}, 82 | {"name": "Attalia", "cost": 12, "production": 10}, 83 | {"name": "Sinope", "cost": 10, "production": 6}, 84 | ], 85 | "demand": 30, 86 | "price": 20, 87 | } 88 | -------------------------------------------------------------------------------- /Chapter04/test_province.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from province import Province, sample_province_data 4 | 5 | 6 | @pytest.fixture(scope="function") 7 | def asia(): 8 | return Province(sample_province_data()) 9 | 10 | 11 | @pytest.fixture(scope="function") 12 | def province_no_producer(): 13 | data = { 14 | "name": "No producers", 15 | "producers": [], 16 | "demand": 30, 17 | "price": 20, 18 | } 19 | return Province(data) 20 | 21 | 22 | def test_province_short_fall(asia): 23 | assert asia.shortfall == 5 24 | 25 | 26 | def test_province_profit(asia): 27 | assert asia.profit == 230 28 | 29 | 30 | def test_set_production_to_producer(asia): 31 | asia.producers[0].production = 20 32 | assert asia.shortfall == -6 33 | assert asia.profit == 292 34 | 35 | 36 | def test_province_no_producer_short_fall(province_no_producer): 37 | assert province_no_producer.shortfall == 30 38 | 39 | 40 | def test_province_no_producer_profit(province_no_producer): 41 | assert province_no_producer.profit == 0 42 | 43 | 44 | def test_province_zero_demand(asia): 45 | asia.demand = 0 46 | assert asia.shortfall == -25 47 | assert asia.profit == 0 48 | 49 | 50 | def test_province_negative_demand(asia): 51 | asia.demand = -1 52 | assert asia.shortfall == -26 53 | assert asia.profit == -10 54 | 55 | 56 | def test_province_empty_string_demand(asia): 57 | with pytest.raises(ValueError): 58 | asia.demand = "" 59 | 60 | 61 | def test_string_for_producers(): 62 | data = { 63 | "name": "String producers", 64 | "producers": "", 65 | "demand": 30, 66 | "price": 20, 67 | } 68 | province = Province(data) 69 | assert province.shortfall == 0 70 | -------------------------------------------------------------------------------- /Chapter06/01-extract-function/example.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | 3 | 4 | def print_owing(invoice): 5 | print_banner() 6 | outstanding = calculate_outstanding(invoice) 7 | record_due_date(invoice) 8 | print_details(invoice, outstanding) 9 | 10 | 11 | def print_banner(): 12 | print("**** Customer Outstanding Debt ****") 13 | 14 | 15 | def calculate_outstanding(invoice): 16 | result = 0 17 | for order in invoice.orders: 18 | result += order.amount 19 | return result 20 | 21 | 22 | def record_due_date(invoice): 23 | today = date.today() 24 | invoice.due_date = today + timedelta(days=30) 25 | 26 | 27 | def print_details(invoice, outstanding): 28 | print(f"Customer: {invoice.customer}") 29 | print(f"Outstanding Debt: {outstanding}") 30 | print(f"Due Date: {invoice.due_date.strftime('%d/%m/%y')}") 31 | -------------------------------------------------------------------------------- /Chapter06/01-extract-function/invoice.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | 4 | class Invoice: 5 | def __init__(self, customer): 6 | self._customer = customer 7 | self._orders = [] 8 | self._due_date = date.today() 9 | 10 | @property 11 | def customer(self): 12 | return self._customer 13 | 14 | @property 15 | def due_date(self): 16 | return self._due_date 17 | 18 | @due_date.setter 19 | def due_date(self, date): 20 | self._due_date = date 21 | 22 | @property 23 | def orders(self): 24 | return self._orders 25 | 26 | def add_order(self, order): 27 | self._orders.append(order) 28 | -------------------------------------------------------------------------------- /Chapter06/01-extract-function/order.py: -------------------------------------------------------------------------------- 1 | class Order: 2 | def __init__(self, amount): 3 | self._amount = amount 4 | 5 | @property 6 | def amount(self): 7 | return self._amount 8 | -------------------------------------------------------------------------------- /Chapter06/01-extract-function/test_example.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | 3 | import pytest 4 | from example import print_owing 5 | 6 | from invoice import Invoice 7 | from order import Order 8 | 9 | 10 | @pytest.fixture 11 | def invoice(): 12 | _invoice = Invoice("Minwoo Jeong") 13 | for amount in range(10, 60, 10): 14 | _invoice.add_order(Order(amount)) 15 | due_date = date.today() + timedelta(days=30) 16 | _invoice.due_date = due_date 17 | 18 | return _invoice 19 | 20 | 21 | def test_print_owing(capsys, invoice): 22 | print_owing(invoice) 23 | expected = ( 24 | "**** Customer Outstanding Debt ****\n" 25 | f"Customer: {invoice.customer}\n" 26 | "Outstanding Debt: 150\n" 27 | f"Due Date: {invoice.due_date.strftime('%d/%m/%y')}\n" 28 | ) 29 | captured = capsys.readouterr() 30 | assert captured.out == expected 31 | -------------------------------------------------------------------------------- /Chapter06/02-inline-function/customer.py: -------------------------------------------------------------------------------- 1 | class Customer: 2 | def __init__(self, name, location) -> None: 3 | self._name = name 4 | self._location = location 5 | 6 | @property 7 | def name(self): 8 | return self._name 9 | 10 | @property 11 | def location(self): 12 | return self._location 13 | -------------------------------------------------------------------------------- /Chapter06/02-inline-function/driver.py: -------------------------------------------------------------------------------- 1 | class Driver: 2 | def __init__(self) -> None: 3 | self._number_of_late_deliveries = 0 4 | 5 | @property 6 | def number_of_late_deliveries(self): 7 | return self._number_of_late_deliveries 8 | 9 | def do_late_delivery(self): 10 | self._number_of_late_deliveries += 1 11 | -------------------------------------------------------------------------------- /Chapter06/02-inline-function/example1.py: -------------------------------------------------------------------------------- 1 | def rating(driver): 2 | return 2 if driver.number_of_late_deliveries > 5 else 1 3 | -------------------------------------------------------------------------------- /Chapter06/02-inline-function/example2.py: -------------------------------------------------------------------------------- 1 | def rating(driver): 2 | return 2 if driver.number_of_late_deliveries > 5 else 1 3 | -------------------------------------------------------------------------------- /Chapter06/02-inline-function/example3.py: -------------------------------------------------------------------------------- 1 | def report_lines(customer): 2 | lines = [] 3 | lines.append(["name", customer.name]) 4 | lines.append(["location", customer.location]) 5 | return lines 6 | -------------------------------------------------------------------------------- /Chapter06/02-inline-function/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from example3 import report_lines 3 | 4 | from customer import Customer 5 | from driver import Driver 6 | from example1 import rating as rating1 7 | from example2 import rating as rating2 8 | 9 | 10 | def get_driver(num_late_delivery): 11 | driver = Driver() 12 | for _ in range(num_late_delivery): 13 | driver.do_late_delivery() 14 | return driver 15 | 16 | 17 | @pytest.fixture 18 | def customer(): 19 | return Customer("Minwoo Jeong", "South Korea") 20 | 21 | 22 | def test_rating1(): 23 | assert rating1(get_driver(10)) == 2 24 | assert rating1(get_driver(5)) == 1 25 | assert rating1(get_driver(1)) == 1 26 | 27 | 28 | def test_rating2(): 29 | assert rating2(get_driver(10)) == 2 30 | assert rating2(get_driver(5)) == 1 31 | assert rating2(get_driver(1)) == 1 32 | 33 | 34 | def test_report_lines(customer): 35 | assert report_lines(customer) == [ 36 | ["name", "Minwoo Jeong"], 37 | ["location", "South Korea"], 38 | ] 39 | -------------------------------------------------------------------------------- /Chapter06/03-extract-variable/order.py: -------------------------------------------------------------------------------- 1 | class Order: 2 | def __init__(self, quantity, item_price): 3 | self._quantity = quantity 4 | self._item_price = item_price 5 | 6 | @property 7 | def quantity(self): 8 | return self._quantity 9 | 10 | @property 11 | def item_price(self): 12 | return self._item_price 13 | 14 | @property 15 | def price(self): 16 | return self.base_price - self.quantity_discount + self.shipping 17 | 18 | @property 19 | def base_price(self): 20 | return self.quantity * self.item_price 21 | 22 | @property 23 | def quantity_discount(self): 24 | return max(0, self.quantity - 500) * self.item_price * 0.05 25 | 26 | @property 27 | def shipping(self): 28 | return min(self.base_price * 0.1, 100) 29 | 30 | -------------------------------------------------------------------------------- /Chapter06/03-extract-variable/price.py: -------------------------------------------------------------------------------- 1 | def price(order): 2 | base_price = order.quantity * order.item_price 3 | quantity_discount = max(0, order.quantity - 500) * order.item_price * 0.05 4 | shipping = min(base_price * 0.1, 100) 5 | return base_price - quantity_discount + shipping 6 | -------------------------------------------------------------------------------- /Chapter06/03-extract-variable/test_example.py: -------------------------------------------------------------------------------- 1 | from price import price 2 | 3 | from order import Order 4 | 5 | 6 | def test_price(): 7 | assert price(Order(2000, 1000)) == 1925100 8 | assert price(Order(600, 100)) == 59600 9 | 10 | 11 | def test_order_price(): 12 | assert Order(2000, 1000).price == 1925100 13 | assert Order(600, 100).price == 59600 14 | -------------------------------------------------------------------------------- /Chapter06/04-inline-variable/order.py: -------------------------------------------------------------------------------- 1 | class Order: 2 | def __init__(self, base_price): 3 | self._base_price = base_price 4 | 5 | def is_expensive(self): 6 | return self._base_price > 1000 7 | -------------------------------------------------------------------------------- /Chapter06/04-inline-variable/test_order.py: -------------------------------------------------------------------------------- 1 | from order import Order 2 | 3 | 4 | def test_order_is_expensive(): 5 | assert Order(1100).is_expensive() 6 | assert not Order(1000).is_expensive() 7 | assert not Order(500).is_expensive() 8 | 9 | -------------------------------------------------------------------------------- /Chapter06/05-change-function-declaration/address.py: -------------------------------------------------------------------------------- 1 | class Address: 2 | def __init__(self, state): 3 | self._state = state 4 | 5 | @property 6 | def state(self): 7 | return self._state 8 | -------------------------------------------------------------------------------- /Chapter06/05-change-function-declaration/book.py: -------------------------------------------------------------------------------- 1 | class Book: 2 | def __init__(self): 3 | self._reservations = [] 4 | 5 | def add_reservation(self, customer, is_priority): 6 | self._reservations.append(customer) 7 | 8 | @property 9 | def reservations(self): 10 | return list(self._reservations) 11 | -------------------------------------------------------------------------------- /Chapter06/05-change-function-declaration/circum.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def circum(radius): 5 | return 2 * math.pi * radius 6 | -------------------------------------------------------------------------------- /Chapter06/05-change-function-declaration/customer.py: -------------------------------------------------------------------------------- 1 | class Customer: 2 | def __init__(self, address): 3 | self._address = address 4 | 5 | @property 6 | def address(self): 7 | return self._address 8 | -------------------------------------------------------------------------------- /Chapter06/05-change-function-declaration/in_new_england.py: -------------------------------------------------------------------------------- 1 | def in_new_england(state_code): 2 | return state_code in ["MA", "CI", "ME", "VT", "NH", "RI"] 3 | -------------------------------------------------------------------------------- /Chapter06/05-change-function-declaration/test_example.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from address import Address 4 | from book import Book 5 | from circum import circum 6 | from customer import Customer 7 | from in_new_england import in_new_england 8 | 9 | 10 | def test_circum(): 11 | assert circum(3) == 2 * math.pi * 3 12 | 13 | 14 | def test_book_add_reservation(): 15 | book = Book() 16 | book.add_reservation("Minwoo Jeong", False) 17 | assert "Minwoo Jeong" in book.reservations 18 | 19 | 20 | def test_in_new_england(): 21 | customer_new_england = Customer(Address("MA")) 22 | customer_korea = Customer(Address("KO")) 23 | assert in_new_england(customer_new_england.address.state) 24 | assert not in_new_england(customer_korea.address.state) 25 | -------------------------------------------------------------------------------- /Chapter06/06-encapsulate-variable/default_owner.py: -------------------------------------------------------------------------------- 1 | _default_owner = {"first name": "Minwoo", "last name": "Jeong"} 2 | 3 | 4 | def default_owner(): 5 | return dict(_default_owner) 6 | 7 | 8 | def set_default_owner(owner): 9 | global _default_owner 10 | _default_owner = owner 11 | -------------------------------------------------------------------------------- /Chapter06/06-encapsulate-variable/test_default_owner.py: -------------------------------------------------------------------------------- 1 | from default_owner import default_owner, set_default_owner 2 | 3 | 4 | def test_get_default_owner(): 5 | spaceship_owner = default_owner() 6 | assert spaceship_owner["first name"] == "Minwoo" 7 | assert spaceship_owner["last name"] == "Jeong" 8 | 9 | 10 | def test_set_default_owner(): 11 | set_default_owner({"first name": "Rebecca", "last name": "Parsons"}) 12 | owner = default_owner() 13 | assert owner["first name"] == "Rebecca" 14 | assert owner["last name"] == "Parsons" 15 | 16 | 17 | def test_change_field_of_default_owner(): 18 | owner1 = default_owner() 19 | owner2 = default_owner() 20 | owner2["first name"] = "Minho" 21 | assert owner1["first name"] != "Minho" 22 | -------------------------------------------------------------------------------- /Chapter06/07-rename-variable/company_name.py: -------------------------------------------------------------------------------- 1 | _company_name = "Samsung" 2 | 3 | 4 | def company_name(): 5 | return _company_name 6 | 7 | 8 | def print_company_name(): 9 | print(_company_name, end="") 10 | -------------------------------------------------------------------------------- /Chapter06/07-rename-variable/test_example.py: -------------------------------------------------------------------------------- 1 | import company_name 2 | import title 3 | 4 | 5 | def test_read_title(): 6 | result = title.title() 7 | assert result == "Untitled" 8 | 9 | 10 | def test_set_title(): 11 | title.set_title("Hello") 12 | assert title.title() == "Hello" 13 | 14 | 15 | def test_company_name(): 16 | assert company_name.company_name() == "Samsung" 17 | 18 | 19 | def test_print_company_name(capsys): 20 | company_name.print_company_name() 21 | captured = capsys.readouterr() 22 | assert captured.out == "Samsung" 23 | -------------------------------------------------------------------------------- /Chapter06/07-rename-variable/title.py: -------------------------------------------------------------------------------- 1 | _title = "Untitled" 2 | 3 | 4 | def title(): 5 | return _title 6 | 7 | 8 | def set_title(title): 9 | global _title 10 | _title = title 11 | -------------------------------------------------------------------------------- /Chapter06/08-introduce-parameter-object/example.py: -------------------------------------------------------------------------------- 1 | def readings_outside_range(station, range): 2 | return [ 3 | reading 4 | for reading in station["readings"] 5 | if not range.contains(reading["temp"]) 6 | ] 7 | -------------------------------------------------------------------------------- /Chapter06/08-introduce-parameter-object/number_range.py: -------------------------------------------------------------------------------- 1 | class NumberRange: 2 | def __init__(self, min, max): 3 | self._data = {"min": min, "max": max} 4 | 5 | @property 6 | def min(self): 7 | return self._data["min"] 8 | 9 | @property 10 | def max(self): 11 | return self._data["max"] 12 | 13 | def contains(self, value): 14 | return self.min <= value <= self.max 15 | -------------------------------------------------------------------------------- /Chapter06/08-introduce-parameter-object/operating_plan.py: -------------------------------------------------------------------------------- 1 | class OperatingPlan: 2 | def __init__(self, temperature_floor, temperature_ceiling): 3 | self._temperature_floor = temperature_floor 4 | self._temperature_ceiling = temperature_ceiling 5 | 6 | @property 7 | def temperature_floor(self): 8 | return self._temperature_floor 9 | 10 | @property 11 | def temperature_ceiling(self): 12 | return self._temperature_ceiling 13 | -------------------------------------------------------------------------------- /Chapter06/08-introduce-parameter-object/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from example import readings_outside_range 3 | 4 | from operating_plan import OperatingPlan 5 | from number_range import NumberRange 6 | 7 | 8 | @pytest.fixture 9 | def station(): 10 | return { 11 | "name": "ZB1", 12 | "readings": [ 13 | {"temp": 47, "time": "2016-11-10 09:10"}, 14 | {"temp": 53, "time": "2016-11-10 09:20"}, 15 | {"temp": 58, "time": "2016-11-10 09:30"}, 16 | {"temp": 53, "time": "2016-11-10 09:40"}, 17 | {"temp": 51, "time": "2016-11-10 09:50"}, 18 | ], 19 | } 20 | 21 | 22 | @pytest.fixture 23 | def operating_plan(): 24 | return OperatingPlan(50, 55) 25 | 26 | 27 | def test_readings_outside_range(station, operating_plan): 28 | range = NumberRange( 29 | operating_plan.temperature_floor, operating_plan.temperature_ceiling 30 | ) 31 | readings = readings_outside_range(station, range) 32 | temperatures = {reading["temp"] for reading in readings} 33 | assert len(temperatures) == 2 34 | assert 47 in temperatures 35 | assert 58 in temperatures 36 | -------------------------------------------------------------------------------- /Chapter06/09-combine-functions-into-class/client1.py: -------------------------------------------------------------------------------- 1 | from reading import Reading, acquire_reading 2 | 3 | reading_data = acquire_reading() 4 | reading = Reading(reading_data) 5 | base_charge = reading.base_charge 6 | -------------------------------------------------------------------------------- /Chapter06/09-combine-functions-into-class/client2.py: -------------------------------------------------------------------------------- 1 | from reading import acquire_reading, Reading 2 | 3 | reading_data = acquire_reading() 4 | reading = Reading(reading_data) 5 | taxable_charge = reading.taxable_charge 6 | -------------------------------------------------------------------------------- /Chapter06/09-combine-functions-into-class/client3.py: -------------------------------------------------------------------------------- 1 | from reading import Reading, acquire_reading 2 | 3 | 4 | reading_data = acquire_reading() 5 | reading = Reading(reading_data) 6 | basic_charge_amount = reading.base_charge 7 | -------------------------------------------------------------------------------- /Chapter06/09-combine-functions-into-class/dummy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy functions for the client examples. 3 | There are no definitions of these functions in the book. 4 | """ 5 | 6 | 7 | def base_rate(month, year): 8 | return 1000 9 | 10 | 11 | def tax_threshold(year): 12 | return 5000 13 | -------------------------------------------------------------------------------- /Chapter06/09-combine-functions-into-class/reading.py: -------------------------------------------------------------------------------- 1 | from dummy import base_rate, tax_threshold 2 | 3 | _reading = {"customer": "Minwoo", "quantity": 10, "month": 5, "year": 2017} 4 | 5 | 6 | class Reading: 7 | def __init__(self, data): 8 | self._customer = data["customer"] 9 | self._quantity = data["quantity"] 10 | self._month = data["month"] 11 | self._year = data["year"] 12 | 13 | @property 14 | def customer(self): 15 | return self._customer 16 | 17 | @property 18 | def quantity(self): 19 | return self._quantity 20 | 21 | @property 22 | def month(self): 23 | return self._month 24 | 25 | @property 26 | def year(self): 27 | return self._year 28 | 29 | @property 30 | def base_charge(self): 31 | return base_rate(self.month, self.year) * self.quantity 32 | 33 | @property 34 | def taxable_charge(self): 35 | return max(0, self.base_charge - tax_threshold(self.year)) 36 | 37 | 38 | def acquire_reading(): 39 | return dict(_reading) 40 | 41 | -------------------------------------------------------------------------------- /Chapter06/09-combine-functions-into-class/test_client.py: -------------------------------------------------------------------------------- 1 | from client1 import base_charge 2 | from client2 import taxable_charge 3 | from client3 import basic_charge_amount 4 | 5 | 6 | def test_base_charge(): 7 | global base_charge 8 | assert base_charge == 10000 9 | 10 | 11 | def test_taxable_charge(): 12 | global taxable_charge 13 | assert taxable_charge == 5000 14 | 15 | 16 | def test_basic_charge_amount(): 17 | global basic_charge_amount 18 | assert basic_charge_amount == 10000 19 | -------------------------------------------------------------------------------- /Chapter06/10-combine-functions-into-transform/client1.py: -------------------------------------------------------------------------------- 1 | from reading import acquire_reading, enrich_reading 2 | 3 | raw_reading = acquire_reading() 4 | reading = enrich_reading(raw_reading) 5 | base_charge = reading["base charge"] 6 | -------------------------------------------------------------------------------- /Chapter06/10-combine-functions-into-transform/client2.py: -------------------------------------------------------------------------------- 1 | from dummy import tax_threshold 2 | from reading import acquire_reading, enrich_reading 3 | 4 | raw_reading = acquire_reading() 5 | reading = enrich_reading(raw_reading) 6 | taxable_charge = reading["taxable charge"] 7 | -------------------------------------------------------------------------------- /Chapter06/10-combine-functions-into-transform/client3.py: -------------------------------------------------------------------------------- 1 | from reading import acquire_reading, enrich_reading 2 | 3 | 4 | raw_reading = acquire_reading() 5 | reading = enrich_reading(raw_reading) 6 | basic_charge_amount = reading["base charge"] 7 | -------------------------------------------------------------------------------- /Chapter06/10-combine-functions-into-transform/dummy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy functions for the client examples. 3 | There are no definitions of these functions in the book. 4 | """ 5 | 6 | 7 | def base_rate(month, year): 8 | return 1000 9 | 10 | 11 | def tax_threshold(year): 12 | return 5000 13 | -------------------------------------------------------------------------------- /Chapter06/10-combine-functions-into-transform/reading.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from dummy import base_rate, tax_threshold 4 | 5 | _reading = {"customer": "Minwoo", "quantity": 10, "month": 5, "year": 2017} 6 | 7 | 8 | def acquire_reading(): 9 | return dict(_reading) 10 | 11 | 12 | def enrich_reading(original): 13 | result = deepcopy(original) 14 | result["base charge"] = calculate_base_charge(result) 15 | result["taxable charge"] = max( 16 | 0, result["base charge"] - tax_threshold(result["year"]) 17 | ) 18 | return result 19 | 20 | 21 | def calculate_base_charge(reading): 22 | return base_rate(reading["month"], reading["year"]) * reading["quantity"] 23 | -------------------------------------------------------------------------------- /Chapter06/10-combine-functions-into-transform/test_example.py: -------------------------------------------------------------------------------- 1 | from client1 import base_charge 2 | from client2 import taxable_charge 3 | from client3 import basic_charge_amount 4 | from reading import enrich_reading 5 | 6 | 7 | def test_base_charge(): 8 | global base_charge 9 | assert base_charge == 10000 10 | 11 | 12 | def test_taxable_charge(): 13 | global taxable_charge 14 | assert taxable_charge == 5000 15 | 16 | 17 | def test_basic_charge_amount(): 18 | global basic_charge_amount 19 | assert basic_charge_amount == 10000 20 | 21 | 22 | def test_enrich_reading_returns_another(): 23 | original = {"customer": "Minwoo", "quantity": 10, "month": 5, "year": 2017} 24 | result = enrich_reading(original) 25 | assert original != result 26 | -------------------------------------------------------------------------------- /Chapter06/11-split-phase/price_data.py: -------------------------------------------------------------------------------- 1 | class PriceData: 2 | def __init__(self, product, quantity) -> None: 3 | self._product = product 4 | self._quantity = quantity 5 | 6 | @property 7 | def quantity(self): 8 | return self._quantity 9 | 10 | @property 11 | def base_price(self): 12 | return self._product.base_price * self._quantity 13 | 14 | @property 15 | def discount(self): 16 | return ( 17 | max(self._quantity - self._product.discount_threshold, 0) 18 | * self._product.base_price 19 | * self._product.discount_rate 20 | ) 21 | -------------------------------------------------------------------------------- /Chapter06/11-split-phase/price_order.py: -------------------------------------------------------------------------------- 1 | from price_data import PriceData 2 | 3 | 4 | def price_order(product, quantity, shipping_method): 5 | return apply_shipping(PriceData(product, quantity), shipping_method) 6 | 7 | 8 | def apply_shipping(price_data, shipping_method): 9 | shipping_per_case = ( 10 | shipping_method.discounted_fee 11 | if price_data.base_price > shipping_method.discount_threshold 12 | else shipping_method.fee_per_case 13 | ) 14 | shipping_cost = price_data.quantity * shipping_per_case 15 | price = price_data.base_price - price_data.discount + shipping_cost 16 | return price 17 | -------------------------------------------------------------------------------- /Chapter06/11-split-phase/product.py: -------------------------------------------------------------------------------- 1 | class Product: 2 | def __init__(self, base_price, discount_threshold, discount_rate) -> None: 3 | self._base_price = base_price 4 | self._discount_threshold = discount_threshold 5 | self._discount_rate = discount_rate 6 | 7 | @property 8 | def base_price(self): 9 | return self._base_price 10 | 11 | @property 12 | def discount_threshold(self): 13 | return self._discount_threshold 14 | 15 | @property 16 | def discount_rate(self): 17 | return self._discount_rate 18 | -------------------------------------------------------------------------------- /Chapter06/11-split-phase/shipping_method.py: -------------------------------------------------------------------------------- 1 | class ShippingMethod: 2 | def __init__( 3 | self, discount_threshold, discounted_fee, fee_per_case 4 | ) -> None: 5 | self._discount_threshold = discount_threshold 6 | self._discounted_fee = discounted_fee 7 | self._fee_per_case = fee_per_case 8 | 9 | @property 10 | def discount_threshold(self): 11 | return self._discount_threshold 12 | 13 | @property 14 | def discounted_fee(self): 15 | return self._discounted_fee 16 | 17 | @property 18 | def fee_per_case(self): 19 | return self._fee_per_case 20 | -------------------------------------------------------------------------------- /Chapter06/11-split-phase/test_price_order.py: -------------------------------------------------------------------------------- 1 | from price_order import price_order 2 | from product import Product 3 | from shipping_method import ShippingMethod 4 | 5 | 6 | def test_price_order(): 7 | assert ( 8 | price_order( 9 | Product(1000, 2500, 0.05), 100, ShippingMethod(5000, 1000, 2500) 10 | ) 11 | == 200000 12 | ) 13 | assert ( 14 | price_order( 15 | Product(1500, 4500, 0.02), 25, ShippingMethod(5500, 1200, 1500) 16 | ) 17 | == 67500 18 | ) 19 | -------------------------------------------------------------------------------- /Chapter06/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 6: A First Set of Refactorings 2 | 3 | ## Contents 4 | 5 | 1. Extract Function 6 | 2. Inline Function 7 | 3. Extract Variable 8 | 4. Inline Variable 9 | 5. Change Function Declaration 10 | 6. Encapsulate Variable 11 | 7. Rename Variable 12 | 8. Introduce Parameter Object 13 | 9. Combine Functions into Class 14 | 10. Combine Functions into Transform 15 | 11. Split Phase 16 | -------------------------------------------------------------------------------- /Chapter07/01-encapsulate-record/customer_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "1995": { 3 | "name": "Minwoo Jeong", 4 | "id": "1995", 5 | "usages": { 6 | "2016": { 7 | "1": 50, 8 | "2": 55 9 | }, 10 | "2015": { 11 | "1": 70, 12 | "2": 63 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Chapter07/01-encapsulate-record/customer_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | from copy import deepcopy 3 | 4 | 5 | class CustomerData: 6 | def __init__(self, data) -> None: 7 | self._data = data 8 | 9 | def usage(self, customer_id, year, month): 10 | return self._data[customer_id]["usages"][year][month] 11 | 12 | def set_usage(self, customer_id, year, month, amount): 13 | self._data[customer_id]["usages"][year][month] = amount 14 | 15 | @property 16 | def raw_data(self): 17 | return deepcopy(self._data) 18 | 19 | 20 | with open("customer_data.json") as infile: 21 | customer_data = CustomerData(json.load(infile)) 22 | 23 | 24 | def get_customer_data(): 25 | return customer_data 26 | 27 | 28 | def set_raw_data_of_customers(new_data): 29 | global customer_data 30 | customer_data = CustomerData(new_data) 31 | -------------------------------------------------------------------------------- /Chapter07/01-encapsulate-record/example1.py: -------------------------------------------------------------------------------- 1 | from organization import get_organization 2 | 3 | 4 | def get_org_name_as_header(): 5 | return f"{person.name}
") 4 | result.append(render_photo(person.photo)) 5 | result.append(emit_photo_data(person.photo)) 6 | return "\n".join(result) 7 | 8 | 9 | def render_photo(photo): 10 | return f"(Rendered HTML of the photo '{photo.title}')
" 11 | 12 | 13 | def photo_div(photo): 14 | return "\n".join(["Title: {photo.title}
", 21 | f"Location: {photo.location}
", 22 | f"Date: {photo.date}
", 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Chapter08/03-move-statements-into-function/person.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, name, photo) -> None: 3 | self._name = name 4 | self._photo = photo 5 | 6 | @property 7 | def name(self): 8 | return self._name 9 | 10 | @property 11 | def photo(self): 12 | return self._photo 13 | -------------------------------------------------------------------------------- /Chapter08/03-move-statements-into-function/photo.py: -------------------------------------------------------------------------------- 1 | class Photo: 2 | def __init__(self, title, location, date) -> None: 3 | self._title = title 4 | self._location = location 5 | self._date = date 6 | 7 | @property 8 | def title(self): 9 | return self._title 10 | 11 | @property 12 | def location(self): 13 | return self._location 14 | 15 | @property 16 | def date(self): 17 | return self._date 18 | 19 | -------------------------------------------------------------------------------- /Chapter08/03-move-statements-into-function/test_example.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | from example import photo_div, render_person 6 | from person import Person 7 | from photo import Photo 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def photo(): 12 | return Photo("Test", "Seoul", datetime(2022, 1, 2, 10, 30, 54)) 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def person(photo): 17 | return Person("Minwoo Jeong", photo) 18 | 19 | 20 | def test_photo_div(photo): 21 | assert photo_div(photo) == "\n".join( 22 | [ 23 | "Title: Test
", 25 | "Location: Seoul
", 26 | "Date: 2022-01-02 10:30:54
", 27 | "Minwoo Jeong
", 36 | "(Rendered HTML of the photo 'Test')
", 37 | "Title: Test
", 38 | "Location: Seoul
", 39 | "Date: 2022-01-02 10:30:54
", 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /Chapter08/04-move-statements-to-callers/example.py: -------------------------------------------------------------------------------- 1 | def render_person(person): 2 | result = [] 3 | result.append(f"{person.name}
") 4 | result.append(render_photo(person.photo)) 5 | result.append(emit_photo_data(person.photo)) 6 | result.append(f"Location: {person.photo.location}
") 7 | return "\n".join(result) 8 | 9 | 10 | def render_photo(photo): 11 | return f"(Rendered HTML of the photo '{photo.title}')
" 12 | 13 | 14 | def photo_div(photo): 15 | return "\n".join( 16 | [ 17 | "Location: {photo.location}
", 20 | "Title: {photo.title}
", f"Date: {photo.date}
",] 28 | ) 29 | -------------------------------------------------------------------------------- /Chapter08/04-move-statements-to-callers/person.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, name, photo) -> None: 3 | self._name = name 4 | self._photo = photo 5 | 6 | @property 7 | def name(self): 8 | return self._name 9 | 10 | @property 11 | def photo(self): 12 | return self._photo 13 | -------------------------------------------------------------------------------- /Chapter08/04-move-statements-to-callers/photo.py: -------------------------------------------------------------------------------- 1 | class Photo: 2 | def __init__(self, title, location, date) -> None: 3 | self._title = title 4 | self._location = location 5 | self._date = date 6 | 7 | @property 8 | def title(self): 9 | return self._title 10 | 11 | @property 12 | def location(self): 13 | return self._location 14 | 15 | @property 16 | def date(self): 17 | return self._date 18 | 19 | -------------------------------------------------------------------------------- /Chapter08/04-move-statements-to-callers/test_example.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | from example import photo_div, render_person 6 | from person import Person 7 | from photo import Photo 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def photo(): 12 | return Photo("Test", "Seoul", datetime(2022, 1, 2, 10, 30, 54)) 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def person(photo): 17 | return Person("Minwoo Jeong", photo) 18 | 19 | 20 | def test_photo_div(photo): 21 | assert photo_div(photo) == "\n".join( 22 | [ 23 | "Title: Test
", 25 | "Date: 2022-01-02 10:30:54
", 26 | "Location: Seoul
", 27 | "Minwoo Jeong
", 36 | "(Rendered HTML of the photo 'Test')
", 37 | "Title: Test
", 38 | "Date: 2022-01-02 10:30:54
", 39 | "Location: Seoul
", 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /Chapter08/07-split-loop/example.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | 4 | def get_youngest_and_total_salary(people): 5 | return ( 6 | f"Youngest: {get_youngest_age(people)}, " 7 | f"Total Salary: {get_total_salary(people)}" 8 | ) 9 | 10 | 11 | def get_youngest_age(people): 12 | return min(map(lambda p: p.age, people)) 13 | 14 | 15 | def get_total_salary(people): 16 | return reduce(lambda total, p: total + p.salary, people, 0) 17 | -------------------------------------------------------------------------------- /Chapter08/07-split-loop/person.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, age, salary) -> None: 3 | self._age = age 4 | self._salary = salary 5 | 6 | @property 7 | def age(self): 8 | return self._age 9 | 10 | @property 11 | def salary(self): 12 | return self._salary 13 | -------------------------------------------------------------------------------- /Chapter08/07-split-loop/test_example.py: -------------------------------------------------------------------------------- 1 | from example import get_youngest_and_total_salary 2 | from person import Person 3 | 4 | 5 | def test_get_youngest_and_total_salary(): 6 | people = [Person(17, 2400), Person(32, 3750), Person(12, 10000)] 7 | assert ( 8 | get_youngest_and_total_salary(people) 9 | == "Youngest: 12, Total Salary: 16150" 10 | ) 11 | -------------------------------------------------------------------------------- /Chapter08/08-replace-loop-with-pipeline/example.py: -------------------------------------------------------------------------------- 1 | def acquire_data(input): 2 | lines = input.split("\n") 3 | result = list( 4 | map( 5 | lambda record: { 6 | "city": record[0].strip(), 7 | "phone": record[2].strip(), 8 | }, 9 | filter( 10 | lambda record: record[1].strip() == "India", 11 | map( 12 | lambda line: line.split(","), 13 | filter(lambda line: line.strip() != "", lines[1:]), 14 | ), 15 | ), 16 | ) 17 | ) 18 | 19 | return result 20 | -------------------------------------------------------------------------------- /Chapter08/08-replace-loop-with-pipeline/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from example import acquire_data 4 | 5 | 6 | @pytest.fixture 7 | def csv(): 8 | return "\n".join( 9 | [ 10 | "office, country, telephone", 11 | "Chicago, USA, +1 312 373 1000", 12 | "Beijing, China, +86 4008 900 505", 13 | "Bangalore, India, +91 80 4064 9570", 14 | "Porto Alegre, Brazil, +55 51 3079 3550", 15 | "Chennai, India, +91 44 660 44766", 16 | ] 17 | ) 18 | 19 | 20 | def test_acquire_data(csv): 21 | assert acquire_data(csv) == [ 22 | {"city": "Bangalore", "phone": "+91 80 4064 9570"}, 23 | {"city": "Chennai", "phone": "+91 44 660 44766"}, 24 | ] 25 | -------------------------------------------------------------------------------- /Chapter08/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 8: Moving Features 2 | 3 | ## Contents 4 | 5 | 1. Move Function 6 | 2. Move Field 7 | 3. Move Statements into Function 8 | 4. Move Statements to Callers 9 | 5. Replace Inline Code with Function Call 10 | 6. Slide Statements 11 | 7. Split Loop 12 | 8. Replace Loop with Pipeline 13 | 9. Remove Dead Code 14 | -------------------------------------------------------------------------------- /Chapter09/01-split-variable/example.py: -------------------------------------------------------------------------------- 1 | def distance_travelled(scenario, time): 2 | primary_acceleration = scenario.primary_force / scenario.mass # a = F / m 3 | primary_time = min(time, scenario.delay) 4 | result = 0.5 * primary_acceleration * primary_time * primary_time 5 | secondary_time = time - scenario.delay 6 | if secondary_time > 0: 7 | primary_velocity = primary_acceleration * scenario.delay 8 | secondary_acceleration = ( 9 | scenario.primary_force + scenario.secondary_force 10 | ) / scenario.mass 11 | result += ( 12 | primary_velocity * secondary_time 13 | + 0.5 * secondary_acceleration * secondary_time * secondary_time 14 | ) 15 | return result 16 | 17 | 18 | def discount(input_value, quantity): 19 | result = input_value 20 | if result > 50: 21 | result = result - 2 22 | if quantity > 100: 23 | result = result - 1 24 | return result 25 | -------------------------------------------------------------------------------- /Chapter09/01-split-variable/scenario.py: -------------------------------------------------------------------------------- 1 | class Scenario: 2 | def __init__(self, delay, primary_force, secondary_force, mass) -> None: 3 | self._delay = delay 4 | self._primary_force = primary_force 5 | self._secondary_force = secondary_force 6 | self._mass = mass 7 | 8 | @property 9 | def delay(self): 10 | return self._delay 11 | 12 | @property 13 | def primary_force(self): 14 | return self._primary_force 15 | 16 | @property 17 | def secondary_force(self): 18 | return self._secondary_force 19 | 20 | @property 21 | def mass(self): 22 | return self._mass 23 | -------------------------------------------------------------------------------- /Chapter09/01-split-variable/test_example.py: -------------------------------------------------------------------------------- 1 | from example import discount, distance_travelled 2 | from scenario import Scenario 3 | 4 | 5 | def test_distance_travelled(): 6 | assert distance_travelled(Scenario(10, 5, 2, 10), 5) == 6.25 7 | assert distance_travelled(Scenario(30, 51, 26, 12), 17) == 614.125 8 | assert distance_travelled(Scenario(50, 121, 56, 27), 100) == 25000 9 | 10 | 11 | def test_discount(): 12 | assert discount(100, 10) == 98 13 | assert discount(221, 102) == 218 14 | assert discount(42, 7) == 42 15 | -------------------------------------------------------------------------------- /Chapter09/02-rename-field/example.py: -------------------------------------------------------------------------------- 1 | from organization import organization 2 | 3 | 4 | def get_organization_info(): 5 | return f"{organization.title} ({organization.country})" 6 | -------------------------------------------------------------------------------- /Chapter09/02-rename-field/organization.py: -------------------------------------------------------------------------------- 1 | class Organization: 2 | def __init__(self, data): 3 | self._title = data["title"] 4 | self._country = data["country"] 5 | 6 | @property 7 | def title(self): 8 | return self._title 9 | 10 | @title.setter 11 | def title(self, new_name): 12 | self._title = new_name 13 | 14 | @property 15 | def country(self): 16 | return self._country 17 | 18 | @country.setter 19 | def country(self, new_country): 20 | self._country = new_country 21 | 22 | 23 | organization = Organization({"title": "Samsung", "country": "KO"}) 24 | -------------------------------------------------------------------------------- /Chapter09/02-rename-field/test_example.py: -------------------------------------------------------------------------------- 1 | from example import get_organization_info 2 | 3 | 4 | def test_get_organization_info(): 5 | assert get_organization_info() == "Samsung (KO)" 6 | -------------------------------------------------------------------------------- /Chapter09/03-replace-derived-variable-with-query/adjustment.py: -------------------------------------------------------------------------------- 1 | class Adjustment: 2 | def __init__(self, amount): 3 | self._amount = amount 4 | 5 | @property 6 | def amount(self): 7 | return self._amount 8 | -------------------------------------------------------------------------------- /Chapter09/03-replace-derived-variable-with-query/production_plan.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | 4 | class ProductionPlan: 5 | def __init__(self): 6 | self._adjustments = [] 7 | 8 | @property 9 | def production(self): 10 | return reduce(lambda sum, a: sum + a.amount, self._adjustments, 0) 11 | 12 | def apply_adjustment(self, adjustment): 13 | self._adjustments.append(adjustment) 14 | -------------------------------------------------------------------------------- /Chapter09/03-replace-derived-variable-with-query/production_plan2.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | 4 | class ProductionPlan: 5 | def __init__(self, production): 6 | self._initial_production = production 7 | self._adjustments = [] 8 | 9 | @property 10 | def production(self): 11 | return self._initial_production + self.calculated_production_accumulator 12 | 13 | @property 14 | def calculated_production_accumulator(self): 15 | return reduce(lambda sum, a: sum + a.amount, self._adjustments, 0) 16 | 17 | def apply_adjustment(self, adjustment): 18 | self._adjustments.append(adjustment) 19 | -------------------------------------------------------------------------------- /Chapter09/03-replace-derived-variable-with-query/test_example.py: -------------------------------------------------------------------------------- 1 | from adjustment import Adjustment 2 | from production_plan import ProductionPlan 3 | from production_plan2 import ProductionPlan as ProductionPlan2 4 | 5 | 6 | def test_production(): 7 | production_plan = ProductionPlan() 8 | production_plan.apply_adjustment(Adjustment(100)) 9 | production_plan.apply_adjustment(Adjustment(250)) 10 | production_plan.apply_adjustment(Adjustment(300)) 11 | assert production_plan.production == 650 12 | 13 | 14 | def test_production2(): 15 | production_plan = ProductionPlan2(200) 16 | production_plan.apply_adjustment(Adjustment(100)) 17 | production_plan.apply_adjustment(Adjustment(250)) 18 | production_plan.apply_adjustment(Adjustment(300)) 19 | assert production_plan.production == 850 20 | -------------------------------------------------------------------------------- /Chapter09/04-change-reference-to-value/person.py: -------------------------------------------------------------------------------- 1 | from telephone_number import TelephoneNumber 2 | 3 | 4 | class Person: 5 | def __init__(self): 6 | self._telephone_number = TelephoneNumber(82, 123456789) 7 | 8 | @property 9 | def office_area_code(self): 10 | return self._telephone_number.area_code 11 | 12 | @office_area_code.setter 13 | def office_area_code(self, arg): 14 | self._telephone_number = TelephoneNumber(arg, self.office_number) 15 | 16 | @property 17 | def office_number(self): 18 | return self._telephone_number.number 19 | 20 | @office_number.setter 21 | def office_number(self, arg): 22 | self._telephone_number = TelephoneNumber(self.office_area_code, arg) 23 | -------------------------------------------------------------------------------- /Chapter09/04-change-reference-to-value/telephone_number.py: -------------------------------------------------------------------------------- 1 | class TelephoneNumber: 2 | def __init__(self, area_code, number): 3 | self._area_code = area_code 4 | self._number = number 5 | 6 | def __eq__(self, o): 7 | return (self.area_code == o.area_code) and (self.number == o.number) 8 | 9 | @property 10 | def area_code(self): 11 | return self._area_code 12 | 13 | @property 14 | def number(self): 15 | return self._number 16 | -------------------------------------------------------------------------------- /Chapter09/04-change-reference-to-value/test_example.py: -------------------------------------------------------------------------------- 1 | from person import Person 2 | from telephone_number import TelephoneNumber 3 | 4 | 5 | def test_office_area_code(): 6 | person = Person() 7 | assert person.office_area_code == 82 8 | person.office_area_code = 100 9 | assert person.office_area_code == 100 10 | 11 | 12 | def test_office_number(): 13 | person = Person() 14 | assert person.office_number == 123456789 15 | person.office_number = 987654321 16 | assert person.office_number == 987654321 17 | 18 | 19 | def test_telephone_eq(): 20 | assert TelephoneNumber(82, 123456789) == TelephoneNumber(82, 123456789) 21 | -------------------------------------------------------------------------------- /Chapter09/05-change-value-to_reference/customer.py: -------------------------------------------------------------------------------- 1 | class Customer: 2 | repository = {} 3 | 4 | def __init__(self, id): 5 | self._id = id 6 | self._name = "" 7 | 8 | @property 9 | def id(self): 10 | return self._id 11 | 12 | @property 13 | def name(self): 14 | return self._name 15 | 16 | @name.setter 17 | def name(self, arg): 18 | self._name = arg 19 | 20 | @staticmethod 21 | def register_customer(id): 22 | if id not in Customer.repository: 23 | Customer.repository[id] = Customer(id) 24 | return Customer.repository.get(id) 25 | -------------------------------------------------------------------------------- /Chapter09/05-change-value-to_reference/order.py: -------------------------------------------------------------------------------- 1 | from customer import Customer 2 | 3 | 4 | class Order: 5 | def __init__(self, data): 6 | self._number = data["number"] 7 | self._customer = Customer.register_customer(data["customer"]) 8 | 9 | @property 10 | def customer(self): 11 | return self._customer 12 | -------------------------------------------------------------------------------- /Chapter09/05-change-value-to_reference/test_order.py: -------------------------------------------------------------------------------- 1 | from order import Order 2 | from functools import reduce 3 | 4 | 5 | def test_customer_change_name(): 6 | order_records = [ 7 | {"number": 1, "customer": 1}, 8 | {"number": 2, "customer": 1}, 9 | {"number": 3, "customer": 1}, 10 | ] 11 | orders = [Order(data) for data in order_records] 12 | customers = [order.customer for order in orders] 13 | customers[0].name = "Minwoo Jeong" 14 | assert customers[0].name == customers[1].name == customers[2].name 15 | -------------------------------------------------------------------------------- /Chapter09/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 9: Organizing Data 2 | 3 | ## Contents 4 | 5 | 1. Split Variable 6 | 2. Rename Field 7 | 3. Replace Derived Variable with Query 8 | 4. Change Reference to Value 9 | 5. Change Value to Reference 10 | -------------------------------------------------------------------------------- /Chapter10/01-decompose-conditional/example.py: -------------------------------------------------------------------------------- 1 | def get_charge(quantity, date, plan): 2 | charge = ( 3 | get_summer_charge(quantity, plan) 4 | if is_summer(date, plan) 5 | else get_regular_charge(quantity, plan) 6 | ) 7 | return charge 8 | 9 | 10 | def is_summer(date, plan): 11 | return not date < plan.summer_start and not date > plan.summer_end 12 | 13 | 14 | def get_summer_charge(quantity, plan): 15 | return quantity * plan.summer_rate 16 | 17 | 18 | def get_regular_charge(quantity, plan): 19 | return quantity * plan.regular_rate + plan.regular_service_charge 20 | -------------------------------------------------------------------------------- /Chapter10/01-decompose-conditional/plan.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class Plan: 5 | def __init__(self, data) -> None: 6 | self._summer_start = datetime.strptime( 7 | data.get("summer_start"), "%Y-%m-%d" 8 | ) 9 | self._summer_end = datetime.strptime(data.get("summer_end"), "%Y-%m-%d") 10 | self._summer_rate = data.get("summer_rate") 11 | self._regular_rate = data.get("regular_rate") 12 | self._regular_service_charge = data.get("regular_service_charge") 13 | 14 | @property 15 | def summer_start(self): 16 | return self._summer_start 17 | 18 | @property 19 | def summer_end(self): 20 | return self._summer_end 21 | 22 | @property 23 | def summer_rate(self): 24 | return self._summer_rate 25 | 26 | @property 27 | def regular_rate(self): 28 | return self._regular_rate 29 | 30 | @property 31 | def regular_service_charge(self): 32 | return self._regular_service_charge 33 | -------------------------------------------------------------------------------- /Chapter10/01-decompose-conditional/test_example.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | from example import get_charge 6 | from plan import Plan 7 | 8 | 9 | @pytest.fixture 10 | def plan(): 11 | return Plan( 12 | { 13 | "summer_start": "2021-07-01", 14 | "summer_end": "2021-09-30", 15 | "summer_rate": 0.2, 16 | "regular_rate": 0.1, 17 | "regular_service_charge": 10000, 18 | } 19 | ) 20 | 21 | 22 | def test_get_charge(plan): 23 | summer_date = datetime(2021, 8, 15) 24 | assert get_charge(1000, summer_date, plan) == 200 25 | no_summer_date = datetime(2021, 2, 17) 26 | assert get_charge(1000, no_summer_date, plan) == 10100 27 | 28 | -------------------------------------------------------------------------------- /Chapter10/02-consolidate-conditional-expression/employee.py: -------------------------------------------------------------------------------- 1 | class Employee: 2 | def __init__(self, seniority, months_disabled, is_part_time, on_vacation) -> None: 3 | self._seniority = seniority 4 | self._months_disabled = months_disabled 5 | self._is_part_time = is_part_time 6 | self._on_vacation = on_vacation 7 | 8 | @property 9 | def seniority(self): 10 | return self._seniority 11 | 12 | @property 13 | def months_disabled(self): 14 | return self._months_disabled 15 | 16 | @property 17 | def is_part_time(self): 18 | return self._is_part_time 19 | 20 | @property 21 | def on_vacation(self): 22 | return self._on_vacation 23 | -------------------------------------------------------------------------------- /Chapter10/02-consolidate-conditional-expression/example.py: -------------------------------------------------------------------------------- 1 | def disability_amount(employee): 2 | if is_not_eligible_for_disability(employee): 3 | return 0 4 | return 1 5 | 6 | 7 | def vacation_amount(employee): 8 | if employee.on_vacation and employee.seniority > 10: 9 | return 1 10 | return 0.5 11 | 12 | 13 | def is_not_eligible_for_disability(employee): 14 | return ( 15 | employee.seniority < 2 16 | or employee.months_disabled > 12 17 | or employee.is_part_time 18 | ) 19 | -------------------------------------------------------------------------------- /Chapter10/02-consolidate-conditional-expression/test_example.py: -------------------------------------------------------------------------------- 1 | from employee import Employee 2 | from example import disability_amount, vacation_amount 3 | 4 | 5 | def test_disability_amount(): 6 | assert disability_amount(Employee(1, 2, False, False)) == 0 7 | assert disability_amount(Employee(2, 13, False, False)) == 0 8 | assert disability_amount(Employee(2, 2, True, False)) == 0 9 | assert disability_amount(Employee(2, 2, False, False)) == 1 10 | 11 | 12 | def test_vacation_amount(): 13 | assert vacation_amount(Employee(20, 2, False, False)) == 0.5 14 | assert vacation_amount(Employee(20, 2, False, True)) == 1 15 | assert vacation_amount(Employee(10, 2, False, False)) == 0.5 16 | assert vacation_amount(Employee(10, 2, False, True)) == 0.5 17 | 18 | -------------------------------------------------------------------------------- /Chapter10/03-replace-nested-conditional-with-guard-clauses/employee.py: -------------------------------------------------------------------------------- 1 | class Employee: 2 | def __init__(self, is_separated, is_retired) -> None: 3 | self._is_separated = is_separated 4 | self._is_retired = is_retired 5 | 6 | @property 7 | def is_separated(self): 8 | return self._is_separated 9 | 10 | @property 11 | def is_retired(self): 12 | return self._is_retired 13 | -------------------------------------------------------------------------------- /Chapter10/03-replace-nested-conditional-with-guard-clauses/example.py: -------------------------------------------------------------------------------- 1 | def pay_amount(employee): 2 | if employee.is_separated: 3 | return {"amount": 0, "reasonCode": "SEP"} 4 | if employee.is_retired: 5 | return {"amount": 0, "reasonCode": "RET"} 6 | # Something to calculate pay 7 | return {"amount": 10000, "reasonCode": "None"} 8 | 9 | 10 | def adjusted_capital(instrument): 11 | if ( 12 | instrument.capital <= 0 13 | or instrument.interest_rate <= 0 14 | or instrument.duration <= 0 15 | ): 16 | return 0 17 | return ( 18 | instrument.income / instrument.duration * instrument.adjustment_factor 19 | ) 20 | -------------------------------------------------------------------------------- /Chapter10/03-replace-nested-conditional-with-guard-clauses/instrument.py: -------------------------------------------------------------------------------- 1 | class Instrument: 2 | def __init__(self, data) -> None: 3 | self._capital = data["capital"] 4 | self._interest_rate = data["interest_rate"] 5 | self._duration = data["duration"] 6 | self._income = data["income"] 7 | self._adjustment_factor = data["adjustment_factor"] 8 | 9 | @property 10 | def capital(self): 11 | return self._capital 12 | 13 | @property 14 | def interest_rate(self): 15 | return self._interest_rate 16 | 17 | @property 18 | def duration(self): 19 | return self._duration 20 | 21 | @property 22 | def income(self): 23 | return self._income 24 | 25 | @property 26 | def adjustment_factor(self): 27 | return self._adjustment_factor 28 | -------------------------------------------------------------------------------- /Chapter10/03-replace-nested-conditional-with-guard-clauses/test_example.py: -------------------------------------------------------------------------------- 1 | from employee import Employee 2 | from example import adjusted_capital, pay_amount 3 | from instrument import Instrument 4 | 5 | 6 | def test_pay_amount(): 7 | assert pay_amount(Employee(True, True)) == { 8 | "amount": 0, 9 | "reasonCode": "SEP", 10 | } 11 | assert pay_amount(Employee(True, False)) == { 12 | "amount": 0, 13 | "reasonCode": "SEP", 14 | } 15 | assert pay_amount(Employee(False, True)) == { 16 | "amount": 0, 17 | "reasonCode": "RET", 18 | } 19 | assert pay_amount(Employee(False, False)) == { 20 | "amount": 10000, 21 | "reasonCode": "None", 22 | } 23 | 24 | 25 | def test_adjusted_capital(): 26 | instrument_data = { 27 | "capital": 10000, 28 | "interest_rate": 0.03, 29 | "duration": 12, 30 | "income": 12000, 31 | "adjustment_factor": 0.8, 32 | } 33 | assert adjusted_capital(Instrument(instrument_data)) == 800 34 | 35 | instrument_data["capital"] = 0 36 | assert adjusted_capital(Instrument(instrument_data)) == 0 37 | 38 | instrument_data["capital"] = 10000 39 | instrument_data["interest_rate"] = 0 40 | assert adjusted_capital(Instrument(instrument_data)) == 0 41 | 42 | instrument_data["interest_rate"] = 0.03 43 | instrument_data["duration"] = 0 44 | assert adjusted_capital(Instrument(instrument_data)) == 0 45 | -------------------------------------------------------------------------------- /Chapter10/04-replace-conditional-with-polymorphism/bird.py: -------------------------------------------------------------------------------- 1 | class Bird: 2 | def __init__(self, bird_object) -> None: 3 | self._name = bird_object.name 4 | self._type = bird_object.type 5 | self._number_of_coconuts = bird_object.number_of_coconuts 6 | self._voltage = bird_object.voltage 7 | self._is_nailed = bird_object.is_nailed 8 | 9 | @property 10 | def name(self): 11 | return self._name 12 | 13 | @property 14 | def type(self): 15 | return self._type 16 | 17 | @property 18 | def number_of_coconuts(self): 19 | return self._number_of_coconuts 20 | 21 | @number_of_coconuts.setter 22 | def number_of_coconuts(self, arg): 23 | self._number_of_coconuts = arg 24 | 25 | @property 26 | def voltage(self): 27 | return self._voltage 28 | 29 | @voltage.setter 30 | def voltage(self, arg): 31 | self._voltage = arg 32 | 33 | @property 34 | def is_nailed(self): 35 | return self._is_nailed 36 | 37 | @is_nailed.setter 38 | def is_nailed(self, arg): 39 | self._is_nailed = arg 40 | 41 | @property 42 | def plumage(self): 43 | return "Unknown" 44 | 45 | @property 46 | def air_speed_velocity(self): 47 | return None 48 | 49 | 50 | class EuropeanSwallow(Bird): 51 | @property 52 | def plumage(self): 53 | return "Normal" 54 | 55 | @property 56 | def air_speed_velocity(self): 57 | return 35 58 | 59 | 60 | class AfricanSwallow(Bird): 61 | @property 62 | def plumage(self): 63 | return "Exhausted" if self._number_of_coconuts > 2 else "Normal" 64 | 65 | @property 66 | def air_speed_velocity(self): 67 | return 40 - 2 * self._number_of_coconuts 68 | 69 | 70 | class NorwegianBlueParrot(Bird): 71 | @property 72 | def plumage(self): 73 | return "Scorched" if self._voltage > 100 else "Pretty" 74 | 75 | @property 76 | def air_speed_velocity(self): 77 | return 0 if self._is_nailed else 10 + self._voltage / 10 78 | 79 | 80 | class BirdObject: 81 | def __init__(self, name, type): 82 | self.name = name 83 | self.type = type 84 | self.number_of_coconuts = 0 85 | self.voltage = 0 86 | self.is_nailed = False 87 | 88 | 89 | def plumages(birds): 90 | return map( 91 | lambda b: [b.name, b.plumage], map(lambda b: create_bird(b), birds) 92 | ) 93 | 94 | 95 | def speeds(birds): 96 | return map( 97 | lambda b: [b.name, b.air_speed_velocity], 98 | map(lambda b: create_bird(b), birds), 99 | ) 100 | 101 | 102 | def create_bird(bird): 103 | if bird.type == "European Swallow": 104 | return EuropeanSwallow(bird) 105 | elif bird.type == "African Swallow": 106 | return AfricanSwallow(bird) 107 | elif bird.type == "Norwegian Blue Parrot": 108 | return NorwegianBlueParrot(bird) 109 | else: 110 | return Bird(bird) 111 | -------------------------------------------------------------------------------- /Chapter10/04-replace-conditional-with-polymorphism/test_bird.py: -------------------------------------------------------------------------------- 1 | from bird import BirdObject, create_bird 2 | 3 | 4 | def get_bird(type): 5 | return create_bird(BirdObject("test", type)) 6 | 7 | 8 | def test_plumage(): 9 | european = get_bird("European Swallow") 10 | assert european.plumage == "Normal" 11 | 12 | african = get_bird("African Swallow") 13 | assert african.plumage == "Normal" 14 | african.number_of_coconuts = 3 15 | assert african.plumage == "Exhausted" 16 | 17 | norwegian = get_bird("Norwegian Blue Parrot") 18 | assert norwegian.plumage == "Pretty" 19 | norwegian.voltage = 101 20 | assert norwegian.plumage == "Scorched" 21 | 22 | other = get_bird("Eagle") 23 | assert other.plumage == "Unknown" 24 | 25 | 26 | def test_air_speed_velocity(): 27 | european = get_bird("European Swallow") 28 | assert european.air_speed_velocity == 35 29 | 30 | african = get_bird("African Swallow") 31 | assert african.air_speed_velocity == 40 32 | african.number_of_coconuts = 3 33 | assert african.air_speed_velocity == 34 34 | 35 | norwegian = get_bird("Norwegian Blue Parrot") 36 | assert norwegian.air_speed_velocity == 10 37 | norwegian.voltage = 101 38 | assert norwegian.air_speed_velocity == 20.1 39 | norwegian.is_nailed = True 40 | assert norwegian.air_speed_velocity == 0 41 | 42 | other = get_bird("Eagle") 43 | assert other.air_speed_velocity is None 44 | -------------------------------------------------------------------------------- /Chapter10/04-replace-conditional-with-polymorphism/test_voyage_rating.py: -------------------------------------------------------------------------------- 1 | from voyage_rating import HistoryObject, VoyageObject, rating 2 | 3 | 4 | def test_rating(): 5 | voyage = VoyageObject("West India", 10) 6 | history = [ 7 | HistoryObject("East India", 5), 8 | HistoryObject("West India", 15), 9 | HistoryObject("China", -2), 10 | HistoryObject("West Africa", 7), 11 | ] 12 | assert rating(voyage, history) == "B" 13 | 14 | voyage.zone = "China" 15 | assert rating(voyage, history) == "A" 16 | -------------------------------------------------------------------------------- /Chapter10/04-replace-conditional-with-polymorphism/voyage_rating.py: -------------------------------------------------------------------------------- 1 | class Rating: 2 | def __init__(self, voyage, history) -> None: 3 | self.voyage = voyage 4 | self.history = history 5 | 6 | @property 7 | def value(self): 8 | vpf = self.voyage_profit_factor 9 | vr = self.voyage_risk 10 | chr = self.captain_history_risk 11 | return "A" if vpf * 3 > (vr + chr * 2) else "B" 12 | 13 | @property 14 | def voyage_risk(self): 15 | result = 1 16 | if self.voyage.length > 4: 17 | result += 2 18 | if self.voyage.length > 8: 19 | result += self.voyage.length - 8 20 | if self.voyage.zone in ["China", "East India"]: 21 | result += 4 22 | return max(result, 0) 23 | 24 | @property 25 | def captain_history_risk(self): 26 | result = 1 27 | if len(self.history) < 5: 28 | result += 4 29 | result += len(list(filter(lambda v: v.profit < 0, self.history))) 30 | return max(result, 0) 31 | 32 | @property 33 | def voyage_profit_factor(self): 34 | result = 2 35 | if self.voyage.zone == "China": 36 | result += 1 37 | if self.voyage.zone == "East India": 38 | result += 1 39 | result += self.history_length_factor 40 | result += self.voyage_length_factor 41 | return result 42 | 43 | @property 44 | def voyage_length_factor(self): 45 | return -1 if self.voyage.length > 14 else 0 46 | 47 | @property 48 | def history_length_factor(self): 49 | return 1 if len(self.history) > 8 else 0 50 | 51 | def has_china_history(self): 52 | return any(filter(lambda v: v.zone == "China", self.history)) 53 | 54 | 55 | class ExperiencedChinaRating(Rating): 56 | @property 57 | def captain_history_risk(self): 58 | return max(super().captain_history_risk - 2, 0) 59 | 60 | @property 61 | def voyage_profit_factor(self): 62 | return super().voyage_profit_factor + 3 63 | 64 | @property 65 | def voyage_length_factor(self): 66 | result = 0 67 | if self.voyage.length > 12: 68 | result += 1 69 | if self.voyage.length > 18: 70 | result += 1 71 | return result 72 | 73 | @property 74 | def history_length_factor(self): 75 | return 1 if len(self.history) > 10 else 0 76 | 77 | 78 | class VoyageObject: 79 | def __init__(self, zone, length) -> None: 80 | self.zone = zone 81 | self.length = length 82 | 83 | 84 | class HistoryObject: 85 | def __init__(self, zone, profit) -> None: 86 | self.zone = zone 87 | self.profit = profit 88 | 89 | 90 | def rating(voyage, history): 91 | return create_rating(voyage, history).value 92 | 93 | 94 | def create_rating(voyage, history): 95 | if voyage.zone == "China" and any( 96 | filter(lambda v: v.zone == "China", history) 97 | ): 98 | return ExperiencedChinaRating(voyage, history) 99 | else: 100 | return Rating(voyage, history) 101 | -------------------------------------------------------------------------------- /Chapter10/05-introduce-special-case/customer.py: -------------------------------------------------------------------------------- 1 | class Customer: 2 | def __init__(self, name, billing_plan, payment_history): 3 | self._name = name 4 | self._billing_plan = billing_plan 5 | self._payment_history = payment_history 6 | 7 | @property 8 | def name(self): 9 | return self._name 10 | 11 | @property 12 | def billing_plan(self): 13 | return self._billing_plan 14 | 15 | @billing_plan.setter 16 | def billing_plan(self, arg): 17 | self._billing_plan = arg 18 | 19 | @property 20 | def payment_history(self): 21 | return self._payment_history 22 | 23 | @property 24 | def is_unknown(self): 25 | return False 26 | 27 | 28 | class UnknownCustomer: 29 | @property 30 | def is_unknown(self): 31 | return True 32 | 33 | @property 34 | def name(self): 35 | return "Resident" 36 | 37 | @property 38 | def billing_plan(self): 39 | return BillingPlan("Basic") 40 | 41 | @billing_plan.setter 42 | def billing_plan(self, arg): 43 | """Nothing to do""" 44 | pass 45 | 46 | @property 47 | def payment_history(self): 48 | return NullPaymentHistory() 49 | 50 | 51 | class BillingPlan: 52 | def __init__(self, name): 53 | self._name = name 54 | 55 | @property 56 | def name(self): 57 | return self._name 58 | 59 | 60 | class PaymentHistory: 61 | def __init__(self, weeks_delinquent_in_last_year): 62 | self._weeks_delinquent_in_last_year = weeks_delinquent_in_last_year 63 | 64 | @property 65 | def weeks_delinquent_in_last_year(self): 66 | return self._weeks_delinquent_in_last_year 67 | 68 | 69 | class NullPaymentHistory: 70 | @property 71 | def weeks_delinquent_in_last_year(self): 72 | return 0 73 | -------------------------------------------------------------------------------- /Chapter10/05-introduce-special-case/example.py: -------------------------------------------------------------------------------- 1 | from customer import BillingPlan 2 | 3 | 4 | def get_customer_name(site): 5 | # Client 1 6 | return site.customer.name 7 | 8 | 9 | def get_billing_plan(customer): 10 | # Client 2 11 | return customer.billing_plan 12 | 13 | 14 | def reset_billing_plan(customer): 15 | # Client 3 16 | customer.billing_plan = BillingPlan("New") 17 | 18 | 19 | def get_weeks_delinquent_in_last_year(customer): 20 | # Client 4 21 | return customer.payment_history.weeks_delinquent_in_last_year 22 | -------------------------------------------------------------------------------- /Chapter10/05-introduce-special-case/my_site.py: -------------------------------------------------------------------------------- 1 | from customer import UnknownCustomer 2 | 3 | 4 | class Site: 5 | def __init__(self, customer): 6 | self._customer = customer 7 | 8 | @property 9 | def customer(self): 10 | return ( 11 | UnknownCustomer() 12 | if self._customer == "Unknown Customer" 13 | else self._customer 14 | ) 15 | -------------------------------------------------------------------------------- /Chapter10/05-introduce-special-case/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from customer import BillingPlan, Customer, PaymentHistory, UnknownCustomer 4 | from example import ( 5 | get_billing_plan, 6 | get_customer_name, 7 | get_weeks_delinquent_in_last_year, 8 | reset_billing_plan, 9 | ) 10 | from my_site import Site 11 | 12 | 13 | @pytest.fixture 14 | def customer(): 15 | return Customer("Minwoo Jeong", BillingPlan("Test"), PaymentHistory(24)) 16 | 17 | 18 | @pytest.fixture 19 | def unknown_customer(): 20 | return UnknownCustomer() 21 | 22 | 23 | def test_get_customer_name(customer, unknown_customer): 24 | assert get_customer_name(Site(customer)) == "Minwoo Jeong" 25 | assert get_customer_name(Site(unknown_customer)) == "Resident" 26 | 27 | 28 | def test_get_billing_plan(customer, unknown_customer): 29 | assert get_billing_plan(customer).name == "Test" 30 | assert get_billing_plan(unknown_customer).name == "Basic" 31 | 32 | 33 | def test_reset_billing_plan(customer, unknown_customer): 34 | reset_billing_plan(customer) 35 | assert customer.billing_plan.name == "New" 36 | reset_billing_plan(unknown_customer) 37 | assert True 38 | 39 | 40 | def test_get_weeks_delinquent_in_last_year(customer, unknown_customer): 41 | assert get_weeks_delinquent_in_last_year(customer) == 24 42 | assert get_weeks_delinquent_in_last_year(unknown_customer) == 0 43 | -------------------------------------------------------------------------------- /Chapter10/06-introduce-assertion/customer.py: -------------------------------------------------------------------------------- 1 | class Customer: 2 | def __init__(self, discount_rate) -> None: 3 | self._discount_rate = discount_rate 4 | 5 | @property 6 | def discount_rate(self): 7 | return self._discount_rate 8 | 9 | @discount_rate.setter 10 | def discount_rate(self, arg): 11 | assert arg >= 0 12 | self._discount_rate = arg 13 | 14 | def apply_discount(self, number): 15 | if self.discount_rate: 16 | assert self.discount_rate >= 0 17 | return number - (self.discount_rate * number) 18 | else: 19 | return number 20 | -------------------------------------------------------------------------------- /Chapter10/06-introduce-assertion/test_customer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from customer import Customer 4 | 5 | 6 | def test_apply_discount(): 7 | assert Customer(0.2).apply_discount(100) == 80 8 | assert Customer(0.32).apply_discount(100) == 68 9 | 10 | 11 | def test_apply_discount_with_assertion(): 12 | with pytest.raises(AssertionError): 13 | Customer(-0.2).apply_discount(100) 14 | -------------------------------------------------------------------------------- /Chapter10/07-replace-control-flag-with-break/example.py: -------------------------------------------------------------------------------- 1 | def check_for_miscreants(people): 2 | return any(map(lambda p: p in ["Joker", "Saruman"], people)) 3 | 4 | 5 | def send_alert(): 6 | print("There is a miscreant!") 7 | -------------------------------------------------------------------------------- /Chapter10/07-replace-control-flag-with-break/test_example.py: -------------------------------------------------------------------------------- 1 | from example import check_for_miscreants 2 | 3 | 4 | def test_check_for_miscreants(): 5 | people = ["Minwoo Jeong", "Joker", "Saruman"] 6 | assert check_for_miscreants(people) 7 | people.remove("Saruman") 8 | assert check_for_miscreants(people) 9 | people.remove("Joker") 10 | assert not check_for_miscreants(people) 11 | -------------------------------------------------------------------------------- /Chapter10/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 10: Simplifying Conditional Logic 2 | 3 | ## Contents 4 | 5 | 1. Decompose Conditional 6 | 2. Consolidate Conditional Expression 7 | 3. Replace Nested Conditional with Guard Clauses 8 | 4. Replace Conditional with Polymorphism 9 | 5. Introduce Special Case 10 | 6. Introduce Assertion 11 | -------------------------------------------------------------------------------- /Chapter11/01-seperate-query-from-modifier/client.py: -------------------------------------------------------------------------------- 1 | from example import find_miscreant, set_off_alarms 2 | 3 | 4 | def client(): 5 | found = find_miscreant(["Minwoo Jeong", "Joker"]) 6 | set_off_alarms() 7 | print(f"The miscreant's name is {found}") 8 | -------------------------------------------------------------------------------- /Chapter11/01-seperate-query-from-modifier/example.py: -------------------------------------------------------------------------------- 1 | def alert_for_miscreant(people): 2 | if find_miscreant(people): 3 | set_off_alarms() 4 | 5 | 6 | def find_miscreant(people): 7 | for p in people: 8 | if p == "Joker": 9 | return "Joker" 10 | if p == "Saruman": 11 | return "Saruman" 12 | return "" 13 | 14 | 15 | def set_off_alarms(): 16 | print("A miscreant has been detected!") 17 | -------------------------------------------------------------------------------- /Chapter11/01-seperate-query-from-modifier/test_example.py: -------------------------------------------------------------------------------- 1 | from example import find_miscreant 2 | 3 | 4 | def test_find_miscreant(): 5 | assert find_miscreant(["Minwoo Jeong", "Joker", "Saruman"]) == "Joker" 6 | assert find_miscreant(["Minwoo Jeong", "Saruman", "Joker"]) == "Saruman" 7 | assert find_miscreant(["Minwoo Jeong", "Batman", "Spiderman"]) == "" 8 | 9 | -------------------------------------------------------------------------------- /Chapter11/02-parameterize-function/example.py: -------------------------------------------------------------------------------- 1 | from math import inf 2 | 3 | 4 | def base_charge(usage): 5 | if usage < 0: 6 | return usd(0) 7 | amount = ( 8 | within_band(usage, 0, 100) * 0.03 9 | + within_band(usage, 100, 200) * 0.05 10 | + within_band(usage, 200, inf) * 0.07 11 | ) 12 | return usd(amount) 13 | 14 | 15 | def usd(amount): 16 | return f"${amount:,.2f}" 17 | 18 | 19 | def within_band(usage, bottom, top): 20 | return min(usage, top) - bottom if usage > bottom else 0 21 | -------------------------------------------------------------------------------- /Chapter11/02-parameterize-function/test_example.py: -------------------------------------------------------------------------------- 1 | from example import base_charge 2 | 3 | 4 | def test_base_charge(): 5 | assert base_charge(50) == "$1.50" 6 | assert base_charge(120) == "$4.00" 7 | assert base_charge(240) == "$10.80" 8 | assert base_charge(500) == "$29.00" 9 | -------------------------------------------------------------------------------- /Chapter11/03-remove-flag-argument/example1/example.py: -------------------------------------------------------------------------------- 1 | def rush_delivery_date(order): 2 | if order.delivery_state in ["MA", "CT"]: 3 | delivery_time = 1 4 | elif order.delivery_state in ["NY", "NH"]: 5 | delivery_time = 2 6 | else: 7 | delivery_time = 3 8 | return order.add_days_to_placed_on(1 + delivery_time) 9 | 10 | 11 | def regular_delivery_date(order): 12 | if order.delivery_state in ["MA", "CT", "NY"]: 13 | delivery_time = 2 14 | elif order.delivery_state in ["ME", "NH"]: 15 | delivery_time = 3 16 | else: 17 | delivery_time = 4 18 | return order.add_days_to_placed_on(2 + delivery_time) 19 | -------------------------------------------------------------------------------- /Chapter11/03-remove-flag-argument/example1/order.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | 4 | class Order: 5 | def __init__(self, delivery_state, placed_on): 6 | self._delivery_state = delivery_state 7 | self._placed_on = placed_on 8 | 9 | @property 10 | def delivery_state(self): 11 | return self._delivery_state 12 | 13 | @property 14 | def placed_on(self): 15 | return self._placed_on 16 | 17 | def add_days_to_placed_on(self, days): 18 | return self._placed_on + timedelta(days=days) 19 | -------------------------------------------------------------------------------- /Chapter11/03-remove-flag-argument/example1/test_example.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from example import rush_delivery_date, regular_delivery_date 4 | from order import Order 5 | 6 | 7 | def test_order_ma(): 8 | order = Order("MA", datetime.strptime("01/25/22", "%m/%d/%y")) 9 | assert rush_delivery_date(order) == datetime.strptime( 10 | "01/27/22", "%m/%d/%y" 11 | ) 12 | assert regular_delivery_date(order) == datetime.strptime( 13 | "01/29/22", "%m/%d/%y" 14 | ) 15 | 16 | 17 | def test_order_ct(): 18 | order = Order("CT", datetime.strptime("01/25/22", "%m/%d/%y")) 19 | assert rush_delivery_date(order) == datetime.strptime( 20 | "01/27/22", "%m/%d/%y" 21 | ) 22 | assert regular_delivery_date(order) == datetime.strptime( 23 | "01/29/22", "%m/%d/%y" 24 | ) 25 | 26 | 27 | def test_order_ny(): 28 | order = Order("NY", datetime.strptime("01/25/22", "%m/%d/%y")) 29 | assert rush_delivery_date(order) == datetime.strptime( 30 | "01/28/22", "%m/%d/%y" 31 | ) 32 | assert regular_delivery_date(order) == datetime.strptime( 33 | "01/29/22", "%m/%d/%y" 34 | ) 35 | 36 | 37 | def test_order_me(): 38 | order = Order("ME", datetime.strptime("01/25/22", "%m/%d/%y")) 39 | assert rush_delivery_date(order) == datetime.strptime( 40 | "01/29/22", "%m/%d/%y" 41 | ) 42 | assert regular_delivery_date(order) == datetime.strptime( 43 | "01/30/22", "%m/%d/%y" 44 | ) 45 | 46 | 47 | def test_order_nh(): 48 | order = Order("NH", datetime.strptime("01/25/22", "%m/%d/%y")) 49 | assert rush_delivery_date(order) == datetime.strptime( 50 | "01/28/22", "%m/%d/%y" 51 | ) 52 | assert regular_delivery_date(order) == datetime.strptime( 53 | "01/30/22", "%m/%d/%y" 54 | ) 55 | -------------------------------------------------------------------------------- /Chapter11/03-remove-flag-argument/example2/example.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | 4 | def rush_delivery_date(order): 5 | return delivery_date(order, True) 6 | 7 | 8 | def regular_delivery_date(order): 9 | return delivery_date(order, False) 10 | 11 | 12 | def delivery_date(order, is_rush): 13 | if order.delivery_state == "MA" or order.delivery_state == "CT": 14 | delivery_time = 1 if is_rush else 2 15 | elif order.delivery_state == "NY" or order.delivery_state == "NH": 16 | delivery_time = 2 17 | if order.delivery_state == "NH" and not is_rush: 18 | delivery_time = 3 19 | elif is_rush: 20 | delivery_time = 3 21 | elif order.delivery_state == "ME": 22 | delivery_time = 3 23 | else: 24 | delivery_time = 4 25 | result = order.add_days_to_placed_on(2 + delivery_time) 26 | if is_rush: 27 | result -= timedelta(days=1) 28 | return result 29 | -------------------------------------------------------------------------------- /Chapter11/03-remove-flag-argument/example2/order.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | 4 | class Order: 5 | def __init__(self, delivery_state, placed_on): 6 | self._delivery_state = delivery_state 7 | self._placed_on = placed_on 8 | 9 | @property 10 | def delivery_state(self): 11 | return self._delivery_state 12 | 13 | @property 14 | def placed_on(self): 15 | return self._placed_on 16 | 17 | def add_days_to_placed_on(self, days): 18 | return self._placed_on + timedelta(days=days) 19 | -------------------------------------------------------------------------------- /Chapter11/03-remove-flag-argument/example2/test_example.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from example import rush_delivery_date, regular_delivery_date 4 | from order import Order 5 | 6 | 7 | def test_order_ma(): 8 | order = Order("MA", datetime.strptime("01/25/22", "%m/%d/%y")) 9 | assert rush_delivery_date(order) == datetime.strptime( 10 | "01/27/22", "%m/%d/%y" 11 | ) 12 | assert regular_delivery_date(order) == datetime.strptime( 13 | "01/29/22", "%m/%d/%y" 14 | ) 15 | 16 | 17 | def test_order_ct(): 18 | order = Order("CT", datetime.strptime("01/25/22", "%m/%d/%y")) 19 | assert rush_delivery_date(order) == datetime.strptime( 20 | "01/27/22", "%m/%d/%y" 21 | ) 22 | assert regular_delivery_date(order) == datetime.strptime( 23 | "01/29/22", "%m/%d/%y" 24 | ) 25 | 26 | 27 | def test_order_ny(): 28 | order = Order("NY", datetime.strptime("01/25/22", "%m/%d/%y")) 29 | assert rush_delivery_date(order) == datetime.strptime( 30 | "01/28/22", "%m/%d/%y" 31 | ) 32 | assert regular_delivery_date(order) == datetime.strptime( 33 | "01/29/22", "%m/%d/%y" 34 | ) 35 | 36 | 37 | def test_order_me(): 38 | order = Order("ME", datetime.strptime("01/25/22", "%m/%d/%y")) 39 | assert rush_delivery_date(order) == datetime.strptime( 40 | "01/29/22", "%m/%d/%y" 41 | ) 42 | assert regular_delivery_date(order) == datetime.strptime( 43 | "01/30/22", "%m/%d/%y" 44 | ) 45 | 46 | 47 | def test_order_nh(): 48 | order = Order("NH", datetime.strptime("01/25/22", "%m/%d/%y")) 49 | assert rush_delivery_date(order) == datetime.strptime( 50 | "01/28/22", "%m/%d/%y" 51 | ) 52 | assert regular_delivery_date(order) == datetime.strptime( 53 | "01/30/22", "%m/%d/%y" 54 | ) 55 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example1/example.py: -------------------------------------------------------------------------------- 1 | def check_room_temperature(plan, room): 2 | if not plan.within_range(room.days_temp_range): 3 | return "The room temperature is out of the range." 4 | return "The room temperature is in appropriate range." 5 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example1/heating_plan.py: -------------------------------------------------------------------------------- 1 | class HeatingPlan: 2 | def __init__(self, temperature_range) -> None: 3 | self._temperature_range = temperature_range 4 | 5 | def within_range(self, range): 6 | return ( 7 | range.low >= self._temperature_range.low 8 | and range.high <= self._temperature_range.high 9 | ) 10 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example1/room.py: -------------------------------------------------------------------------------- 1 | class Room: 2 | def __init__(self, days_temp_range) -> None: 3 | self._days_temp_range = days_temp_range 4 | 5 | @property 6 | def days_temp_range(self): 7 | return self._days_temp_range 8 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example1/temperature_range.py: -------------------------------------------------------------------------------- 1 | class TemperatureRange: 2 | def __init__(self, low, high) -> None: 3 | self._low = low 4 | self._high = high 5 | 6 | @property 7 | def low(self): 8 | return self._low 9 | 10 | @property 11 | def high(self): 12 | return self._high 13 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example1/test_example.py: -------------------------------------------------------------------------------- 1 | from example import check_room_temperature 2 | from heating_plan import HeatingPlan 3 | from room import Room 4 | from temperature_range import TemperatureRange 5 | 6 | 7 | def test_check_room_temperature(): 8 | plan = HeatingPlan(TemperatureRange(15, 30)) 9 | assert ( 10 | check_room_temperature(plan, Room(TemperatureRange(20, 25))) 11 | == "The room temperature is in appropriate range." 12 | ) 13 | assert ( 14 | check_room_temperature(plan, Room(TemperatureRange(10, 20))) 15 | == "The room temperature is out of the range." 16 | ) 17 | assert ( 18 | check_room_temperature(plan, Room(TemperatureRange(31, 40))) 19 | == "The room temperature is out of the range." 20 | ) 21 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example2/example.py: -------------------------------------------------------------------------------- 1 | def check_room_temperature(plan, room): 2 | temp_range = room.days_temp_range 3 | is_within_range = plan.within_range(temp_range) 4 | if not is_within_range: 5 | return "The room temperature is out of the range." 6 | return "The room temperature is in appropriate range." 7 | 8 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example2/heating_plan.py: -------------------------------------------------------------------------------- 1 | class HeatingPlan: 2 | def __init__(self, temperature_range) -> None: 3 | self._temperature_range = temperature_range 4 | 5 | def within_range(self, temp_range): 6 | return ( 7 | temp_range.low >= self._temperature_range.low 8 | and temp_range.high <= self._temperature_range.high 9 | ) 10 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example2/room.py: -------------------------------------------------------------------------------- 1 | class Room: 2 | def __init__(self, days_temp_range) -> None: 3 | self._days_temp_range = days_temp_range 4 | 5 | @property 6 | def days_temp_range(self): 7 | return self._days_temp_range 8 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example2/temperature_range.py: -------------------------------------------------------------------------------- 1 | class TemperatureRange: 2 | def __init__(self, low, high) -> None: 3 | self._low = low 4 | self._high = high 5 | 6 | @property 7 | def low(self): 8 | return self._low 9 | 10 | @property 11 | def high(self): 12 | return self._high 13 | -------------------------------------------------------------------------------- /Chapter11/04-preserve-whole-object/example2/test_example.py: -------------------------------------------------------------------------------- 1 | from example import check_room_temperature 2 | from heating_plan import HeatingPlan 3 | from room import Room 4 | from temperature_range import TemperatureRange 5 | 6 | 7 | def test_check_room_temperature(): 8 | plan = HeatingPlan(TemperatureRange(15, 30)) 9 | assert ( 10 | check_room_temperature(plan, Room(TemperatureRange(20, 25))) 11 | == "The room temperature is in appropriate range." 12 | ) 13 | assert ( 14 | check_room_temperature(plan, Room(TemperatureRange(10, 20))) 15 | == "The room temperature is out of the range." 16 | ) 17 | assert ( 18 | check_room_temperature(plan, Room(TemperatureRange(31, 40))) 19 | == "The room temperature is out of the range." 20 | ) 21 | -------------------------------------------------------------------------------- /Chapter11/05-replace-parameter-with-query/order.py: -------------------------------------------------------------------------------- 1 | class Order: 2 | def __init__(self, quantity, item_price): 3 | self._quantity = quantity 4 | self._item_price = item_price 5 | 6 | @property 7 | def quantity(self): 8 | return self._quantity 9 | 10 | @property 11 | def item_price(self): 12 | return self._item_price 13 | 14 | @property 15 | def final_price(self): 16 | base_price = self.quantity * self.item_price 17 | return self.discounted_price(base_price) 18 | 19 | @property 20 | def discount_level(self): 21 | return 2 if self.quantity > 100 else 1 22 | 23 | def discounted_price(self, base_price): 24 | if self.discount_level == 1: 25 | return base_price * 0.95 26 | elif self.discount_level == 2: 27 | return base_price * 0.9 28 | else: 29 | return base_price 30 | -------------------------------------------------------------------------------- /Chapter11/05-replace-parameter-with-query/test_order.py: -------------------------------------------------------------------------------- 1 | from order import Order 2 | 3 | 4 | def test_final_price(): 5 | assert Order(150, 1000).final_price == 135000 6 | assert Order(70, 120).final_price == 7980 7 | -------------------------------------------------------------------------------- /Chapter11/06-replace-query-with-parameter/example.py: -------------------------------------------------------------------------------- 1 | import thermostat 2 | 3 | 4 | def get_temp_instruction(plan): 5 | if ( 6 | plan.target_temperature(thermostat.selected_temperature) 7 | > thermostat.current_temperature 8 | ): 9 | return "Set to heat" 10 | elif ( 11 | plan.target_temperature(thermostat.selected_temperature) 12 | < thermostat.current_temperature 13 | ): 14 | return "Set to cool" 15 | else: 16 | return "Set off" 17 | -------------------------------------------------------------------------------- /Chapter11/06-replace-query-with-parameter/heating_plan.py: -------------------------------------------------------------------------------- 1 | class HeatingPlan: 2 | def __init__(self, min, max) -> None: 3 | self._min = min 4 | self._max = max 5 | 6 | def target_temperature(self, selected_temperature): 7 | if selected_temperature > self._max: 8 | return self._max 9 | elif selected_temperature < self._min: 10 | return self._min 11 | else: 12 | return selected_temperature 13 | -------------------------------------------------------------------------------- /Chapter11/06-replace-query-with-parameter/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import thermostat 4 | from example import get_temp_instruction 5 | from heating_plan import HeatingPlan 6 | 7 | 8 | @pytest.fixture 9 | def plan(): 10 | return HeatingPlan(15, 30) 11 | 12 | 13 | def test_get_temp_instruction(plan): 14 | thermostat.set_selected_temperature(25) 15 | thermostat.set_current_temperature(10) 16 | assert get_temp_instruction(plan) == "Set to heat" 17 | thermostat.set_current_temperature(25) 18 | assert get_temp_instruction(plan) == "Set off" 19 | thermostat.set_current_temperature(30) 20 | assert get_temp_instruction(plan) == "Set to cool" 21 | 22 | 23 | def test_get_temp_instruction_low_selected_temp(plan): 24 | thermostat.set_selected_temperature(12) 25 | thermostat.set_current_temperature(10) 26 | assert get_temp_instruction(plan) == "Set to heat" 27 | thermostat.set_current_temperature(15) 28 | assert get_temp_instruction(plan) == "Set off" 29 | thermostat.set_current_temperature(25) 30 | assert get_temp_instruction(plan) == "Set to cool" 31 | 32 | 33 | def test_get_temp_instruction_high_selected_temp(plan): 34 | thermostat.set_selected_temperature(32) 35 | thermostat.set_current_temperature(25) 36 | assert get_temp_instruction(plan) == "Set to heat" 37 | thermostat.set_current_temperature(30) 38 | assert get_temp_instruction(plan) == "Set off" 39 | thermostat.set_current_temperature(32) 40 | assert get_temp_instruction(plan) == "Set to cool" 41 | -------------------------------------------------------------------------------- /Chapter11/06-replace-query-with-parameter/thermostat.py: -------------------------------------------------------------------------------- 1 | selected_temperature = 25 2 | current_temperature = 16 3 | 4 | 5 | def set_selected_temperature(value): 6 | global selected_temperature 7 | selected_temperature = value 8 | 9 | 10 | def set_current_temperature(value): 11 | global current_temperature 12 | current_temperature = value 13 | -------------------------------------------------------------------------------- /Chapter11/07-remove-setting-method/person.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, id): 3 | self._name = "" 4 | self._id = id 5 | 6 | @property 7 | def name(self): 8 | return self._name 9 | 10 | @name.setter 11 | def name(self, value): 12 | self._name = value 13 | 14 | @property 15 | def id(self): 16 | return self._id 17 | -------------------------------------------------------------------------------- /Chapter11/07-remove-setting-method/test_person.py: -------------------------------------------------------------------------------- 1 | from person import Person 2 | 3 | 4 | def test_person_properties(): 5 | """This test is somewhat redundant but exists for the example.""" 6 | person = Person(1234) 7 | person.name = "Minwoo Jeong" 8 | 9 | assert person.id == 1234 10 | assert person.name == "Minwoo Jeong" 11 | -------------------------------------------------------------------------------- /Chapter11/08-replace-constructor-with-factory-function/employee.py: -------------------------------------------------------------------------------- 1 | class Employee: 2 | def __init__(self, name, type_code) -> None: 3 | self._name = name 4 | self._type_code = type_code 5 | 6 | @property 7 | def name(self): 8 | return self._name 9 | 10 | @property 11 | def type(self): 12 | return Employee.legal_type_codes()[self._type_code] 13 | 14 | @staticmethod 15 | def legal_type_codes(): 16 | return {"E": "Engineer", "M": "Manager", "S": "Salesperson"} 17 | 18 | 19 | def create_employee(name, type_code): 20 | return Employee(name, type_code) 21 | 22 | 23 | def create_engineer(name): 24 | return Employee(name, "E") 25 | 26 | 27 | def create_manager(name): 28 | return Employee(name, "M") 29 | 30 | 31 | def create_salesperson(name): 32 | return Employee(name, "S") 33 | -------------------------------------------------------------------------------- /Chapter11/08-replace-constructor-with-factory-function/test_employee.py: -------------------------------------------------------------------------------- 1 | from employee import create_engineer, create_manager, create_salesperson 2 | 3 | 4 | def test_employee_type(): 5 | assert create_engineer("Minwoo Jeong").type == "Engineer" 6 | assert create_manager("Minwoo Jeong").type == "Manager" 7 | assert create_salesperson("Minwoo Jeong").type == "Salesperson" 8 | -------------------------------------------------------------------------------- /Chapter11/09-replace-function-with-command/candidate.py: -------------------------------------------------------------------------------- 1 | class Candidate: 2 | def __init__(self, state) -> None: 3 | self._origin_state = state 4 | 5 | @property 6 | def origin_state(self): 7 | return self._origin_state 8 | -------------------------------------------------------------------------------- /Chapter11/09-replace-function-with-command/medical_exam.py: -------------------------------------------------------------------------------- 1 | class MedicalExam: 2 | def __init__(self, is_smoker) -> None: 3 | self._is_smoker = is_smoker 4 | 5 | @property 6 | def is_smoker(self): 7 | return self._is_smoker 8 | -------------------------------------------------------------------------------- /Chapter11/09-replace-function-with-command/score.py: -------------------------------------------------------------------------------- 1 | def score(candidate, medical_exam, scoring_guide): 2 | return Scorer(candidate, medical_exam, scoring_guide).execute() 3 | 4 | 5 | class Scorer: 6 | def __init__(self, candidate, medical_exam, scoring_guide) -> None: 7 | self._candidate = candidate 8 | self._medical_exam = medical_exam 9 | self._scoring_guide = scoring_guide 10 | 11 | def execute(self): 12 | self._result = 0 13 | self._health_level = 0 14 | self._high_medical_risk_flag = False 15 | 16 | self.score_smoking() 17 | 18 | self._certification_grade = "regular" 19 | if self._scoring_guide.state_with_low_certification( 20 | self._candidate.origin_state 21 | ): 22 | self._certification_grade = "low" 23 | self._result -= 5 24 | 25 | # ... 26 | self._result -= max(self._health_level - 5, 0) 27 | return self._result 28 | 29 | def score_smoking(self): 30 | if self._medical_exam.is_smoker: 31 | self._health_level += 10 32 | self._high_medical_risk_flag = True 33 | -------------------------------------------------------------------------------- /Chapter11/09-replace-function-with-command/scoring_guide.py: -------------------------------------------------------------------------------- 1 | class ScoringGuide: 2 | @staticmethod 3 | def state_with_low_certification(state): 4 | if state == "unhealthy": 5 | return True 6 | return False 7 | -------------------------------------------------------------------------------- /Chapter11/09-replace-function-with-command/test_score.py: -------------------------------------------------------------------------------- 1 | from candidate import Candidate 2 | from medical_exam import MedicalExam 3 | from score import score 4 | from scoring_guide import ScoringGuide 5 | 6 | 7 | def test_score(): 8 | assert score(Candidate("Healthy"), MedicalExam(True), ScoringGuide()) == -5 9 | assert ( 10 | score(Candidate("Unhealthy"), MedicalExam(True), ScoringGuide()) == -5 11 | ) 12 | assert score(Candidate("Healthy"), MedicalExam(False), ScoringGuide()) == 0 13 | assert ( 14 | score(Candidate("Unhealthy"), MedicalExam(False), ScoringGuide()) == 0 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /Chapter11/10-replace-command-with-function/charge.py: -------------------------------------------------------------------------------- 1 | def get_month_charge(customer, usage, provider): 2 | month_charge = charge(customer, usage, provider) 3 | return month_charge 4 | 5 | 6 | def charge(customer, usage, provider): 7 | base_charge = customer.base_rate * usage 8 | return base_charge + provider.connection_charge 9 | -------------------------------------------------------------------------------- /Chapter11/10-replace-command-with-function/customer.py: -------------------------------------------------------------------------------- 1 | class Customer: 2 | def __init__(self, base_rate) -> None: 3 | self._base_rate = base_rate 4 | 5 | @property 6 | def base_rate(self): 7 | return self._base_rate 8 | -------------------------------------------------------------------------------- /Chapter11/10-replace-command-with-function/provider.py: -------------------------------------------------------------------------------- 1 | class Provider: 2 | def __init__(self, connection_charge) -> None: 3 | self._connection_charge = connection_charge 4 | 5 | @property 6 | def connection_charge(self): 7 | return self._connection_charge 8 | -------------------------------------------------------------------------------- /Chapter11/10-replace-command-with-function/test_charge.py: -------------------------------------------------------------------------------- 1 | from charge import get_month_charge 2 | from customer import Customer 3 | from provider import Provider 4 | 5 | 6 | def test_get_month_charge(): 7 | assert get_month_charge(Customer(0.2), 1000, Provider(1000)) == 1200 8 | assert get_month_charge(Customer(0.2), 1000, Provider(1500)) == 1700 9 | assert get_month_charge(Customer(0.3), 1000, Provider(1500)) == 1800 10 | assert get_month_charge(Customer(0.3), 1525, Provider(1500)) == 1957.5 11 | -------------------------------------------------------------------------------- /Chapter11/11-return-modified-value/ascent_calculator.py: -------------------------------------------------------------------------------- 1 | class AscentCalculator: 2 | def __init__(self, points) -> None: 3 | self._points = points 4 | self._total_ascent = self.calculate_ascent() 5 | 6 | @property 7 | def total_ascent(self): 8 | return self._total_ascent 9 | 10 | def calculate_ascent(self): 11 | result = 0 12 | for i in range(1, len(self._points)): 13 | vertical_change = ( 14 | self._points[i].elevation - self._points[i - 1].elevation 15 | ) 16 | result += vertical_change if vertical_change > 0 else 0 17 | return result 18 | -------------------------------------------------------------------------------- /Chapter11/11-return-modified-value/point.py: -------------------------------------------------------------------------------- 1 | class Point: 2 | def __init__(self, elevation) -> None: 3 | self._elevation = elevation 4 | 5 | @property 6 | def elevation(self): 7 | return self._elevation 8 | -------------------------------------------------------------------------------- /Chapter11/11-return-modified-value/test_ascent_calculator.py: -------------------------------------------------------------------------------- 1 | from ascent_calculator import AscentCalculator 2 | from point import Point 3 | 4 | 5 | def points(nums): 6 | return [Point(n) for n in nums] 7 | 8 | 9 | def test_total_ascent(): 10 | assert AscentCalculator(points([1, 2, 3, 4, 5])).total_ascent == 4 11 | assert AscentCalculator(points([1, 2, 1, 7, 2])).total_ascent == 7 12 | assert AscentCalculator(points([3, 2, 7, 5, 11])).total_ascent == 11 13 | -------------------------------------------------------------------------------- /Chapter11/12-replace-error-code-with-exception/country_data.py: -------------------------------------------------------------------------------- 1 | shipping_rules = { 2 | "KO": 1, 3 | "JP": 2, 4 | "CN": 3, 5 | } 6 | -------------------------------------------------------------------------------- /Chapter11/12-replace-error-code-with-exception/error.py: -------------------------------------------------------------------------------- 1 | from distutils.log import error 2 | 3 | 4 | class OrderProcessingError(Exception): 5 | def __init__(self, error_code) -> None: 6 | super().__init__(f"Order Processing Error: {error_code}") 7 | self._code = error_code 8 | 9 | @property 10 | def code(self): 11 | return self._code 12 | 13 | @property 14 | def name(self): 15 | return "OrderProcessingError" 16 | -------------------------------------------------------------------------------- /Chapter11/12-replace-error-code-with-exception/example.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | import country_data 4 | from error import OrderProcessingError 5 | from shipping_rules import ShippingRules 6 | 7 | _error_list = [] 8 | 9 | 10 | def get_order_shipping_cost(order_data): 11 | try: 12 | return calculate_shipping_costs(order_data) 13 | except Exception as e: 14 | if isinstance(e, OrderProcessingError): 15 | _error_list.append({"order": order_data, "error_code": e.code}) 16 | return e.code 17 | else: 18 | raise e 19 | 20 | 21 | def get_error_list(): 22 | return deepcopy(_error_list) 23 | 24 | 25 | def calculate_shipping_costs(order): 26 | shipping_rules = local_shipping_rules(order.country) 27 | return 100 28 | 29 | 30 | def local_shipping_rules(country): 31 | data = country_data.shipping_rules.get(country) 32 | if data: 33 | return ShippingRules(data) 34 | else: 35 | raise OrderProcessingError(-23) 36 | -------------------------------------------------------------------------------- /Chapter11/12-replace-error-code-with-exception/order.py: -------------------------------------------------------------------------------- 1 | class Order: 2 | def __init__(self, country) -> None: 3 | self._country = country 4 | 5 | @property 6 | def country(self): 7 | return self._country 8 | 9 | def __eq__(self, __o: object) -> bool: 10 | return self._country == __o._country 11 | -------------------------------------------------------------------------------- /Chapter11/12-replace-error-code-with-exception/shipping_rules.py: -------------------------------------------------------------------------------- 1 | class ShippingRules: 2 | def __init__(self, rule) -> None: 3 | self._rule = rule 4 | -------------------------------------------------------------------------------- /Chapter11/12-replace-error-code-with-exception/test_example.py: -------------------------------------------------------------------------------- 1 | from example import get_order_shipping_cost, get_error_list 2 | from order import Order 3 | 4 | 5 | def test_get_order_shipping_cost(): 6 | assert get_order_shipping_cost(Order("KO")) == 100 7 | assert get_order_shipping_cost(Order("JP")) == 100 8 | assert get_order_shipping_cost(Order("CN")) == 100 9 | 10 | 11 | def test_get_order_shipping_cost_with_err(): 12 | assert get_order_shipping_cost(Order("USA")) == -23 13 | error = get_error_list()[-1] 14 | assert error["order"] == Order("USA") 15 | assert error["error_code"] == -23 16 | 17 | -------------------------------------------------------------------------------- /Chapter11/13-replace-exception-with-precheck/resource_pool.py: -------------------------------------------------------------------------------- 1 | class ResourcePool: 2 | def __init__(self) -> None: 3 | self._available = [] 4 | self._allocated = [] 5 | 6 | def get(self): 7 | result = self._available.pop() if self._available else Resource() 8 | self._allocated.append(result) 9 | return result 10 | 11 | def add(self): 12 | self._available.append(Resource()) 13 | 14 | 15 | class Resource: 16 | __id = 0 17 | 18 | def __init__(self) -> None: 19 | self._id = Resource.__id 20 | Resource.__id += 1 21 | 22 | @property 23 | def id(self): 24 | return self._id 25 | 26 | @staticmethod 27 | def reset(): 28 | Resource.__id = 0 29 | -------------------------------------------------------------------------------- /Chapter11/13-replace-exception-with-precheck/test_resource.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from resource_pool import Resource, ResourcePool 4 | 5 | 6 | @pytest.fixture 7 | def resource_pool(): 8 | yield ResourcePool() 9 | Resource.reset() 10 | 11 | 12 | def test_add_resouce(resource_pool): 13 | for _ in range(3): 14 | resource_pool.add() 15 | 16 | for i in range(2, -1, -1): 17 | assert resource_pool.get().id == i 18 | 19 | 20 | def test_get_resource_without_add(resource_pool): 21 | for i in range(3): 22 | assert resource_pool.get().id == i 23 | -------------------------------------------------------------------------------- /Chapter11/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 11: Refactoring APIs 2 | 3 | ## Contents 4 | 5 | 1. Separate Query from Modifier 6 | 2. Parameterize Function 7 | 3. Remove Flag Argument 8 | 4. Preserve Whole Object 9 | 5. Replace Parameter with Query 10 | 6. Replace Query with Parameter 11 | 7. Remove Setting Method 12 | 8. Replace Constructor with Factory Function 13 | 9. Replace Function with Command 14 | 10. Replace Command with Function 15 | -------------------------------------------------------------------------------- /Chapter12/01-pull-up-method/department.py: -------------------------------------------------------------------------------- 1 | from party import Party 2 | 3 | 4 | class Department(Party): 5 | @property 6 | def monthly_cost(self): 7 | return 1000 8 | -------------------------------------------------------------------------------- /Chapter12/01-pull-up-method/employee.py: -------------------------------------------------------------------------------- 1 | from party import Party 2 | 3 | 4 | class Employee(Party): 5 | @property 6 | def monthly_cost(self): 7 | return 100 8 | -------------------------------------------------------------------------------- /Chapter12/01-pull-up-method/party.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Party(ABC): 5 | @property 6 | @abstractmethod 7 | def monthly_cost(self): 8 | raise NotImplementedError( 9 | "A subclass of 'Party' must implement this property." 10 | ) 11 | 12 | @property 13 | def annual_cost(self): 14 | return self.monthly_cost * 12 15 | -------------------------------------------------------------------------------- /Chapter12/01-pull-up-method/test_party.py: -------------------------------------------------------------------------------- 1 | from department import Department 2 | from employee import Employee 3 | 4 | 5 | def test_department_annual_cost(): 6 | assert Department().annual_cost == 12000 7 | 8 | 9 | def test_employee_annual_cost(): 10 | assert Employee().annual_cost == 1200 11 | -------------------------------------------------------------------------------- /Chapter12/03-pull-up-constructor-body/example1/department.py: -------------------------------------------------------------------------------- 1 | from party import Party 2 | 3 | 4 | class Department(Party): 5 | def __init__(self, name, staff) -> None: 6 | super().__init__(name) 7 | self._staff = staff 8 | 9 | @property 10 | def name(self): 11 | return self._name 12 | 13 | @property 14 | def staff(self): 15 | return self._staff 16 | -------------------------------------------------------------------------------- /Chapter12/03-pull-up-constructor-body/example1/employee.py: -------------------------------------------------------------------------------- 1 | from party import Party 2 | 3 | 4 | class Employee(Party): 5 | def __init__(self, name, id, monthly_cost) -> None: 6 | super().__init__(name) 7 | self._id = id 8 | self._monthly_cost = monthly_cost 9 | 10 | @property 11 | def name(self): 12 | return self._name 13 | 14 | @property 15 | def id(self): 16 | return self._id 17 | 18 | @property 19 | def monthly_cost(self): 20 | return self._monthly_cost 21 | -------------------------------------------------------------------------------- /Chapter12/03-pull-up-constructor-body/example1/party.py: -------------------------------------------------------------------------------- 1 | class Party: 2 | def __init__(self, name) -> None: 3 | self._name = name 4 | -------------------------------------------------------------------------------- /Chapter12/03-pull-up-constructor-body/example1/test_party.py: -------------------------------------------------------------------------------- 1 | from department import Department 2 | from employee import Employee 3 | 4 | 5 | def test_get_name_department(): 6 | assert ( 7 | Department("Team1", Employee("Minwoo Jeong", 1, 1000)).name == "Team1" 8 | ) 9 | 10 | 11 | def test_get_name_employee(): 12 | assert Employee("Minwoo Jeong", 1, 1000).name == "Minwoo Jeong" 13 | -------------------------------------------------------------------------------- /Chapter12/03-pull-up-constructor-body/example2/employee.py: -------------------------------------------------------------------------------- 1 | class Employee: 2 | def __init__(self, name) -> None: 3 | self._name = name 4 | 5 | def finish_construction(self): 6 | if self.is_privileged: 7 | self.assign_car() 8 | 9 | @property 10 | def is_privileged(self): 11 | pass 12 | 13 | def assign_car(self): 14 | pass 15 | -------------------------------------------------------------------------------- /Chapter12/03-pull-up-constructor-body/example2/manager.py: -------------------------------------------------------------------------------- 1 | from employee import Employee 2 | 3 | 4 | class Manager(Employee): 5 | def __init__(self, name, grade) -> None: 6 | super().__init__(name) 7 | self._grade = grade 8 | self.finish_construction() 9 | 10 | @property 11 | def is_privileged(self): 12 | return self._grade > 4 13 | -------------------------------------------------------------------------------- /Chapter12/06-replace-type-code-with-subclasses/example1/employee.py: -------------------------------------------------------------------------------- 1 | class Employee: 2 | def __init__(self, name) -> None: 3 | self._name = name 4 | 5 | 6 | class Engineer(Employee): 7 | def __str__(self) -> str: 8 | return f"{self._name} (engineer)" 9 | 10 | 11 | class Manager(Employee): 12 | def __str__(self) -> str: 13 | return f"{self._name} (manager)" 14 | 15 | 16 | class Salesperson(Employee): 17 | def __str__(self) -> str: 18 | return f"{self._name} (salesperson)" 19 | 20 | 21 | def create_employee(name, type): 22 | if type == "engineer": 23 | return Engineer(name) 24 | if type == "manager": 25 | return Manager(name) 26 | if type == "salesperson": 27 | return Salesperson(name) 28 | raise ValueError(f'There is no "{type}" employee type.') 29 | -------------------------------------------------------------------------------- /Chapter12/06-replace-type-code-with-subclasses/example1/test_employee.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from employee import create_employee 4 | 5 | 6 | def test_employee_str(): 7 | assert ( 8 | str(create_employee("Minwoo Jeong", "engineer")) 9 | == "Minwoo Jeong (engineer)" 10 | ) 11 | assert ( 12 | str(create_employee("Minwoo Jeong", "manager")) 13 | == "Minwoo Jeong (manager)" 14 | ) 15 | assert ( 16 | str(create_employee("Minwoo Jeong", "salesperson")) 17 | == "Minwoo Jeong (salesperson)" 18 | ) 19 | 20 | 21 | def test_employee_with_error(): 22 | with pytest.raises(ValueError): 23 | create_employee("Minwoo Jeong", "Other") 24 | -------------------------------------------------------------------------------- /Chapter12/06-replace-type-code-with-subclasses/example2/employee.py: -------------------------------------------------------------------------------- 1 | class EmployeeType: 2 | @property 3 | def capitalized_name(self): 4 | return str(self).capitalize() 5 | 6 | 7 | class Engineer(EmployeeType): 8 | def __str__(self) -> str: 9 | return "engineer" 10 | 11 | 12 | class Manager(EmployeeType): 13 | def __str__(self) -> str: 14 | return "manager" 15 | 16 | 17 | class Salesperson(EmployeeType): 18 | def __str__(self) -> str: 19 | return "salesperson" 20 | 21 | 22 | class Employee: 23 | def __init__(self, name, type) -> None: 24 | self._name = name 25 | self.type = type 26 | 27 | @property 28 | def type(self): 29 | return self._type 30 | 31 | @type.setter 32 | def type(self, value): 33 | self._type = Employee.create_employee_type(value) 34 | 35 | @staticmethod 36 | def create_employee_type(value): 37 | if value == "engineer": 38 | return Engineer() 39 | if value == "manager": 40 | return Manager() 41 | if value == "salesperson": 42 | return Salesperson() 43 | raise ValueError(f'There is no "{value}" employee type.') 44 | 45 | def __str__(self) -> str: 46 | return f"{self._name} ({self.type.capitalized_name})" 47 | -------------------------------------------------------------------------------- /Chapter12/06-replace-type-code-with-subclasses/example2/test_employee.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from employee import Employee 4 | 5 | 6 | def test_employee_str(): 7 | assert ( 8 | str(Employee("Minwoo Jeong", "engineer")) == "Minwoo Jeong (Engineer)" 9 | ) 10 | assert str(Employee("Minwoo Jeong", "manager")) == "Minwoo Jeong (Manager)" 11 | assert ( 12 | str(Employee("Minwoo Jeong", "salesperson")) 13 | == "Minwoo Jeong (Salesperson)" 14 | ) 15 | 16 | 17 | def test_employee_with_error(): 18 | with pytest.raises(ValueError): 19 | Employee("Minwoo Jeong", "Other") 20 | -------------------------------------------------------------------------------- /Chapter12/07-remove-subclass/client.py: -------------------------------------------------------------------------------- 1 | def get_number_of_males(people): 2 | return len(list(filter(lambda p: p.is_male, people))) 3 | -------------------------------------------------------------------------------- /Chapter12/07-remove-subclass/person.py: -------------------------------------------------------------------------------- 1 | class Person: 2 | def __init__(self, name, gender_code) -> None: 3 | self._name = name 4 | self._gender_code = gender_code 5 | 6 | @property 7 | def name(self): 8 | return self._name 9 | 10 | @property 11 | def gender_code(self): 12 | return self._gender_code 13 | 14 | @property 15 | def is_male(self): 16 | return self._gender_code == "M" 17 | 18 | 19 | def create_person(record): 20 | if record["gender"] == "M": 21 | return Person(record["name"], "M") 22 | if record["gender"] == "F": 23 | return Person(record["name"], "F") 24 | return Person(record["name"], "X") 25 | -------------------------------------------------------------------------------- /Chapter12/07-remove-subclass/test_client.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from client import get_number_of_males 4 | from person import create_person 5 | 6 | 7 | @pytest.fixture 8 | def data(): 9 | return [ 10 | {"name": "Person1", "gender": "M"}, 11 | {"name": "Person2", "gender": "M"}, 12 | {"name": "Person3", "gender": "F"}, 13 | {"name": "Person4", "gender": "F"}, 14 | {"name": "Person5", "gender": "M"}, 15 | {"name": "Person6", "gender": "X"}, 16 | ] 17 | 18 | 19 | @pytest.fixture 20 | def people(data): 21 | return [create_person(record) for record in data] 22 | 23 | 24 | def test_get_number_of_males(people): 25 | assert get_number_of_males(people) == 3 26 | -------------------------------------------------------------------------------- /Chapter12/08-extract-superclass/department.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | from functools import reduce 3 | 4 | from party import Party 5 | 6 | 7 | class Department(Party): 8 | def __init__(self, name, staff) -> None: 9 | super().__init__(name) 10 | self._staff = staff 11 | 12 | @property 13 | def staff(self): 14 | return copy(self._staff) 15 | 16 | @property 17 | def monthly_cost(self): 18 | return reduce( 19 | lambda sum, cost: sum + cost, 20 | map(lambda e: e.monthly_cost, self._staff), 21 | 0, 22 | ) 23 | 24 | @property 25 | def head_count(self): 26 | return len(self.staff) 27 | -------------------------------------------------------------------------------- /Chapter12/08-extract-superclass/employee.py: -------------------------------------------------------------------------------- 1 | from party import Party 2 | 3 | 4 | class Employee(Party): 5 | def __init__(self, id, name, monthly_cost) -> None: 6 | super().__init__(name) 7 | self._id = id 8 | self._monthly_cost = monthly_cost 9 | 10 | @property 11 | def monthly_cost(self): 12 | return self._monthly_cost 13 | 14 | @property 15 | def id(self): 16 | return self._id 17 | -------------------------------------------------------------------------------- /Chapter12/08-extract-superclass/party.py: -------------------------------------------------------------------------------- 1 | class Party: 2 | def __init__(self, name) -> None: 3 | self._name = name 4 | 5 | @property 6 | def name(self): 7 | return self._name 8 | 9 | @property 10 | def annual_cost(self): 11 | return self.monthly_cost * 12 12 | -------------------------------------------------------------------------------- /Chapter12/08-extract-superclass/test_example.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from department import Department 4 | from employee import Employee 5 | 6 | 7 | def test_employee_annual_cost(): 8 | assert Employee(1, "Minwoo Jeong", 500).annual_cost == 6000 9 | 10 | 11 | @pytest.fixture 12 | def staff(): 13 | return [ 14 | Employee(1, "A", 100), 15 | Employee(2, "B", 200), 16 | Employee(3, "C", 300), 17 | Employee(4, "D", 400), 18 | Employee(5, "E", 500), 19 | ] 20 | 21 | 22 | def test_department_monthly_cost(staff): 23 | assert Department("Dept", staff).monthly_cost == 1500 24 | 25 | 26 | def test_department_annual_cost(staff): 27 | assert Department("Dept", staff).annual_cost == 18000 28 | -------------------------------------------------------------------------------- /Chapter12/10-replace-subclass-with-delegate/example1/booking.py: -------------------------------------------------------------------------------- 1 | class Booking: 2 | def __init__(self, show, date) -> None: 3 | self._show = show 4 | self._date = date 5 | 6 | @property 7 | def is_peak_day(self): 8 | if self._date.weekday() >= 5: # weekend 9 | return True 10 | return False 11 | 12 | @property 13 | def is_premium(self): 14 | return hasattr(self, "_premium_delegate") 15 | 16 | @property 17 | def has_talkback(self): 18 | if self.is_premium: 19 | return self._premium_delegate.has_talkback 20 | return hasattr(self._show, "talkback") and not self.is_peak_day 21 | 22 | @property 23 | def base_price(self): 24 | result = self._show.price 25 | if self.is_peak_day: 26 | result += round(result * 0.15) 27 | if self.is_premium: 28 | return self._premium_delegate.extend_base_price(result) 29 | return result 30 | 31 | @property 32 | def has_dinner(self): 33 | if self.is_premium: 34 | return self._premium_delegate.has_dinner 35 | raise RuntimeError( 36 | "The Booking class without being premium " 37 | "does not have 'has_dinner' property." 38 | ) 39 | 40 | def _be_premium(self, extras): 41 | self._premium_delegate = PremiumBookingDelegate(self, extras) 42 | 43 | 44 | class PremiumBookingDelegate: 45 | def __init__(self, host_booking, extras) -> None: 46 | self._host = host_booking 47 | self._extras = extras 48 | 49 | @property 50 | def has_talkback(self): 51 | return hasattr(self._host._show, "talkback") 52 | 53 | def extend_base_price(self, base_price): 54 | return round(base_price + self._extras.premium_fee) 55 | 56 | @property 57 | def has_dinner(self): 58 | return hasattr(self._extras, "dinner") 59 | 60 | 61 | def create_booking(show, date): 62 | return Booking(show, date) 63 | 64 | 65 | def create_premium_booking(show, date, extras): 66 | result = Booking(show, date) 67 | result._be_premium(extras) 68 | return result 69 | -------------------------------------------------------------------------------- /Chapter12/10-replace-subclass-with-delegate/example1/extras.py: -------------------------------------------------------------------------------- 1 | class Extras: 2 | def __init__(self, premium_fee) -> None: 3 | self._premium_fee = premium_fee 4 | 5 | @property 6 | def premium_fee(self): 7 | return self._premium_fee 8 | 9 | def set_dinner(self): 10 | self.dinner = True 11 | -------------------------------------------------------------------------------- /Chapter12/10-replace-subclass-with-delegate/example1/show.py: -------------------------------------------------------------------------------- 1 | class Show: 2 | def __init__(self, name, price) -> None: 3 | self._name = name 4 | self._price = price 5 | 6 | @property 7 | def name(self): 8 | return self._name 9 | 10 | @property 11 | def price(self): 12 | return self._price 13 | 14 | def set_talkback(self): 15 | self.talkback = True 16 | -------------------------------------------------------------------------------- /Chapter12/10-replace-subclass-with-delegate/example1/test_booking.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | from booking import create_booking, create_premium_booking 6 | from extras import Extras 7 | from show import Show 8 | 9 | 10 | @pytest.fixture 11 | def weekday(): 12 | return datetime.strptime("16 02 2022", "%d %m %Y") 13 | 14 | 15 | @pytest.fixture 16 | def weekend(): 17 | return datetime.strptime("19 02 2022", "%d %m %Y") 18 | 19 | 20 | @pytest.fixture 21 | def show(): 22 | return Show("Show!", 1000) 23 | 24 | 25 | @pytest.fixture 26 | def extras(): 27 | return Extras(200) 28 | 29 | 30 | def test_booking_is_peak_day(show, weekday, weekend): 31 | assert not create_booking(show, weekday).is_peak_day 32 | assert create_booking(show, weekend).is_peak_day 33 | 34 | 35 | def test_booking_has_talkback(show, weekday, weekend): 36 | assert not create_booking(show, weekday).has_talkback 37 | show.set_talkback() 38 | assert create_booking(show, weekday).has_talkback 39 | assert not create_booking(show, weekend).has_talkback 40 | 41 | 42 | def test_booking_base_price(show, weekday, weekend): 43 | assert create_booking(show, weekday).base_price == 1000 44 | assert create_booking(show, weekend).base_price == 1150 45 | 46 | 47 | def test_premium_booking_has_talkback(show, weekday, weekend, extras): 48 | assert not create_premium_booking(show, weekday, extras).has_talkback 49 | show.set_talkback() 50 | assert create_premium_booking(show, weekday, extras).has_talkback 51 | assert create_premium_booking(show, weekend, extras).has_talkback 52 | 53 | 54 | def test_premium_booking_base_price(show, weekday, weekend, extras): 55 | assert create_premium_booking(show, weekday, extras).base_price == 1200 56 | assert create_premium_booking(show, weekend, extras).base_price == 1350 57 | 58 | 59 | def test_premium_booking_has_dinner(show, weekday, extras): 60 | assert not create_premium_booking(show, weekday, extras).has_dinner 61 | extras.set_dinner() 62 | assert create_premium_booking(show, weekday, extras).has_dinner 63 | -------------------------------------------------------------------------------- /Chapter12/10-replace-subclass-with-delegate/example2/bird.py: -------------------------------------------------------------------------------- 1 | class Bird: 2 | def __init__(self, data) -> None: 3 | self._name = data.get("name") 4 | self._plumage = data.get("plumage") 5 | self._species_delegate = self.select_species_delegate(data) 6 | 7 | def select_species_delegate(self, data): 8 | if data.get("type") == "EuropeanSwallow": 9 | return EuropeanSwallowDelegate(data, self) 10 | elif data.get("type") == "AfricanSwallow": 11 | return AfricanSwallowDelegate(data, self) 12 | elif data.get("type") == "NorwegianBlueParrot": 13 | return NorwegianBlueParrotDelegate(data, self) 14 | return SpeciesDelegate(data, self) 15 | 16 | @property 17 | def has_species_delegate(self): 18 | return self._species_delegate is not None 19 | 20 | @property 21 | def name(self): 22 | return self._name 23 | 24 | @property 25 | def plumage(self): 26 | return self._species_delegate.plumage 27 | 28 | @property 29 | def air_speed_velocity(self): 30 | return self._species_delegate.air_speed_velocity 31 | 32 | 33 | class SpeciesDelegate: 34 | def __init__(self, data, bird) -> None: 35 | self._bird = bird 36 | 37 | @property 38 | def plumage(self): 39 | return self._bird._plumage if self._bird._plumage else "Normal" 40 | 41 | @property 42 | def air_speed_velocity(self): 43 | return None 44 | 45 | 46 | class EuropeanSwallowDelegate(SpeciesDelegate): 47 | @property 48 | def air_speed_velocity(self): 49 | return 35 50 | 51 | 52 | class AfricanSwallowDelegate(SpeciesDelegate): 53 | def __init__(self, data, bird) -> None: 54 | super().__init__(data, bird) 55 | self._number_of_coconuts = data.get("number_of_coconuts") 56 | 57 | @property 58 | def air_speed_velocity(self): 59 | return 40 - 2 * self._number_of_coconuts 60 | 61 | 62 | class NorwegianBlueParrotDelegate(SpeciesDelegate): 63 | def __init__(self, data, bird) -> None: 64 | super().__init__(data, bird) 65 | self._voltage = data.get("voltage") 66 | self._is_nailed = data.get("is_nailed") 67 | 68 | @property 69 | def plumage(self): 70 | if self._voltage > 100: 71 | return "Scorched" 72 | else: 73 | return self._bird._plumage if self._bird._plumage else "Beautiful" 74 | 75 | @property 76 | def air_speed_velocity(self): 77 | return 0 if self._is_nailed else 10 + self._voltage / 10 78 | 79 | 80 | def create_bird(data): 81 | return Bird(data) 82 | -------------------------------------------------------------------------------- /Chapter12/10-replace-subclass-with-delegate/example2/test_bird.py: -------------------------------------------------------------------------------- 1 | from bird import create_bird 2 | 3 | 4 | def test_bird(): 5 | bird = create_bird({"type": "Other", "name": "Bird1", "plumage": "Wow"}) 6 | assert bird.plumage == "Wow" 7 | assert bird.air_speed_velocity is None 8 | 9 | bird = create_bird({"type": "Other", "name": "Bird2"}) 10 | assert bird.plumage == "Normal" 11 | 12 | 13 | def test_european_swallow(): 14 | bird = create_bird({"type": "EuropeanSwallow", "name": "Bird1"}) 15 | assert bird.air_speed_velocity == 35 16 | 17 | 18 | def test_african_swallow(): 19 | bird = create_bird( 20 | {"type": "AfricanSwallow", "name": "Bird1", "number_of_coconuts": 2} 21 | ) 22 | assert bird.air_speed_velocity == 36 23 | 24 | 25 | def test_norwegian_blue_parrot(): 26 | bird_data = { 27 | "type": "NorwegianBlueParrot", 28 | "name": "Bird1", 29 | "voltage": 150, 30 | "is_nailed": True, 31 | } 32 | bird = create_bird(bird_data) 33 | assert bird.plumage == "Scorched" 34 | assert bird.air_speed_velocity == 0 35 | 36 | bird_data["is_nailed"] = False 37 | bird = create_bird(bird_data) 38 | assert bird.air_speed_velocity == 25 39 | 40 | bird_data["voltage"] = 50 41 | bird = create_bird(bird_data) 42 | assert bird.plumage == "Beautiful" 43 | 44 | bird_data["plumage"] = "Wow" 45 | bird = create_bird(bird_data) 46 | assert bird.plumage == "Wow" 47 | -------------------------------------------------------------------------------- /Chapter12/11-replace-superclass-with-delegate/catalog_item.py: -------------------------------------------------------------------------------- 1 | class CatalogItem: 2 | def __init__(self, id, title, tags) -> None: 3 | self._id = id 4 | self._title = title 5 | self._tags = tags 6 | 7 | @property 8 | def id(self): 9 | return self._id 10 | 11 | @property 12 | def title(self): 13 | return self._title 14 | 15 | def has_tag(self, tag): 16 | return tag in self._tags 17 | -------------------------------------------------------------------------------- /Chapter12/11-replace-superclass-with-delegate/scroll.py: -------------------------------------------------------------------------------- 1 | class Scroll: 2 | def __init__(self, id, date_last_cleaned, catalog_id, catalog) -> None: 3 | self._id = id 4 | self._catalog_item = catalog.get(catalog_id) 5 | self._last_cleaned = date_last_cleaned 6 | 7 | @property 8 | def id(self): 9 | return self._id 10 | 11 | @property 12 | def title(self): 13 | return self._catalog_item.title 14 | 15 | def has_tag(self, tag): 16 | return self._catalog_item.has_tag(tag) 17 | 18 | def needs_cleaning(self, target_date): 19 | threshold = 700 if self.has_tag("revered") else 1500 20 | return self.days_since_last_cleaning(target_date) > threshold 21 | 22 | def days_since_last_cleaning(self, target_date): 23 | return (target_date - self._last_cleaned).days 24 | -------------------------------------------------------------------------------- /Chapter12/11-replace-superclass-with-delegate/test_scroll.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | 3 | from catalog_item import CatalogItem 4 | from scroll import Scroll 5 | 6 | 7 | def test_days_since_last_cleaning(): 8 | expect = 600 9 | target_date = date.today() 10 | last_cleaned_date = target_date - timedelta(days=expect) 11 | catalog = {1: CatalogItem(1, "Catalog1", [])} 12 | assert ( 13 | Scroll(0, last_cleaned_date, 1, catalog).days_since_last_cleaning( 14 | target_date 15 | ) 16 | == expect 17 | ) 18 | 19 | 20 | def test_needs_cleaning(): 21 | target_date = date.today() 22 | catalog = {1: CatalogItem(1, "Catalog1", [])} 23 | assert not Scroll( 24 | 1, target_date - timedelta(days=1500), 1, catalog 25 | ).needs_cleaning(target_date) 26 | assert Scroll( 27 | 2, target_date - timedelta(days=1501), 1, catalog 28 | ).needs_cleaning(target_date) 29 | 30 | 31 | def test_needs_cleaning_with_revered_tag(): 32 | target_date = date.today() 33 | catalog = {1: CatalogItem(1, "Catalog1", ["revered"])} 34 | assert not Scroll( 35 | 1, target_date - timedelta(days=700), 1, catalog 36 | ).needs_cleaning(target_date) 37 | assert Scroll( 38 | 2, target_date - timedelta(days=701), 1, catalog 39 | ).needs_cleaning(target_date) 40 | -------------------------------------------------------------------------------- /Chapter12/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 12: Dealing with Inheritance 2 | 3 | ## Contents 4 | 5 | 1. Pull Up Method 6 | 2. Pull Up Field 7 | 3. Pull Up Constructor Body 8 | 4. Push Down Method 9 | 5. Push Down Field 10 | 6. Repalce Type Code with Subclasses 11 | 7. Remove Subclass 12 | 8. Extract Superclass 13 | 9. Collapse Hierarchy 14 | 10. Replace Subclass with Delegate 15 | 11. Replace Superclass with Delegate 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Refactoring Python 2 | 3 | ## What does this repository do? 4 | 5 | - Review _[Refactoring 2nd Edition](https://www.amazon.com/Refactoring-Improving-Existing-Addison-Wesley-Signature/dp/0134757599)_ 6 | - Contain _example source code written in python_ in order to study refactoring techniques 7 | - Make you understand each refactoring technique much clearer 8 | 9 | ## How to use this repository? 10 | 11 | - Follow commit history of each directory corresponding to each refactoring technique. 12 | - Each commit corresponds to each step of a refactoring technique. 13 | - e.g. Follow the commit history of [Chapter12/06-replace-type-code-with-subclasses](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter12/06-replace-type-code-with-subclasses) if you want to study the refactoring technique _Replace Type Code with Subclasses_. 14 | 15 | ## Contents 16 | 17 | - [Chapter 1. Refactoring: A First Example](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter01) 18 | - [Chapter 4. Building Tests](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter04) 19 | - [Chapter 6. A First Set of Refactorings](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter06) 20 | - [Chapter 7. Encapsulation](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter07) 21 | - [Chapter 8. Moving Features](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter08) 22 | - [Chapter 9. Organizing Data](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter09) 23 | - [Chapter 10. Simplifying Conditional Logic](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter10) 24 | - [Chapter 11. Refactoring APIs](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter11) 25 | - [Chapter 12. Dealing with Inheritance](https://github.com/mwjjeong/refactoring-python/tree/main/Chapter12) 26 | 27 | Some chapters were omitted if there was no example source code. 28 | 29 | ## Notice 30 | 31 | Feel free to ask a question or report a problem via the [Issue tab](https://github.com/mwjjeong/refactoring-python/issues). 32 | --------------------------------------------------------------------------------