├── .gitattributes ├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── LICENSE ├── Makefile ├── README.md ├── conftest.py └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | *.md linguist-documentation=false 2 | *.md linguist-language=Python 3 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.10 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: "3.10" 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | - name: Test with pytest 35 | run: | 36 | pytest 37 | -------------------------------------------------------------------------------- /.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 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # VSCODE 128 | .vscode 129 | .idea 130 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8.3" 4 | # command to install dependencies 5 | install: 6 | - make deps 7 | # command to run tests 8 | script: 9 | - make tests 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Rigel Di Scala 2 | Zachary Anglin 3 | AirbusDriver 4 | Micheal 5 | Erik OShaughnessy 6 | Mukhammad Karimov 7 | sitnarf 8 | Miguel Gonzalez 9 | Anvar 10 | Martin Pavlásek 11 | Shahrukh Khan 12 | Aaron Law 13 | Fredson Chaves 14 | MartinThoma 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryan McDermott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps clean tests 2 | 3 | ENV=.env 4 | PYTHON=python3 5 | PYTHON_VERSION=$(shell ${PYTHON} -V | cut -d " " -f 2 | cut -c1-3) 6 | SITE_PACKAGES=${ENV}/lib/python${PYTHON_VERSION}/site-packages 7 | IN_ENV=source ${ENV}/bin/activate; 8 | 9 | default: tests 10 | 11 | ${ENV}: 12 | @echo "Creating Python environment..." >&2 13 | @${PYTHON} -m venv ${ENV} 14 | @echo "Updating pip..." >&2 15 | @${IN_ENV} pip install -U pip 16 | 17 | ${SITE_PACKAGES}/pytest.py: 18 | @${IN_ENV} pip install -r requirements.txt 19 | 20 | deps: ${SITE_PACKAGES}/pytest.py 21 | 22 | tests: ${ENV} ${SITE_PACKAGES}/pytest.py 23 | @${IN_ENV} pytest 24 | 25 | clean: 26 | @rm -rf ${ENV} .env .pytest_cache 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # کد تمیز در پایتون 2 | 3 | ## فهرست مطالب 4 | 1. [مقدمه](#مقدمه) 5 | 2. [متغیر ها](#متغیر-ها) 6 | 3. [توابع](#توابع) 7 | 5. [کلاس ها](#کلاس-ها) 8 | * [S: Single Responsibility Principle (SRP)](#single-responsibility-principle-srp) 9 | * [O: Open/Closed Principle (OCP)](#openclosed-principle-ocp) 10 | * [L: Liskov Substitution Principle (LSP)](#liskov-substitution-principle-lsp) 11 | * [I: Interface Segregation Principle (ISP)](#interface-segregation-principle-isp) 12 | * [D: Dependency Inversion Principle (DIP)](#dependency-inversion-principle-dip) 13 | 6. [Don't repeat yourself (DRY)](#dont-repeat-yourself-dry) 14 | 15 | ## مقدمه 16 | 17 | اصول مهندسی نرم افزار، از کتاب [*کد تمیز*](https://www.digikala.com/product/dkp-4964829/%DA%A9%D8%AA%D8%A7%D8%A8-clean-code-a-handbook-of-agile-software-craftsmanship-%D8%A7%D8%AB%D8%B1-robert-c-martin-%D8%A7%D9%86%D8%AA%D8%B4%D8%A7%D8%B1%D8%A7%D8%AA-pearson/) نوشته ی Robert C. Martin، برای پایتون. این یک راهنمای تولید نیست، این یک راهنما برای تولید نرم افزار های خوانا، قابل استفاده مجدد و قابل از نو بازسازی است. 18 | نیازی بر سرسختگیری بر هر اصل گفته شده در اینجا نیست، و فقط تعداد کمی از آنها به صورت عمومی مورد تایید هستند. این ها چیزی جز اصول نیستند، اما اصول کدنویسی شده ای توسط سالها تجربه از نویسنده های *کد تمیز* هستند. 19 | ## **متغیر ها** 20 | ### از اسم های متغیر با معنا و قابل تلفظ استفاده کنید 21 | 22 | **بد:** 23 | 24 | ```python 25 | import datetime 26 | 27 | 28 | ymdstr = datetime.date.today().strftime("%y-%m-%d") 29 | ``` 30 | 31 | به علاوه، نیازی به استفاده کردن تایپ `str` برای اسم نیست. 32 | 33 | **خوب**: 34 | 35 | ```python 36 | import datetime 37 | 38 | 39 | current_date: str = datetime.date.today().strftime("%y-%m-%d") 40 | ``` 41 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 42 | 43 | ### از واژگان یکسان برای همان نوع متغیر استفاده کنید 44 | **بد:** 45 | ما در اینجا از چند اسم برای یک موجود استفاده میکنیم: 46 | 47 | ```python 48 | def get_user_info(): pass 49 | def get_client_data(): pass 50 | def get_customer_record(): pass 51 | ``` 52 | 53 | **خوب**: 54 | اگر موجودیت یکسان است، باید در ارجاع به آن در توابع خود ثابت قدم باشید: 55 | 56 | ```python 57 | def get_user_info(): pass 58 | def get_user_data(): pass 59 | def get_user_record(): pass 60 | ``` 61 | 62 | **حتی بهتر** 63 | 64 | پایتون (همچنین) یک زبان برنامه نویسی شی گرا است. اگر منطقی است، توابع را همراه با پیاده‌سازی مشخص موجودیت در کد خود، به‌عنوان ویژگی‌های نمونه، متدهای ویژگی یا متدها بسته‌بندی کنید 65 | 66 | ```python 67 | from typing import Union, Dict 68 | 69 | 70 | class Record: 71 | pass 72 | 73 | 74 | class User: 75 | info : str 76 | 77 | @property 78 | def data(self) -> Dict[str, str]: 79 | return {} 80 | 81 | def get_record(self) -> Union[Record, None]: 82 | return Record() 83 | ``` 84 | 85 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 86 | 87 | ### از اسامی قابل جست و جو استفاده کنید 88 | ما بیشتر کد میخوانیم تا بنویسیم. این خیلی مهم است که کدی که مینویسیم خوانا و قابل جستجو باشد. با نام گذاری *نکردن* متغیر هایی که برای برنامه قابل فهم باشد، به خواننده هایمان آسیب میرسانیم. اسامی متغیرهایتان را قابل جست و جو کنید. 89 | 90 | **بد:** 91 | 92 | ```python 93 | import time 94 | 95 | 96 | # What is the number 86400 for again? 97 | time.sleep(86400) 98 | ``` 99 | 100 | **خوب**: 101 | 102 | ```python 103 | import time 104 | 105 | 106 | # Declare them in the global namespace for the module. 107 | SECONDS_IN_A_DAY = 60 * 60 * 24 108 | time.sleep(SECONDS_IN_A_DAY) 109 | ``` 110 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 111 | 112 | ### از متغیر های توضیحی استفاده کنید 113 | **بد:** 114 | 115 | ```python 116 | import re 117 | 118 | 119 | address = "One Infinite Loop, Cupertino 95014" 120 | city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$" 121 | 122 | matches = re.match(city_zip_code_regex, address) 123 | if matches: 124 | print(f"{matches[1]}: {matches[2]}") 125 | ``` 126 | 127 | **بد نیست**: 128 | 129 | بهتر است، اما هنوز به شدت به رجکس وابسته ایم. 130 | ```python 131 | import re 132 | 133 | 134 | address = "One Infinite Loop, Cupertino 95014" 135 | city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$" 136 | matches = re.match(city_zip_code_regex, address) 137 | 138 | if matches: 139 | city, zip_code = matches.groups() 140 | print(f"{city}: {zip_code}") 141 | ``` 142 | 143 | **خوب**: 144 | 145 | 146 | وابستگی به رجکس را با نام گذاری الگوهای فرعی کمتر کنید. 147 | 148 | ```python 149 | import re 150 | 151 | 152 | address = "One Infinite Loop, Cupertino 95014" 153 | city_zip_code_regex = r"^[^,\\]+[,\\\s]+(?P.+?)\s*(?P\d{5})?$" 154 | 155 | matches = re.match(city_zip_code_regex, address) 156 | if matches: 157 | print(f"{matches['city']}, {matches['zip_code']}") 158 | ``` 159 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 160 | 161 | ### از نقشه برداری ذهنی خودداری کنید 162 | 163 | خواننده ی کدتان را مجبور نکنید که اسامی متغیرهای شمارا ترجمه کنند. 164 | صریح بهتر از ضمنی است. 165 | 166 | **بد:** 167 | 168 | ```python 169 | seq = ("Austin", "New York", "San Francisco") 170 | 171 | for item in seq: 172 | #do_stuff() 173 | #do_some_other_stuff() 174 | 175 | # Wait, what's `item` again? 176 | print(item) 177 | ``` 178 | 179 | **خوب**: 180 | 181 | ```python 182 | locations = ("Austin", "New York", "San Francisco") 183 | 184 | for location in locations: 185 | #do_stuff() 186 | #do_some_other_stuff() 187 | # ... 188 | print(location) 189 | ``` 190 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 191 | 192 | 193 | ### توضیحات اضافی ننویسید 194 | 195 | اگر اسم کلاس/شیء شما چیزی را میگوید، نیازی نیست که آنرا در اسامی متغیر هایتان تکرار کنید. 196 | 197 | **بد:** 198 | 199 | ```python 200 | class Car: 201 | car_make: str 202 | car_model: str 203 | car_color: str 204 | ``` 205 | 206 | **خوب**: 207 | 208 | ```python 209 | class Car: 210 | make: str 211 | model: str 212 | color: str 213 | ``` 214 | 215 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 216 | 217 | ### از آرگومان های پیشفرض به جای شروط یک خطی استفاده کنید. 218 | 219 | **بد** 220 | 221 | چرا بنویسیم : 222 | 223 | ```python 224 | import hashlib 225 | 226 | 227 | def create_micro_brewery(name): 228 | name = "Hipster Brew Co." if name is None else name 229 | slug = hashlib.sha1(name.encode()).hexdigest() 230 | # etc. 231 | ``` 232 | 233 | ...زمانی که میتوانید از یک آرگومان پیشفرض استفاده کنید ؟ این همچنین خواننده را کاملا روشن میکند که شما برای آرگومانتان یک رشته میخواهید. 234 | 235 | **خوب**: 236 | 237 | ```python 238 | import hashlib 239 | 240 | 241 | def create_micro_brewery(name: str = "Hipster Brew Co."): 242 | slug = hashlib.sha1(name.encode()).hexdigest() 243 | # etc. 244 | ``` 245 | 246 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 247 | 248 | ### آرگومان های توابع (ترجیحاً دو یا کمتر) 249 | 250 | کم کردن مقادیر پارامتر های توابع بسیار مهم است چون تست کردن آنهارا راحت تر میکند. بیشتر از سه تا منجر به انفجاری ترکیبی میشود چون باید حالت های مختلف را با آرگومان های متفاوت تست کنید. 251 | 252 | آرگومان نداشتن حالت آیده آل است. یکی یا دوتا خوب است، و باید از سه تا اجتناب کرد. 253 | هرچیزی بیشتر از آن باید یک رقم شود. معمولا اگر بیشتر از دو آرگومان دارید، کد شما دارد چند کار را باهم انجام میدهد. در شرایطی که اینگونه نیست، اغلب اوقات یک شی سطح بالاتر به عنوان یک آرگومان کافی است. 254 | 255 | **بد:** 256 | 257 | ```python 258 | def create_menu(title, body, button_text, cancellable): 259 | pass 260 | ``` 261 | 262 | **به سبک جاوا**: 263 | 264 | ```python 265 | class Menu: 266 | def __init__(self, config: dict): 267 | self.title = config["title"] 268 | self.body = config["body"] 269 | # ... 270 | 271 | menu = Menu( 272 | { 273 | "title": "My Menu", 274 | "body": "Something about my menu", 275 | "button_text": "OK", 276 | "cancellable": False 277 | } 278 | ) 279 | ``` 280 | 281 | **این هم خوب است** 282 | 283 | ```python 284 | class MenuConfig: 285 | """A configuration for the Menu. 286 | 287 | Attributes: 288 | title: The title of the Menu. 289 | body: The body of the Menu. 290 | button_text: The text for the button label. 291 | cancellable: Can it be cancelled? 292 | """ 293 | title: str 294 | body: str 295 | button_text: str 296 | cancellable: bool = False 297 | 298 | 299 | def create_menu(config: MenuConfig) -> None: 300 | title = config.title 301 | body = config.body 302 | # ... 303 | 304 | 305 | config = MenuConfig() 306 | config.title = "My delicious menu" 307 | config.body = "A description of the various items on the menu" 308 | config.button_text = "Order now!" 309 | # The instance attribute overrides the default class attribute. 310 | config.cancellable = True 311 | 312 | create_menu(config) 313 | ``` 314 | 315 | **تفننی** 316 | 317 | ```python 318 | from typing import NamedTuple 319 | 320 | 321 | class MenuConfig(NamedTuple): 322 | """A configuration for the Menu. 323 | 324 | Attributes: 325 | title: The title of the Menu. 326 | body: The body of the Menu. 327 | button_text: The text for the button label. 328 | cancellable: Can it be cancelled? 329 | """ 330 | title: str 331 | body: str 332 | button_text: str 333 | cancellable: bool = False 334 | 335 | 336 | def create_menu(config: MenuConfig): 337 | title, body, button_text, cancellable = config 338 | # ... 339 | 340 | 341 | create_menu( 342 | MenuConfig( 343 | title="My delicious menu", 344 | body="A description of the various items on the menu", 345 | button_text="Order now!" 346 | ) 347 | ) 348 | ``` 349 | 350 | **تفننی تر** 351 | 352 | ```python 353 | from dataclasses import astuple, dataclass 354 | 355 | 356 | @dataclass 357 | class MenuConfig: 358 | """A configuration for the Menu. 359 | 360 | Attributes: 361 | title: The title of the Menu. 362 | body: The body of the Menu. 363 | button_text: The text for the button label. 364 | cancellable: Can it be cancelled? 365 | """ 366 | title: str 367 | body: str 368 | button_text: str 369 | cancellable: bool = False 370 | 371 | def create_menu(config: MenuConfig): 372 | title, body, button_text, cancellable = astuple(config) 373 | # ... 374 | 375 | 376 | create_menu( 377 | MenuConfig( 378 | title="My delicious menu", 379 | body="A description of the various items on the menu", 380 | button_text="Order now!" 381 | ) 382 | ) 383 | ``` 384 | 385 | **باز هم تفننی تر، فقط پایتون 3.8+** 386 | 387 | ```python 388 | from typing import TypedDict 389 | 390 | 391 | class MenuConfig(TypedDict): 392 | """A configuration for the Menu. 393 | 394 | Attributes: 395 | title: The title of the Menu. 396 | body: The body of the Menu. 397 | button_text: The text for the button label. 398 | cancellable: Can it be cancelled? 399 | """ 400 | title: str 401 | body: str 402 | button_text: str 403 | cancellable: bool 404 | 405 | 406 | def create_menu(config: MenuConfig): 407 | title = config["title"] 408 | # ... 409 | 410 | 411 | create_menu( 412 | # You need to supply all the parameters 413 | MenuConfig( 414 | title="My delicious menu", 415 | body="A description of the various items on the menu", 416 | button_text="Order now!", 417 | cancellable=True 418 | ) 419 | ) 420 | ``` 421 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 422 | 423 | ## **توابع** 424 | ### توابع فقط باید یک کار انجام دهند. 425 | این مهم ترین قانون در طراحی نرم افزار است. وقتی توابع بیشتر از یک کار انجام میدهند، سخت تر میتوان آنهارا نوشت و تست و استدلال کرد. وقتی که یک تابع را فقط به یک رفتار ایزوله میکنید، به راحتی میتوان آنهارا از نو نوشت و کدتان بسیار تمیزتر خواهد بود. اگر فقط همین مورد را از این راهنما یاد بگیرید، از بسیاری از برنامه نویس ها جلوتر خواهید بود. 426 | 427 | **بد:** 428 | 429 | ```python 430 | from typing import List 431 | 432 | 433 | class Client: 434 | active: bool 435 | 436 | 437 | def email(client: Client) -> None: 438 | pass 439 | 440 | 441 | def email_clients(clients: List[Client]) -> None: 442 | """Filter active clients and send them an email. 443 | """ 444 | for client in clients: 445 | if client.active: 446 | email(client) 447 | ``` 448 | 449 | **خوب**: 450 | 451 | ```python 452 | from typing import List 453 | 454 | 455 | class Client: 456 | active: bool 457 | 458 | 459 | def email(client: Client) -> None: 460 | pass 461 | 462 | 463 | def get_active_clients(clients: List[Client]) -> List[Client]: 464 | """Filter active clients. 465 | """ 466 | return [client for client in clients if client.active] 467 | 468 | 469 | def email_clients(clients: List[Client]) -> None: 470 | """Send an email to a given list of clients. 471 | """ 472 | for client in get_active_clients(clients): 473 | email(client) 474 | ``` 475 | 476 | آیا اکنون فرصتی برای استفاده از چنریتور ها می بینید؟ 477 | 478 | **حتی بهتر** 479 | 480 | ```python 481 | from typing import Generator, Iterator 482 | 483 | 484 | class Client: 485 | active: bool 486 | 487 | 488 | def email(client: Client): 489 | pass 490 | 491 | 492 | def active_clients(clients: Iterator[Client]) -> Generator[Client, None, None]: 493 | """Only active clients""" 494 | return (client for client in clients if client.active) 495 | 496 | 497 | def email_client(clients: Iterator[Client]) -> None: 498 | """Send an email to a given list of clients. 499 | """ 500 | for client in active_clients(clients): 501 | email(client) 502 | ``` 503 | 504 | 505 | ### اسامی توابع باید کاری که انجام میدهند را بگویند. 506 | 507 | **بد:** 508 | 509 | ```python 510 | class Email: 511 | def handle(self) -> None: 512 | pass 513 | 514 | message = Email() 515 | # What is this supposed to do again? 516 | message.handle() 517 | ``` 518 | 519 | **خوب:** 520 | 521 | ```python 522 | class Email: 523 | def send(self) -> None: 524 | """Send this message""" 525 | 526 | message = Email() 527 | message.send() 528 | ``` 529 | 530 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 531 | 532 | ### توابع باید فقط یک مرحله از انتزاع داشته باشند. 533 | 534 | وقتی که کدتان بیشتر از یک مرحله انتزاع دارد، کدتان دارد زیاد کار انجام میدهد. تکه تکه کردن توابع باعث خوانایی کد و تست نویسی آسان تر میشود. 535 | 536 | **بد:** 537 | 538 | ```python 539 | # type: ignore 540 | 541 | def parse_better_js_alternative(code: str) -> None: 542 | regexes = [ 543 | # ... 544 | ] 545 | 546 | statements = code.split('\n') 547 | tokens = [] 548 | for regex in regexes: 549 | for statement in statements: 550 | pass 551 | 552 | ast = [] 553 | for token in tokens: 554 | pass 555 | 556 | for node in ast: 557 | pass 558 | ``` 559 | 560 | **خوب:** 561 | 562 | ```python 563 | from typing import Tuple, List, Dict 564 | 565 | 566 | REGEXES: Tuple = ( 567 | # ... 568 | ) 569 | 570 | 571 | def parse_better_js_alternative(code: str) -> None: 572 | tokens: List = tokenize(code) 573 | syntax_tree: List = parse(tokens) 574 | 575 | for node in syntax_tree: 576 | pass 577 | 578 | 579 | def tokenize(code: str) -> List: 580 | statements = code.split() 581 | tokens: List[Dict] = [] 582 | for regex in REGEXES: 583 | for statement in statements: 584 | pass 585 | 586 | return tokens 587 | 588 | 589 | def parse(tokens: List) -> List: 590 | syntax_tree: List[Dict] = [] 591 | for token in tokens: 592 | pass 593 | 594 | return syntax_tree 595 | ``` 596 | 597 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 598 | 599 | ### از فلگ ها برای پارامتر های تابعتان استفاده نکنید. 600 | 601 | فلگ ها به کاربر ها میگویند که این تابع بیشتر از یک کار انجام میدهد. توابع باید یک کار انجام دهند. توابعتان را تکه تکه کنید اگر کدهای متفاوتی بر حسب یک بولین هستند. 602 | 603 | **بد:** 604 | 605 | ```python 606 | from tempfile import gettempdir 607 | from pathlib import Path 608 | 609 | 610 | def create_file(name: str, temp: bool) -> None: 611 | if temp: 612 | (Path(gettempdir()) / name).touch() 613 | else: 614 | Path(name).touch() 615 | ``` 616 | 617 | **خوب:** 618 | 619 | ```python 620 | from tempfile import gettempdir 621 | from pathlib import Path 622 | 623 | 624 | def create_file(name: str) -> None: 625 | Path(name).touch() 626 | 627 | 628 | def create_temp_file(name: str) -> None: 629 | (Path(gettempdir()) / name).touch() 630 | ``` 631 | 632 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 633 | 634 | ### از تاثیرات جانبی خودداری کنید 635 | 636 | یک تابع هنگامی اثر جانبی دارد که کاری بجز گرفتن یک مقدار و برگرداندن مقدار یا مقدار هایی دیگر انجام دهد. برای مثال، یک اثر جانبی میتواند نوشتن به یک فایل، تغییر دادن متغیر های گلوبال و یا انتقال تمامی اموالتان به یک غریبه باشد. 637 | حالا، بعضی اوقات نیاز است که برنامه شما اثرات جانبی داشته باشد - برای مثال، همانند مثال قبلی، نیاز داشته باشید که به یک فایل بنویسید. در این شرایط، شما باید متمرکز باشید و مشخص کنید که در کجا اثرات جانبی ایجاد میکنید. چندین تابع و کلاس نداشته باشید که به یک فایل مینویسند، بلکه یک و فقط یک سرویس داشته باشید که تمامی این کارها را انجام میدهد. 638 | نکته اصیل اجتناب از دام های رایج مثل تبادل وضعیت بین چندین شیء بدون هیچ ساختمان، استفاده از دیتاتایپ های قابل تغییر که هرچیزی بتواند به آن بنویسد یا استفاده کردن نمونه ای از یک کلاس به جای متمرکز کردن مکان هایی که اثرات جانبی خواهید داشت. 639 | اگر بتوانید این کار را انجام دهید، از بیشتر برنامه نویس ها خوشحال تر خواهید بود. 640 | 641 | 642 | **بد:** 643 | 644 | ```python 645 | # type: ignore 646 | 647 | # This is a module-level name. 648 | # It's good practice to define these as immutable values, such as a string. 649 | # However... 650 | fullname = "Ryan McDermott" 651 | 652 | def split_into_first_and_last_name() -> None: 653 | # The use of the global keyword here is changing the meaning of the 654 | # the following line. This function is now mutating the module-level 655 | # state and introducing a side-effect! 656 | global fullname 657 | fullname = fullname.split() 658 | 659 | split_into_first_and_last_name() 660 | 661 | # MyPy will spot the problem, complaining about 'Incompatible types in 662 | # assignment: (expression has type "List[str]", variable has type "str")' 663 | print(fullname) # ["Ryan", "McDermott"] 664 | 665 | # OK. It worked the first time, but what will happen if we call the 666 | # function again? 667 | ``` 668 | 669 | **خوب:** 670 | 671 | ```python 672 | from typing import List, AnyStr 673 | 674 | 675 | def split_into_first_and_last_name(name: AnyStr) -> List[AnyStr]: 676 | return name.split() 677 | 678 | fullname = "Ryan McDermott" 679 | name, surname = split_into_first_and_last_name(fullname) 680 | 681 | print(name, surname) # => Ryan McDermott 682 | ``` 683 | **همچنین خوب :** 684 | 685 | ```python 686 | from dataclasses import dataclass 687 | 688 | 689 | @dataclass 690 | class Person: 691 | name: str 692 | 693 | @property 694 | def name_as_first_and_last(self) -> list: 695 | return self.name.split() 696 | 697 | 698 | # The reason why we create instances of classes is to manage state! 699 | person = Person("Ryan McDermott") 700 | print(person.name) # => "Ryan McDermott" 701 | print(person.name_as_first_and_last) # => ["Ryan", "McDermott"] 702 | ``` 703 | 704 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 705 | 706 | ## **کلاس ها** 707 | 708 | ### **Single Responsibility Principle (SRP)** 709 | 710 | رابرت سی. مارتین مینویسد : 711 | 712 | > یک کلاس باید فقط یک دلیل برای تغییر داشته باشد. 713 | 714 | " دلایل تغییر " ذاتاً مسئولیت هایی است که توسط یک کلاس یا تابع مدیریت میشود. 715 | 716 | در مثال های زیر، ما یک عنصر HTML را میسازیم که یک کامنت و ورژن را نشان میدهد. 717 | 718 | **بد :** 719 | ```python 720 | from importlib import metadata 721 | 722 | 723 | class VersionCommentElement: 724 | """An element that renders an HTML comment with the program's version number 725 | """ 726 | 727 | def get_version(self) -> str: 728 | """Get the package version""" 729 | return metadata.version("pip") 730 | 731 | def render(self) -> None: 732 | print(f'') 733 | 734 | 735 | VersionCommentElement().render() 736 | ``` 737 | این کلاس دو وظیفه دارد: 738 | * گرفتن نسخه ی پکیج پایتون 739 | * رندر کردن آن به یک عنصر HTML 740 | 741 | هر تغییری در یکی از آن ها، ریسک تاثیر گذاشتن روی آن یکی را ایجاد میکند. 742 | ما میتوانیم کلاس را از نو بنویسیم و مسئولیت ها را جدا کنیم. 743 | 744 | **خوب :** 745 | ```python 746 | from importlib import metadata 747 | 748 | 749 | def get_version(pkg_name:str) -> str: 750 | """Retrieve the version of a given package""" 751 | return metadata.version(pkg_name) 752 | 753 | 754 | class VersionCommentElement: 755 | """An element that renders an HTML comment with the program's version number 756 | """ 757 | 758 | def __init__(self, version: str): 759 | self.version = version 760 | 761 | def render(self) -> None: 762 | print(f'') 763 | 764 | 765 | VersionCommentElement(get_version("pip")).render() 766 | ``` 767 | نتیجه این خواهد بود که کلاس فقط باید رندر کردن را به عهده بگیرد. ورژن را هنگام نمونه گیری دریافت میکند و متن توسط تابعی جدا به نام `get_version()` گرفته میشود. تغییر دادن کلاس هیچ تاثیری روی آن یکی ندارد، و بالعکس، تا زمانی که قرارداد های بینشان تغییر نکند، یعنی فانکشن یک متن برگرداند و متود `__init__` کلاس یک رشته را دریافت کند. 768 | 769 | و همینطور، تابع `get_version()` قابل استفاده در همه جا است. 770 | 771 | ### **Open/Closed Principle (OCP)** 772 | 773 | > “ قابلیت های جدید را با گسترش دادن ایجاد کنید، نه با تغییر دادن(آن). “ Uncle Bob. 774 | 775 | اشیاء باید برای گسترش دادن دردسترس باشند، اما بسته برای تغییرات. باید امکان تقویت عملکرد ارائه شده توسط یک شیء (برای مثال، یک کلاس) بدون تغییر دادن قرارداد های داخلی آن باشد. یک شیء میتواند این قابلیت را هنگامی ایجاد کند که طوری نوشته شده باشد که اجازه اینکار را به راحتی بدهد. 776 | 777 | در مثال بعدی، تلاش میکنیم یک فریم ورک وب ساده پیاده سازی کنیم که درخواست های HTTP را هندل و پاسخی را برمیگرداند. کلاس `View` یک متد `.get()` دارد که زمانی صدا زده خواهد شد که سرور HTTP یک درخواست GET دریافت کند. 778 | 779 | کلاس `View` به طور قصد ساده است و پاسخ های `text/plain` برمیگرداند. ما همچنین پاسخ هایی بر پایه فایل های تمپلیت میخواهیم پس با کلاس `TemplateView` از آن ارث بری میکنیم. 780 | 781 | **بد:** 782 | ```python 783 | from dataclasses import dataclass 784 | 785 | 786 | @dataclass 787 | class Response: 788 | """An HTTP response""" 789 | 790 | status: int 791 | content_type: str 792 | body: str 793 | 794 | 795 | class View: 796 | """A simple view that returns plain text responses""" 797 | 798 | def get(self, request) -> Response: 799 | """Handle a GET request and return a message in the response""" 800 | return Response( 801 | status=200, 802 | content_type='text/plain', 803 | body="Welcome to my web site" 804 | ) 805 | 806 | 807 | class TemplateView(View): 808 | """A view that returns HTML responses based on a template file.""" 809 | 810 | def get(self, request) -> Response: 811 | """Handle a GET request and return an HTML document in the response""" 812 | with open("index.html") as fd: 813 | return Response( 814 | status=200, 815 | content_type='text/html', 816 | body=fd.read() 817 | ) 818 | 819 | ``` 820 | 821 | کلاس `TemplateView` رفتار های داخلی والدش را تغییر داده تا بتواند عملکرد های پیشرفته تر اضافه کند. با اینکار، کلاس `TemplateView` به والدش تکیه میکند که عملکرد `.get()` تغییر نکند، که حالا باید در زمان منجمد شود. برای مثال، نمی‌توانیم برخی بررسی‌های اضافی را در تمام کلاس‌های مشتق از `View` معرفی کنیم، زیرا این رفتار حداقل در یک نوع فرعی لغو شده است و ما باید آن را به‌روزرسانی کنیم. 822 | 823 | بیایید کلاس های خود را از نو طراحی کنیم تا این مشکل حل شود و اجازه دهیم تا کلاس `View` به تمیزی گسترش (نه تغییر) داده شود. 824 | 825 | **خوب :** 826 | ```python 827 | from dataclasses import dataclass 828 | 829 | 830 | @dataclass 831 | class Response: 832 | """An HTTP response""" 833 | 834 | status: int 835 | content_type: str 836 | body: str 837 | 838 | 839 | class View: 840 | """A simple view that returns plain text responses""" 841 | 842 | content_type = "text/plain" 843 | 844 | def render_body(self) -> str: 845 | """Render the message body of the response""" 846 | return "Welcome to my web site" 847 | 848 | def get(self, request) -> Response: 849 | """Handle a GET request and return a message in the response""" 850 | return Response( 851 | status=200, 852 | content_type=self.content_type, 853 | body=self.render_body() 854 | ) 855 | 856 | 857 | class TemplateView(View): 858 | """A view that returns HTML responses based on a template file.""" 859 | 860 | content_type = "text/html" 861 | template_file = "index.html" 862 | 863 | def render_body(self) -> str: 864 | """Render the message body as HTML""" 865 | with open(self.template_file) as fd: 866 | return fd.read() 867 | 868 | 869 | ``` 870 | توجه داشته باشید که نیاز داشتیم `render_body()` را از نو نویسی کنیم تا منبع بدنه را عوض کنیم، اما این کلاس، تنها یک مسئولیت به درستی تعیین شده دارد که **کلاس های فرعی را دعوت میکند که آن را از نو نویسی کنند**. جوری ساخته شده است که اجازه ی گسترش داده شدن توسط کلاس های فرعی را بدهد. 871 | یک راه خوب دیگر برای استفاده از قدرت های ارث بری و ترکیب اشیاء استفاده از [Mixins](https://docs.djangoproject.com/en/4.1/topics/class-based-views/mixins/) ها است. 872 | 873 | میکسین ها کلاس های ریشه و ساده ای هستند که به طور انحصاری با سایر کلاس های مرتبط استفاده می شوند. آنها با کلاس هدف با استفاده از وراثت چندگانه "مخلوط" می شوند تا رفتار هدف را تغییر دهند. 874 | 875 | چند قانون : 876 | - میکسین ها باید از `object` ارث بری کنند. 877 | - میکسین ها همیشه قبل از کلاس هدف میایند. برای مثال : 878 | 879 | `class Foo(MixinA, MixinB, TargetClass): ...` 880 | 881 | **همچنین خوب :** 882 | ```python 883 | from dataclasses import dataclass, field 884 | from typing import Protocol 885 | 886 | 887 | @dataclass 888 | class Response: 889 | """An HTTP response""" 890 | 891 | status: int 892 | content_type: str 893 | body: str 894 | headers: dict = field(default_factory=dict) 895 | 896 | 897 | class View: 898 | """A simple view that returns plain text responses""" 899 | 900 | content_type = "text/plain" 901 | 902 | def render_body(self) -> str: 903 | """Render the message body of the response""" 904 | return "Welcome to my web site" 905 | 906 | def get(self, request) -> Response: 907 | """Handle a GET request and return a message in the response""" 908 | return Response( 909 | status=200, 910 | content_type=self.content_type, 911 | body=self.render_body() 912 | ) 913 | 914 | 915 | class TemplateRenderMixin: 916 | """A mixin class for views that render HTML documents using a template file 917 | 918 | Not to be used by itself! 919 | """ 920 | template_file: str = "" 921 | 922 | def render_body(self) -> str: 923 | """Render the message body as HTML""" 924 | if not self.template_file: 925 | raise ValueError("The path to a template file must be given.") 926 | 927 | with open(self.template_file) as fd: 928 | return fd.read() 929 | 930 | 931 | class ContentLengthMixin: 932 | """A mixin class for views that injects a Content-Length header in the 933 | response 934 | 935 | Not to be used by itself! 936 | """ 937 | 938 | def get(self, request) -> Response: 939 | """Introspect and amend the response to inject the new header""" 940 | response = super().get(request) # type: ignore 941 | response.headers['Content-Length'] = len(response.body) 942 | return response 943 | 944 | 945 | class TemplateView(TemplateRenderMixin, ContentLengthMixin, View): 946 | """A view that returns HTML responses based on a template file.""" 947 | 948 | content_type = "text/html" 949 | template_file = "index.html" 950 | 951 | ``` 952 | 953 | همانطور که میبینید، میکسین ها ترکیب اشیاء را، با بسته بندی کردن عملکرد های مشابه و تبدیل آنها به یک کلاس قابل استفاده دوباره با یک مسئولیت و اجازه جدا کردن آنها، آسان میکنند. 954 | فریم ورک مشهور جنگو استفاده زیادی از میکسین ها میکند تا ویو های مبتنی بر کلاس خود را بسازد. 955 | 956 | ### **Liskov Substitution Principle (LSP)** 957 | 958 | > “ توابعی که از اشاره گر ها یا ارجاع ها به کلاس های پایه استفاده میکنند باید بتوانند اشیاء کلاس ارث بری شده را بدون دانستن استفاد کنند. “ Uncle Bob. 959 | 960 | این اصل به افتخار Barbara Liskov نامگذاری شده، که با دانشمند کامپیوتر دیگری به نام Jeannette Wing در مقاله ی "A behavioral notion of subtyping (1994)" همکاری کرده است. 961 | یک اصل اصلی مقاله این است که " یک نوع فرعی باید رفتار متود های نوع اصلی خود را حفظ و همچنین تمام ویژگی‌های تغییرناپذیر و تاریخی نوع اصلی خود را نگه دارد. 962 | 963 | این به این معناست که، یک تابع که یک نوع اصلی را قبول میکند باید همچنین نوع های فرعی آن را بدون هیچ تغیری قبول کند. 964 | 965 | میتوانید مشکل کد زیر را پیدا کنید ؟ 966 | 967 | **بد :** 968 | ```python 969 | from dataclasses import dataclass 970 | 971 | 972 | @dataclass 973 | class Response: 974 | """An HTTP response""" 975 | 976 | status: int 977 | content_type: str 978 | body: str 979 | 980 | 981 | class View: 982 | """A simple view that returns plain text responses""" 983 | 984 | content_type = "text/plain" 985 | 986 | def render_body(self) -> str: 987 | """Render the message body of the response""" 988 | return "Welcome to my web site" 989 | 990 | def get(self, request) -> Response: 991 | """Handle a GET request and return a message in the response""" 992 | return Response( 993 | status=200, 994 | content_type=self.content_type, 995 | body=self.render_body() 996 | ) 997 | 998 | 999 | class TemplateView(View): 1000 | """A view that returns HTML responses based on a template file.""" 1001 | 1002 | content_type = "text/html" 1003 | 1004 | def get(self, request, template_file: str) -> Response: # type: ignore 1005 | """Render the message body as HTML""" 1006 | with open(template_file) as fd: 1007 | return Response( 1008 | status=200, 1009 | content_type=self.content_type, 1010 | body=fd.read() 1011 | ) 1012 | 1013 | 1014 | def render(view: View, request) -> Response: 1015 | """Render a View""" 1016 | return view.get(request) 1017 | 1018 | ``` 1019 | 1020 | انتظار میرود که فانکشن `render()` به خوبی بتواند با `View` و زیرکلاسش `TemplateView` کار کند، اما زیرکلاسش این سازگاری را با تغییر متود `.get()` از بین برده است. 1021 | تابع خطای `TypeError` را هنگامی که با `TemplateView` استفاده شود برمیگرداند. 1022 | 1023 | اگر بخواهیم که تابع `render()` با هر زیرنوعی از `View` کار کند، باید توجه کنیم که پروتوکل عمومی آن را نشکنیم. اما از کجا بدانیم که چه چیزی یک کلاس را تشکیل میدهد؟ Type Hinter هایی مثل *mypy* اگر اشتباهاتی مثل این ببینند، اخطار هایی به این شکل برمیگردانند : 1024 | ``` 1025 | error: Signature of "get" incompatible with supertype "View" 1026 | :36: note: Superclass: 1027 | :36: note: def get(self, request: Any) -> Response 1028 | :36: note: Subclass: 1029 | :36: note: def get(self, request: Any, template_file: str) -> Response 1030 | ``` 1031 | 1032 | ### **Interface Segregation Principle (ISP)** 1033 | 1034 | > اینترفیس ها را کوچک نگه دارید تا کاربر ها نیاز وابسته به چیزهایی که از آنها بی نیازند نشوند. Uncle Bob. 1035 | 1036 | چندین زبان برنامه نویسی مشهور شیء گرا مثل Go و Java مفهمومی به نام اینترفیس ها دارند. اینترفیس ها متود های عمومی و ویژگی های یک شیء را بدون پیاده سازی آن تعریف میکند. آنها زمانی مفید هستند که نمی خواهیم امضای یک تابع را با یک شیء مشخص مرتبط کنیم. 1037 | پایتون اینترفیس ندارد. اما به جایش Abstract Base Clase دارد که کمی متقاوت هستند، اما میتوانند همانکار را انجام دهند. 1038 | 1039 | **خوب** 1040 | ```python 1041 | 1042 | from abc import ABCMeta, abstractmethod 1043 | 1044 | 1045 | # Define the Abstract Class for a generic Greeter object 1046 | class Greeter(metaclass=ABCMeta): 1047 | """An object that can perform a greeting action.""" 1048 | 1049 | @staticmethod 1050 | @abstractmethod 1051 | def greet(name: str) -> None: 1052 | """Display a greeting for the user with the given name""" 1053 | 1054 | 1055 | class FriendlyActor(Greeter): 1056 | """An actor that greets the user with a friendly salutation""" 1057 | 1058 | @staticmethod 1059 | def greet(name: str) -> None: 1060 | """Greet a person by name""" 1061 | print(f"Hello {name}!") 1062 | 1063 | 1064 | def welcome_user(user_name: str, actor: Greeter): 1065 | """Welcome a user with a given name using the provided actor""" 1066 | actor.greet(user_name) 1067 | 1068 | 1069 | welcome_user("Barbara", FriendlyActor()) 1070 | ``` 1071 | حالا این سناریو را در نظر بگیرید : میخواهیم تعداد مشخصی سند PDF داشته باشیم که ما نوشتیم و میخواهیم به بازدید کننده های وبسایتمان داده شوند. ما از یک وب فریمورک پایتون استفاده میکنیم و شاید وسوسه شویم که کلاسی را طراحی کنیم که این اسناد را مدیریت کند، پس یک کلاس پایه انتزاعی جامع برای سند خود طراحی می کنیم. 1072 | 1073 | **خطا** 1074 | ```python 1075 | import abc 1076 | 1077 | 1078 | class Persistable(metaclass=abc.ABCMeta): 1079 | """Serialize a file to data and back""" 1080 | 1081 | @property 1082 | @abc.abstractmethod 1083 | def data(self) -> bytes: 1084 | """The raw data of the file""" 1085 | 1086 | @classmethod 1087 | @abc.abstractmethod 1088 | def load(cls, name: str): 1089 | """Load the file from disk""" 1090 | 1091 | @abc.abstractmethod 1092 | def save(self) -> None: 1093 | """Save the file to disk""" 1094 | 1095 | 1096 | # We just want to serve the documents, so our concrete PDF document 1097 | # implementation just needs to implement the `.load()` method and have 1098 | # a public attribute named `data`. 1099 | 1100 | class PDFDocument(Persistable): 1101 | """A PDF document""" 1102 | 1103 | @property 1104 | def data(self) -> bytes: 1105 | """The raw bytes of the PDF document""" 1106 | ... # Code goes here - omitted for brevity 1107 | 1108 | @classmethod 1109 | def load(cls, name: str): 1110 | """Load the file from the local filesystem""" 1111 | ... # Code goes here - omitted for brevity 1112 | 1113 | 1114 | def view(request): 1115 | """A web view that handles a GET request for a document""" 1116 | requested_name = request.qs['name'] # We want to validate this! 1117 | return PDFDocument.load(requested_name).data 1118 | 1119 | ``` 1120 | اما نمیتونیم! اگر متود `.save()` را پیاده سازی نکنیم یک خطا به ما نشان داده میشود : 1121 | 1122 | ``` 1123 | Can't instantiate abstract class PDFDocument with abstract method save. 1124 | ``` 1125 | این اعصاب خورد کن است. ما واقعا نیازی به پیاده سازی `.save()` در اینجا نداریم. میتوانیم یک متود بنویسیم که هیچ کاری نکند یا خطای `NotImplementedError` را برگرداند اما این فقط کد اضافی ای است که باید مراقبش باشیم. 1126 | 1127 | در عین حال، اگر `.save()` را از کلاس انتزاعی حذف کنیم نیاز خواهیم داشت بعدا برای اینکه کاربران بتوانند اسنادشان را ارسال کنند از نو پیاده سازی کنیم که دوباره مارا به خانه ی اول برمیگرداند. 1128 | 1129 | مشکل اینجاست که ما یک *اینترفیس* نوشتیم که قابلیت هایی دارد که ما در حال حاظر به آنها یا نیاز نداریم یا استفاده نمیکنیم. 1130 | 1131 | راه حل این است که اینترفیست را به قطعات کوچک تر که کارهای جدا میکنند تکه تکه کنیم. 1132 | 1133 | **خوب** 1134 | ```python 1135 | import abc 1136 | 1137 | 1138 | class DataCarrier(metaclass=abc.ABCMeta): 1139 | """Carries a data payload""" 1140 | @property 1141 | def data(self): 1142 | ... 1143 | 1144 | class Loadable(DataCarrier): 1145 | """Can load data from storage by name""" 1146 | @classmethod 1147 | @abc.abstractmethod 1148 | def load(cls, name: str): 1149 | ... 1150 | 1151 | class Saveable(DataCarrier): 1152 | """Can save data to storage""" 1153 | @abc.abstractmethod 1154 | def save(self) -> None: 1155 | ... 1156 | 1157 | 1158 | class PDFDocument(Loadable): 1159 | """A PDF document""" 1160 | 1161 | @property 1162 | def data(self) -> bytes: 1163 | """The raw bytes of the PDF document""" 1164 | ... # Code goes here - omitted for brevity 1165 | 1166 | @classmethod 1167 | def load(cls, name: str): 1168 | """Load the file from the local filesystem""" 1169 | ... # Code goes here - omitted for brevity 1170 | 1171 | 1172 | def view(request): 1173 | """A web view that handles a GET request for a document""" 1174 | requested_name = request.qs['name'] # We want to validate this! 1175 | return PDFDocument.load(requested_name).data 1176 | 1177 | ``` 1178 | 1179 | ### **Dependency Inversion Principle (DIP)** 1180 | 1181 | > “به انتزاع وابسته شوید، نه جزئیات مشخص.”, 1182 | > Uncle Bob. 1183 | 1184 | تصور کنید میخواستیم که یک وب ویو درست کنیم که پاسخ HTTP ای را برمیگرداند که ردیف هایی از فایل CSV ای که ساختیم برمیگراند. میخواهیم از نویسنده ی CSV ای استفاده کنیم که توسط کتابخانه ی استاندارد پایتون ارائه میشود. 1185 | 1186 | **بد** 1187 | ```python 1188 | import csv 1189 | from io import StringIO 1190 | 1191 | 1192 | class StreamingHttpResponse: 1193 | """A streaming HTTP response""" 1194 | ... # implementation code goes here 1195 | 1196 | 1197 | def some_view(request): 1198 | rows = ( 1199 | ['First row', 'Foo', 'Bar', 'Baz'], 1200 | ['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"] 1201 | ) 1202 | # Define a generator to stream data directly to the client 1203 | def stream(): 1204 | buffer_ = StringIO() 1205 | writer = csv.writer(buffer_, delimiter=';', quotechar='"') 1206 | for row in rows: 1207 | writer.writerow(row) 1208 | buffer_.seek(0) 1209 | data = buffer_.read() 1210 | buffer_.seek(0) 1211 | buffer_.truncate() 1212 | yield data 1213 | 1214 | # Create the streaming response object with the appropriate CSV header. 1215 | response = StreamingHttpResponse(stream(), content_type='text/csv') 1216 | response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' 1217 | 1218 | return response 1219 | 1220 | ``` 1221 | اولین پیاده‌سازی ما حول رابط نویسنده CSV با دستکاری یک شی `StringIO` (که شبه فایل است) و انجام چندین عملیات سطح پایین به منظور جداسازی ردیف‌ها از نویسنده کار می‌کند. 1222 | 1223 | یک راه بهتر این است که از این واقعیت استفاده کنیم که نویسنده به یک شی با متد `.write()‍‍` برای انجام کار ما نیاز دارد. 1224 | چرا یک شی ساختگی که بلافاصله ردیف جدید ساخته شده را برمی گرداند، به آن منتقل نکنیم تا کلاس ‍`StreamingHttpResponse` بتواند بلافاصله آن را به کلاینت بازگرداند؟ 1225 | 1226 | **خوب** 1227 | ```python 1228 | import csv 1229 | 1230 | 1231 | class Echo: 1232 | """An object that implements just the write method of the file-like 1233 | interface. 1234 | """ 1235 | def write(self, value): 1236 | """Write the value by returning it, instead of storing in a buffer.""" 1237 | return value 1238 | 1239 | def some_streaming_csv_view(request): 1240 | """A view that streams a large CSV file.""" 1241 | rows = ( 1242 | ['First row', 'Foo', 'Bar', 'Baz'], 1243 | ['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"] 1244 | ) 1245 | writer = csv.writer(Echo(), delimiter=';', quotechar='"') 1246 | return StreamingHttpResponse( 1247 | (writer.writerow(row) for row in rows), 1248 | content_type="text/csv", 1249 | headers={'Content-Disposition': 'attachment; filename="somefilename.csv"'}, 1250 | ) 1251 | 1252 | ``` 1253 | خیلی بهتر شد، و مثل جادو کار میکند! دلیل اینکه پیاده سازی آن بهتر از قبلیست باید واضخ باشد: کد کمتر و بازدهی بیشتر برای رسیدن به پاسخ یکسان. ما تصمیم گرفتیم که از واقعیت این که کلاس نویسنده به انتزاع `.write()` از شیء ای که دریافت میکند وابسته است، بدون اینکه به جزئیات سطح پایین عملکرد متود توجه ای کند. 1254 | این مثال از [ارسالی به مستندات جنگو](https://code.djangoproject.com/ticket/21179) توسط این نویسنده گرفته شده است. 1255 | 1256 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 1257 | 1258 | ## **Don't repeat yourself (DRY)** 1259 | 1260 | تلاش کنید که اصل [DRY](https://fa.wikipedia.org/wiki/Don%27t_repeat_yourself) را متوجه بشوید. 1261 | 1262 | بهترین تلاشتان را بکنید که کدی را دوباره ننویسید. تکرار کد بد است چون به این معنیست که هنگام ایجاد تغییر، باید بیشتر از دو جا را تغییر دهید. 1263 | 1264 | تصور کنید که صاحب رستورانی هستید و میخواهید آمار انبارتان را داشته باشید: تمامی گوجه ها، پیاز ها، سیر ها، ادویه ها و... . اگر چندین لیست داشته باشید که آمار اینهارا ثبت کنند، هروقت که غذایی با گوجه سرو کنید باید تمامی این لیست هارا آپدیت کنید. اما اگر فقط یک لیست داشته باشید، فقط یک جا را باید آپدیت کنید! 1265 | 1266 | بیشتر اوقات شما وقتی کد را چندین بار تکرار میکنید که دو چیز یا بیشتر کمی متفاوت داشته باشید، که بسیار شبیه به هم باشند، اما این شباهتشان شمارا به داشتن دو تابع یا بیشتر مجبور میکنند که اکثراً یک کار را انجام میدهند. حذف کردن کد های تکراری به معنی ساختن انتزاعی است که بتواند این کارهارا با یک تابع/ماژول/کلاس انجام دهد. 1267 | 1268 | درست ساختن انتزاع مهم است. انتزاع بد میتواند از کد تکراری هم بدتر باشد! با این، اگر میتوانید یک انتزاع خوب بسازید، انجامش دهید! خودتان را تکرار نکنید، وگرنه مجبور میشوید چندین جا را برای تغییر کوچکی آپدیت کنید. 1269 | 1270 | **بد:** 1271 | 1272 | ```python 1273 | from typing import List, Dict 1274 | from dataclasses import dataclass 1275 | 1276 | @dataclass 1277 | class Developer: 1278 | def __init__(self, experience: float, github_link: str) -> None: 1279 | self._experience = experience 1280 | self._github_link = github_link 1281 | 1282 | @property 1283 | def experience(self) -> float: 1284 | return self._experience 1285 | 1286 | @property 1287 | def github_link(self) -> str: 1288 | return self._github_link 1289 | 1290 | @dataclass 1291 | class Manager: 1292 | def __init__(self, experience: float, github_link: str) -> None: 1293 | self._experience = experience 1294 | self._github_link = github_link 1295 | 1296 | @property 1297 | def experience(self) -> float: 1298 | return self._experience 1299 | 1300 | @property 1301 | def github_link(self) -> str: 1302 | return self._github_link 1303 | 1304 | 1305 | def get_developer_list(developers: List[Developer]) -> List[Dict]: 1306 | developers_list = [] 1307 | for developer in developers: 1308 | developers_list.append({ 1309 | 'experience' : developer.experience, 1310 | 'github_link' : developer.github_link 1311 | }) 1312 | return developers_list 1313 | 1314 | def get_manager_list(managers: List[Manager]) -> List[Dict]: 1315 | managers_list = [] 1316 | for manager in managers: 1317 | managers_list.append({ 1318 | 'experience' : manager.experience, 1319 | 'github_link' : manager.github_link 1320 | }) 1321 | return managers_list 1322 | 1323 | ## create list objects of developers 1324 | company_developers = [ 1325 | Developer(experience=2.5, github_link='https://github.com/1'), 1326 | Developer(experience=1.5, github_link='https://github.com/2') 1327 | ] 1328 | company_developers_list = get_developer_list(developers=company_developers) 1329 | 1330 | ## create list objects of managers 1331 | company_managers = [ 1332 | Manager(experience=4.5, github_link='https://github.com/3'), 1333 | Manager(experience=5.7, github_link='https://github.com/4') 1334 | ] 1335 | company_managers_list = get_manager_list(managers=company_managers) 1336 | ``` 1337 | 1338 | **خوب:** 1339 | 1340 | ```python 1341 | from typing import List, Dict 1342 | from dataclasses import dataclass 1343 | 1344 | @dataclass 1345 | class Employee: 1346 | def __init__(self, experience: float, github_link: str) -> None: 1347 | self._experience = experience 1348 | self._github_link = github_link 1349 | 1350 | @property 1351 | def experience(self) -> float: 1352 | return self._experience 1353 | 1354 | @property 1355 | def github_link(self) -> str: 1356 | return self._github_link 1357 | 1358 | 1359 | 1360 | def get_employee_list(employees: List[Employee]) -> List[Dict]: 1361 | employees_list = [] 1362 | for employee in employees: 1363 | employees_list.append({ 1364 | 'experience' : employee.experience, 1365 | 'github_link' : employee.github_link 1366 | }) 1367 | return employees_list 1368 | 1369 | ## create list objects of developers 1370 | company_developers = [ 1371 | Employee(experience=2.5, github_link='https://github.com/1'), 1372 | Employee(experience=1.5, github_link='https://github.com/2') 1373 | ] 1374 | company_developers_list = get_employee_list(employees=company_developers) 1375 | 1376 | ## create list objects of managers 1377 | company_managers = [ 1378 | Employee(experience=4.5, github_link='https://github.com/3'), 1379 | Employee(experience=5.7, github_link='https://github.com/4') 1380 | ] 1381 | company_managers_list = get_employee_list(employees=company_managers) 1382 | ``` 1383 | 1384 | **[⬆ برگشت به بالا](#فهرست-مطالب)** 1385 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import re 3 | import time 4 | import typing 5 | from pathlib import Path 6 | 7 | import pytest 8 | from mypy import api 9 | 10 | code_rxp = re.compile('```python(.*?)```', re.DOTALL | re.MULTILINE) 11 | 12 | 13 | class MyPyValidationError(BaseException): 14 | """A validation error occurred when MyPy attempted to validate the code""" 15 | 16 | 17 | def fake_print(*args, **kwargs): 18 | """Dummy replacement for print() that does nothing""" 19 | pass 20 | 21 | 22 | def pytest_collect_file(parent, path): 23 | """Collect all file suitable for use in tests""" 24 | if path.basename == "README.md": 25 | return ReadmeFile.from_parent(parent, path=Path(path)) 26 | 27 | 28 | class ReadmeFile(pytest.File): 29 | """A Markdown formatted readme file containing code snippets""" 30 | 31 | def collect(self): 32 | """Collect all code snippets""" 33 | raw_text = self.fspath.open().read() 34 | for idx, code in enumerate(code_rxp.findall(raw_text), 1): 35 | yield ReadmeItem.from_parent( 36 | self, name=str(idx), spec=code.strip() 37 | ) 38 | 39 | 40 | def _with_patched_sleep(func, *args, **kwargs): 41 | """Patch the sleep function so that it does nothing""" 42 | _sleep = time.sleep 43 | time.sleep = lambda *args: None 44 | try: 45 | return func(*args, **kwargs) 46 | finally: 47 | time.sleep = _sleep 48 | 49 | 50 | class ReadmeItem(pytest.Item): 51 | """A readme test item that validates a code snippet""" 52 | builtins = ( 53 | ('typing', typing), 54 | ('datetime', importlib.import_module('datetime')), 55 | ('hashlib', importlib.import_module('hashlib')), 56 | ('print', fake_print) 57 | ) 58 | 59 | def __init__(self, name, parent, spec): 60 | super().__init__(name, parent) 61 | self.spec = spec 62 | 63 | def runtest(self): 64 | """Run the test""" 65 | builtins = dict(self.builtins) 66 | byte_code = compile(self.spec, '', 'exec') 67 | _with_patched_sleep(exec, byte_code, builtins) 68 | msg, _, error = api.run(['--no-color-output', '-c', self.spec]) 69 | if error: 70 | # Ignore missing errors related to the injected names 71 | for name in builtins: 72 | if f"Name '{name}' is not defined" in msg: 73 | break 74 | else: 75 | raise MyPyValidationError(msg) 76 | 77 | def repr_failure(self, excinfo, **kwargs): 78 | """ called when self.runtest() raises an exception. """ 79 | return ( 80 | f"Code snippet {self.name} raised an error: {excinfo.value}. " 81 | f"The executed code was: {self.spec}" 82 | ) 83 | 84 | def reportinfo(self): 85 | """Report some basic information on the test outcome""" 86 | return self.fspath, 0, "usecase: {}".format(self.name) 87 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mypy 3 | --------------------------------------------------------------------------------