Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

🛁 Clean Code concepts adapted for Python

License

NotificationsYou must be signed in to change notification settings

SaAminzadeh/clean-code-python

 
 

Repository files navigation

فهرست مطالب

  1. مقدمه
  2. متغیر ها
  3. توابع
  4. کلاس ها
  5. Don't repeat yourself (DRY)

مقدمه

اصول مهندسی نرم افزار، از کتابکد تمیز نوشته ی Robert C. Martin، برای پایتون. این یک راهنمای تولید نیست، این یک راهنما برای تولید نرم افزار های خوانا، قابل استفاده مجدد و قابل از نو بازسازی است.نیازی بر سرسختگیری بر هر اصل گفته شده در اینجا نیست، و فقط تعداد کمی از آنها به صورت عمومی مورد تایید هستند. این ها چیزی جز اصول نیستند، اما اصول کدنویسی شده ای توسط سالها تجربه از نویسنده هایکد تمیز هستند.

متغیر ها

از اسم های متغیر با معنا و قابل تلفظ استفاده کنید

بد:

importdatetimeymdstr=datetime.date.today().strftime("%y-%m-%d")

به علاوه، نیازی به استفاده کردن تایپstr برای اسم نیست.

خوب:

importdatetimecurrent_date:str=datetime.date.today().strftime("%y-%m-%d")

⬆ برگشت به بالا

از واژگان یکسان برای همان نوع متغیر استفاده کنید

بد:ما در اینجا از چند اسم برای یک موجود استفاده میکنیم:

defget_user_info():passdefget_client_data():passdefget_customer_record():pass

خوب:اگر موجودیت یکسان است، باید در ارجاع به آن در توابع خود ثابت قدم باشید:

defget_user_info():passdefget_user_data():passdefget_user_record():pass

حتی بهتر

پایتون (همچنین) یک زبان برنامه نویسی شی گرا است. اگر منطقی است، توابع را همراه با پیاده‌سازی مشخص موجودیت در کد خود، به‌عنوان ویژگی‌های نمونه، متدهای ویژگی یا متدها بسته‌بندی کنید

fromtypingimportUnion,DictclassRecord:passclassUser:info :str@propertydefdata(self)->Dict[str,str]:return {}defget_record(self)->Union[Record,None]:returnRecord()

⬆ برگشت به بالا

از اسامی قابل جست و جو استفاده کنید

ما بیشتر کد میخوانیم تا بنویسیم. این خیلی مهم است که کدی که مینویسیم خوانا و قابل جستجو باشد. با نام گذارینکردن متغیر هایی که برای برنامه قابل فهم باشد، به خواننده هایمان آسیب میرسانیم. اسامی متغیرهایتان را قابل جست و جو کنید.

بد:

importtime# What is the number 86400 for again?time.sleep(86400)

خوب:

importtime# Declare them in the global namespace for the module.SECONDS_IN_A_DAY=60*60*24time.sleep(SECONDS_IN_A_DAY)

⬆ برگشت به بالا

از متغیر های توضیحی استفاده کنید

بد:

importreaddress="One Infinite Loop, Cupertino 95014"city_zip_code_regex=r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$"matches=re.match(city_zip_code_regex,address)ifmatches:print(f"{matches[1]}:{matches[2]}")

بد نیست:

بهتر است، اما هنوز به شدت به رجکس وابسته ایم.

importreaddress="One Infinite Loop, Cupertino 95014"city_zip_code_regex=r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$"matches=re.match(city_zip_code_regex,address)ifmatches:city,zip_code=matches.groups()print(f"{city}:{zip_code}")

خوب:

وابستگی به رجکس را با نام گذاری الگوهای فرعی کمتر کنید.

importreaddress="One Infinite Loop, Cupertino 95014"city_zip_code_regex=r"^[^,\\]+[,\\\s]+(?P<city>.+?)\s*(?P<zip_code>\d{5})?$"matches=re.match(city_zip_code_regex,address)ifmatches:print(f"{matches['city']},{matches['zip_code']}")

⬆ برگشت به بالا

از نقشه برداری ذهنی خودداری کنید

خواننده ی کدتان را مجبور نکنید که اسامی متغیرهای شمارا ترجمه کنند.صریح بهتر از ضمنی است.

بد:

seq= ("Austin","New York","San Francisco")foriteminseq:#do_stuff()#do_some_other_stuff()# Wait, what's `item` again?print(item)

خوب:

locations= ("Austin","New York","San Francisco")forlocationinlocations:#do_stuff()#do_some_other_stuff()# ...print(location)

⬆ برگشت به بالا

توضیحات اضافی ننویسید

اگر اسم کلاس/شیء شما چیزی را میگوید، نیازی نیست که آنرا در اسامی متغیر هایتان تکرار کنید.

بد:

classCar:car_make:strcar_model:strcar_color:str

خوب:

classCar:make:strmodel:strcolor:str

⬆ برگشت به بالا

از آرگومان های پیشفرض به جای شروط یک خطی استفاده کنید.

بد

چرا بنویسیم :

importhashlibdefcreate_micro_brewery(name):name="Hipster Brew Co."ifnameisNoneelsenameslug=hashlib.sha1(name.encode()).hexdigest()# etc.

...زمانی که میتوانید از یک آرگومان پیشفرض استفاده کنید ؟ این همچنین خواننده را کاملا روشن میکند که شما برای آرگومانتان یک رشته میخواهید.

خوب:

importhashlibdefcreate_micro_brewery(name:str="Hipster Brew Co."):slug=hashlib.sha1(name.encode()).hexdigest()# etc.

⬆ برگشت به بالا

آرگومان های توابع (ترجیحاً دو یا کمتر)

کم کردن مقادیر پارامتر های توابع بسیار مهم است چون تست کردن آنهارا راحت تر میکند. بیشتر از سه تا منجر به انفجاری ترکیبی میشود چون باید حالت های مختلف را با آرگومان های متفاوت تست کنید.

آرگومان نداشتن حالت آیده آل است. یکی یا دوتا خوب است، و باید از سه تا اجتناب کرد.هرچیزی بیشتر از آن باید یک رقم شود. معمولا اگر بیشتر از دو آرگومان دارید، کد شما دارد چند کار را باهم انجام میدهد. در شرایطی که اینگونه نیست، اغلب اوقات یک شی سطح بالاتر به عنوان یک آرگومان کافی است.

بد:

defcreate_menu(title,body,button_text,cancellable):pass

به سبک جاوا:

classMenu:def__init__(self,config:dict):self.title=config["title"]self.body=config["body"]# ...menu=Menu(    {"title":"My Menu","body":"Something about my menu","button_text":"OK","cancellable":False    })

این هم خوب است

classMenuConfig:"""A configuration for the Menu.    Attributes:        title: The title of the Menu.        body: The body of the Menu.        button_text: The text for the button label.        cancellable: Can it be cancelled?    """title:strbody:strbutton_text:strcancellable:bool=Falsedefcreate_menu(config:MenuConfig)->None:title=config.titlebody=config.body# ...config=MenuConfig()config.title="My delicious menu"config.body="A description of the various items on the menu"config.button_text="Order now!"# The instance attribute overrides the default class attribute.config.cancellable=Truecreate_menu(config)

تفننی

fromtypingimportNamedTupleclassMenuConfig(NamedTuple):"""A configuration for the Menu.    Attributes:        title: The title of the Menu.        body: The body of the Menu.        button_text: The text for the button label.        cancellable: Can it be cancelled?    """title:strbody:strbutton_text:strcancellable:bool=Falsedefcreate_menu(config:MenuConfig):title,body,button_text,cancellable=config# ...create_menu(MenuConfig(title="My delicious menu",body="A description of the various items on the menu",button_text="Order now!"    ))

تفننی تر

fromdataclassesimportastuple,dataclass@dataclassclassMenuConfig:"""A configuration for the Menu.    Attributes:        title: The title of the Menu.        body: The body of the Menu.        button_text: The text for the button label.        cancellable: Can it be cancelled?    """title:strbody:strbutton_text:strcancellable:bool=Falsedefcreate_menu(config:MenuConfig):title,body,button_text,cancellable=astuple(config)# ...create_menu(MenuConfig(title="My delicious menu",body="A description of the various items on the menu",button_text="Order now!"    ))

باز هم تفننی تر، فقط پایتون 3.8+

fromtypingimportTypedDictclassMenuConfig(TypedDict):"""A configuration for the Menu.    Attributes:        title: The title of the Menu.        body: The body of the Menu.        button_text: The text for the button label.        cancellable: Can it be cancelled?    """title:strbody:strbutton_text:strcancellable:booldefcreate_menu(config:MenuConfig):title=config["title"]# ...create_menu(# You need to supply all the parametersMenuConfig(title="My delicious menu",body="A description of the various items on the menu",button_text="Order now!",cancellable=True    ))

⬆ برگشت به بالا

توابع

توابع فقط باید یک کار انجام دهند.

این مهم ترین قانون در طراحی نرم افزار است. وقتی توابع بیشتر از یک کار انجام میدهند، سخت تر میتوان آنهارا نوشت و تست و استدلال کرد. وقتی که یک تابع را فقط به یک رفتار ایزوله میکنید، به راحتی میتوان آنهارا از نو نوشت و کدتان بسیار تمیزتر خواهد بود. اگر فقط همین مورد را از این راهنما یاد بگیرید، از بسیاری از برنامه نویس ها جلوتر خواهید بود.

بد:

fromtypingimportListclassClient:active:booldefemail(client:Client)->None:passdefemail_clients(clients:List[Client])->None:"""Filter active clients and send them an email.    """forclientinclients:ifclient.active:email(client)

خوب:

fromtypingimportListclassClient:active:booldefemail(client:Client)->None:passdefget_active_clients(clients:List[Client])->List[Client]:"""Filter active clients.    """return [clientforclientinclientsifclient.active]defemail_clients(clients:List[Client])->None:"""Send an email to a given list of clients.    """forclientinget_active_clients(clients):email(client)

آیا اکنون فرصتی برای استفاده از چنریتور ها می بینید؟

حتی بهتر

fromtypingimportGenerator,IteratorclassClient:active:booldefemail(client:Client):passdefactive_clients(clients:Iterator[Client])->Generator[Client,None,None]:"""Only active clients"""return (clientforclientinclientsifclient.active)defemail_client(clients:Iterator[Client])->None:"""Send an email to a given list of clients.    """forclientinactive_clients(clients):email(client)

اسامی توابع باید کاری که انجام میدهند را بگویند.

بد:

classEmail:defhandle(self)->None:passmessage=Email()# What is this supposed to do again?message.handle()

خوب:

classEmail:defsend(self)->None:"""Send this message"""message=Email()message.send()

⬆ برگشت به بالا

توابع باید فقط یک مرحله از انتزاع داشته باشند.

وقتی که کدتان بیشتر از یک مرحله انتزاع دارد، کدتان دارد زیاد کار انجام میدهد. تکه تکه کردن توابع باعث خوانایی کد و تست نویسی آسان تر میشود.

بد:

# type: ignoredefparse_better_js_alternative(code:str)->None:regexes= [# ...    ]statements=code.split('\n')tokens= []forregexinregexes:forstatementinstatements:passast= []fortokenintokens:passfornodeinast:pass

خوب:

fromtypingimportTuple,List,DictREGEXES:Tuple= (# ...)defparse_better_js_alternative(code:str)->None:tokens:List=tokenize(code)syntax_tree:List=parse(tokens)fornodeinsyntax_tree:passdeftokenize(code:str)->List:statements=code.split()tokens:List[Dict]= []forregexinREGEXES:forstatementinstatements:passreturntokensdefparse(tokens:List)->List:syntax_tree:List[Dict]= []fortokenintokens:passreturnsyntax_tree

⬆ برگشت به بالا

از فلگ ها برای پارامتر های تابعتان استفاده نکنید.

فلگ ها به کاربر ها میگویند که این تابع بیشتر از یک کار انجام میدهد. توابع باید یک کار انجام دهند. توابعتان را تکه تکه کنید اگر کدهای متفاوتی بر حسب یک بولین هستند.

بد:

fromtempfileimportgettempdirfrompathlibimportPathdefcreate_file(name:str,temp:bool)->None:iftemp:        (Path(gettempdir())/name).touch()else:Path(name).touch()

خوب:

fromtempfileimportgettempdirfrompathlibimportPathdefcreate_file(name:str)->None:Path(name).touch()defcreate_temp_file(name:str)->None:    (Path(gettempdir())/name).touch()

⬆ برگشت به بالا

از تاثیرات جانبی خودداری کنید

یک تابع هنگامی اثر جانبی دارد که کاری بجز گرفتن یک مقدار و برگرداندن مقدار یا مقدار هایی دیگر انجام دهد. برای مثال، یک اثر جانبی میتواند نوشتن به یک فایل، تغییر دادن متغیر های گلوبال و یا انتقال تمامی اموالتان به یک غریبه باشد.حالا، بعضی اوقات نیاز است که برنامه شما اثرات جانبی داشته باشد - برای مثال، همانند مثال قبلی، نیاز داشته باشید که به یک فایل بنویسید. در این شرایط، شما باید متمرکز باشید و مشخص کنید که در کجا اثرات جانبی ایجاد میکنید. چندین تابع و کلاس نداشته باشید که به یک فایل مینویسند، بلکه یک و فقط یک سرویس داشته باشید که تمامی این کارها را انجام میدهد.نکته اصیل اجتناب از دام های رایج مثل تبادل وضعیت بین چندین شیء بدون هیچ ساختمان، استفاده از دیتاتایپ های قابل تغییر که هرچیزی بتواند به آن بنویسد یا استفاده کردن نمونه ای از یک کلاس به جای متمرکز کردن مکان هایی که اثرات جانبی خواهید داشت.اگر بتوانید این کار را انجام دهید، از بیشتر برنامه نویس ها خوشحال تر خواهید بود.

بد:

# type: ignore# This is a module-level name.# It's good practice to define these as immutable values, such as a string.# However...fullname="Ryan McDermott"defsplit_into_first_and_last_name()->None:# The use of the global keyword here is changing the meaning of the# the following line. This function is now mutating the module-level# state and introducing a side-effect!globalfullnamefullname=fullname.split()split_into_first_and_last_name()# MyPy will spot the problem, complaining about 'Incompatible types in# assignment: (expression has type "List[str]", variable has type "str")'print(fullname)# ["Ryan", "McDermott"]# OK. It worked the first time, but what will happen if we call the# function again?

خوب:

fromtypingimportList,AnyStrdefsplit_into_first_and_last_name(name:AnyStr)->List[AnyStr]:returnname.split()fullname="Ryan McDermott"name,surname=split_into_first_and_last_name(fullname)print(name,surname)# => Ryan McDermott

همچنین خوب :

fromdataclassesimportdataclass@dataclassclassPerson:name:str@propertydefname_as_first_and_last(self)->list:returnself.name.split()# The reason why we create instances of classes is to manage state!person=Person("Ryan McDermott")print(person.name)# => "Ryan McDermott"print(person.name_as_first_and_last)# => ["Ryan", "McDermott"]

⬆ برگشت به بالا

کلاس ها

Single Responsibility Principle (SRP)

رابرت سی. مارتین مینویسد :

یک کلاس باید فقط یک دلیل برای تغییر داشته باشد.

" دلایل تغییر " ذاتاً مسئولیت هایی است که توسط یک کلاس یا تابع مدیریت میشود.

در مثال های زیر، ما یک عنصر HTML را میسازیم که یک کامنت و ورژن را نشان میدهد.

بد :

fromimportlibimportmetadataclassVersionCommentElement:"""An element that renders an HTML comment with the program's version number   """defget_version(self)->str:"""Get the package version"""returnmetadata.version("pip")defrender(self)->None:print(f'<!-- Version:{self.get_version()} -->')VersionCommentElement().render()

این کلاس دو وظیفه دارد:

  • گرفتن نسخه ی پکیج پایتون
  • رندر کردن آن به یک عنصر HTML

هر تغییری در یکی از آن ها، ریسک تاثیر گذاشتن روی آن یکی را ایجاد میکند.ما میتوانیم کلاس را از نو بنویسیم و مسئولیت ها را جدا کنیم.

خوب :

fromimportlibimportmetadatadefget_version(pkg_name:str)->str:"""Retrieve the version of a given package"""returnmetadata.version(pkg_name)classVersionCommentElement:"""An element that renders an HTML comment with the program's version number   """def__init__(self,version:str):self.version=versiondefrender(self)->None:print(f'<!-- Version:{self.version} -->')VersionCommentElement(get_version("pip")).render()

نتیجه این خواهد بود که کلاس فقط باید رندر کردن را به عهده بگیرد. ورژن را هنگام نمونه گیری دریافت میکند و متن توسط تابعی جدا به نامget_version() گرفته میشود. تغییر دادن کلاس هیچ تاثیری روی آن یکی ندارد، و بالعکس، تا زمانی که قرارداد های بینشان تغییر نکند، یعنی فانکشن یک متن برگرداند و متود__init__ کلاس یک رشته را دریافت کند.

و همینطور، تابعget_version() قابل استفاده در همه جا است.

Open/Closed Principle (OCP)

“ قابلیت های جدید را با گسترش دادن ایجاد کنید، نه با تغییر دادن(آن). “ Uncle Bob.

اشیاء باید برای گسترش دادن دردسترس باشند، اما بسته برای تغییرات. باید امکان تقویت عملکرد ارائه شده توسط یک شیء (برای مثال، یک کلاس) بدون تغییر دادن قرارداد های داخلی آن باشد. یک شیء میتواند این قابلیت را هنگامی ایجاد کند که طوری نوشته شده باشد که اجازه اینکار را به راحتی بدهد.

در مثال بعدی، تلاش میکنیم یک فریم ورک وب ساده پیاده سازی کنیم که درخواست های HTTP را هندل و پاسخی را برمیگرداند. کلاسView یک متد.get() دارد که زمانی صدا زده خواهد شد که سرور HTTP یک درخواست GET دریافت کند.

کلاسView به طور قصد ساده است و پاسخ هایtext/plain برمیگرداند. ما همچنین پاسخ هایی بر پایه فایل های تمپلیت میخواهیم پس با کلاسTemplateView از آن ارث بری میکنیم.

بد:

fromdataclassesimportdataclass@dataclassclassResponse:"""An HTTP response"""status:intcontent_type:strbody:strclassView:"""A simple view that returns plain text responses"""defget(self,request)->Response:"""Handle a GET request and return a message in the response"""returnResponse(status=200,content_type='text/plain',body="Welcome to my web site"        )classTemplateView(View):"""A view that returns HTML responses based on a template file."""defget(self,request)->Response:"""Handle a GET request and return an HTML document in the response"""withopen("index.html")asfd:returnResponse(status=200,content_type='text/html',body=fd.read()            )

کلاسTemplateView رفتار های داخلی والدش را تغییر داده تا بتواند عملکرد های پیشرفته تر اضافه کند. با اینکار، کلاسTemplateView به والدش تکیه میکند که عملکرد.get() تغییر نکند، که حالا باید در زمان منجمد شود. برای مثال، نمی‌توانیم برخی بررسی‌های اضافی را در تمام کلاس‌های مشتق ازView معرفی کنیم، زیرا این رفتار حداقل در یک نوع فرعی لغو شده است و ما باید آن را به‌روزرسانی کنیم.

بیایید کلاس های خود را از نو طراحی کنیم تا این مشکل حل شود و اجازه دهیم تا کلاسView به تمیزی گسترش (نه تغییر) داده شود.

خوب :

fromdataclassesimportdataclass@dataclassclassResponse:"""An HTTP response"""status:intcontent_type:strbody:strclassView:"""A simple view that returns plain text responses"""content_type="text/plain"defrender_body(self)->str:"""Render the message body of the response"""return"Welcome to my web site"defget(self,request)->Response:"""Handle a GET request and return a message in the response"""returnResponse(status=200,content_type=self.content_type,body=self.render_body()        )classTemplateView(View):"""A view that returns HTML responses based on a template file."""content_type="text/html"template_file="index.html"defrender_body(self)->str:"""Render the message body as HTML"""withopen(self.template_file)asfd:returnfd.read()

توجه داشته باشید که نیاز داشتیمrender_body() را از نو نویسی کنیم تا منبع بدنه را عوض کنیم، اما این کلاس، تنها یک مسئولیت به درستی تعیین شده دارد کهکلاس های فرعی را دعوت میکند که آن را از نو نویسی کنند. جوری ساخته شده است که اجازه ی گسترش داده شدن توسط کلاس های فرعی را بدهد.یک راه خوب دیگر برای استفاده از قدرت های ارث بری و ترکیب اشیاء استفاده ازMixins ها است.

میکسین ها کلاس های ریشه و ساده ای هستند که به طور انحصاری با سایر کلاس های مرتبط استفاده می شوند. آنها با کلاس هدف با استفاده از وراثت چندگانه "مخلوط" می شوند تا رفتار هدف را تغییر دهند.

چند قانون :

  • میکسین ها باید ازobject ارث بری کنند.
  • میکسین ها همیشه قبل از کلاس هدف میایند. برای مثال :

class Foo(MixinA, MixinB, TargetClass): ...

همچنین خوب :

fromdataclassesimportdataclass,fieldfromtypingimportProtocol@dataclassclassResponse:"""An HTTP response"""status:intcontent_type:strbody:strheaders:dict=field(default_factory=dict)classView:"""A simple view that returns plain text responses"""content_type="text/plain"defrender_body(self)->str:"""Render the message body of the response"""return"Welcome to my web site"defget(self,request)->Response:"""Handle a GET request and return a message in the response"""returnResponse(status=200,content_type=self.content_type,body=self.render_body()        )classTemplateRenderMixin:"""A mixin class for views that render HTML documents using a template file    Not to be used by itself!    """template_file:str=""defrender_body(self)->str:"""Render the message body as HTML"""ifnotself.template_file:raiseValueError("The path to a template file must be given.")withopen(self.template_file)asfd:returnfd.read()classContentLengthMixin:"""A mixin class for views that injects a Content-Length header in the    response    Not to be used by itself!    """defget(self,request)->Response:"""Introspect and amend the response to inject the new header"""response=super().get(request)# type: ignoreresponse.headers['Content-Length']=len(response.body)returnresponseclassTemplateView(TemplateRenderMixin,ContentLengthMixin,View):"""A view that returns HTML responses based on a template file."""content_type="text/html"template_file="index.html"

همانطور که میبینید، میکسین ها ترکیب اشیاء را، با بسته بندی کردن عملکرد های مشابه و تبدیل آنها به یک کلاس قابل استفاده دوباره با یک مسئولیت و اجازه جدا کردن آنها، آسان میکنند.فریم ورک مشهور جنگو استفاده زیادی از میکسین ها میکند تا ویو های مبتنی بر کلاس خود را بسازد.

Liskov Substitution Principle (LSP)

“ توابعی که از اشاره گر ها یا ارجاع ها به کلاس های پایه استفاده میکنند باید بتوانند اشیاء کلاس ارث بری شده را بدون دانستن استفاد کنند. “ Uncle Bob.

این اصل به افتخار Barbara Liskov نامگذاری شده، که با دانشمند کامپیوتر دیگری به نام Jeannette Wing در مقاله ی "A behavioral notion of subtyping (1994)" همکاری کرده است.یک اصل اصلی مقاله این است که " یک نوع فرعی باید رفتار متود های نوع اصلی خود را حفظ و همچنین تمام ویژگی‌های تغییرناپذیر و تاریخی نوع اصلی خود را نگه دارد.

این به این معناست که، یک تابع که یک نوع اصلی را قبول میکند باید همچنین نوع های فرعی آن را بدون هیچ تغیری قبول کند.

میتوانید مشکل کد زیر را پیدا کنید ؟

بد :

fromdataclassesimportdataclass@dataclassclassResponse:"""An HTTP response"""status:intcontent_type:strbody:strclassView:"""A simple view that returns plain text responses"""content_type="text/plain"defrender_body(self)->str:"""Render the message body of the response"""return"Welcome to my web site"defget(self,request)->Response:"""Handle a GET request and return a message in the response"""returnResponse(status=200,content_type=self.content_type,body=self.render_body()        )classTemplateView(View):"""A view that returns HTML responses based on a template file."""content_type="text/html"defget(self,request,template_file:str)->Response:# type: ignore"""Render the message body as HTML"""withopen(template_file)asfd:returnResponse(status=200,content_type=self.content_type,body=fd.read()            )defrender(view:View,request)->Response:"""Render a View"""returnview.get(request)

انتظار میرود که فانکشنrender() به خوبی بتواند باView و زیرکلاسشTemplateView کار کند، اما زیرکلاسش این سازگاری را با تغییر متود.get() از بین برده است.تابع خطایTypeError را هنگامی که باTemplateView استفاده شود برمیگرداند.

اگر بخواهیم که تابعrender() با هر زیرنوعی ازView کار کند، باید توجه کنیم که پروتوکل عمومی آن را نشکنیم. اما از کجا بدانیم که چه چیزی یک کلاس را تشکیل میدهد؟ Type Hinter هایی مثلmypy اگر اشتباهاتی مثل این ببینند، اخطار هایی به این شکل برمیگردانند :

error: Signature of "get" incompatible with supertype "View"<string>:36: note:      Superclass:<string>:36: note:          def get(self, request: Any) -> Response<string>:36: note:      Subclass:<string>:36: note:          def get(self, request: Any, template_file: str) -> Response

Interface Segregation Principle (ISP)

اینترفیس ها را کوچک نگه دارید تا کاربر ها نیاز وابسته به چیزهایی که از آنها بی نیازند نشوند. Uncle Bob.

چندین زبان برنامه نویسی مشهور شیء گرا مثل Go و Java مفهمومی به نام اینترفیس ها دارند. اینترفیس ها متود های عمومی و ویژگی های یک شیء را بدون پیاده سازی آن تعریف میکند. آنها زمانی مفید هستند که نمی خواهیم امضای یک تابع را با یک شیء مشخص مرتبط کنیم.پایتون اینترفیس ندارد. اما به جایش Abstract Base Clase دارد که کمی متقاوت هستند، اما میتوانند همانکار را انجام دهند.

خوب

fromabcimportABCMeta,abstractmethod# Define the Abstract Class for a generic Greeter objectclassGreeter(metaclass=ABCMeta):"""An object that can perform a greeting action."""@staticmethod@abstractmethoddefgreet(name:str)->None:"""Display a greeting for the user with the given name"""classFriendlyActor(Greeter):"""An actor that greets the user with a friendly salutation"""@staticmethoddefgreet(name:str)->None:"""Greet a person by name"""print(f"Hello{name}!")defwelcome_user(user_name:str,actor:Greeter):"""Welcome a user with a given name using the provided actor"""actor.greet(user_name)welcome_user("Barbara",FriendlyActor())

حالا این سناریو را در نظر بگیرید : میخواهیم تعداد مشخصی سند PDF داشته باشیم که ما نوشتیم و میخواهیم به بازدید کننده های وبسایتمان داده شوند. ما از یک وب فریمورک پایتون استفاده میکنیم و شاید وسوسه شویم که کلاسی را طراحی کنیم که این اسناد را مدیریت کند، پس یک کلاس پایه انتزاعی جامع برای سند خود طراحی می کنیم.

خطا

importabcclassPersistable(metaclass=abc.ABCMeta):"""Serialize a file to data and back"""@property@abc.abstractmethoddefdata(self)->bytes:"""The raw data of the file"""@classmethod@abc.abstractmethoddefload(cls,name:str):"""Load the file from disk"""@abc.abstractmethoddefsave(self)->None:"""Save the file to disk"""# We just want to serve the documents, so our concrete PDF document# implementation just needs to implement the `.load()` method and have# a public attribute named `data`.classPDFDocument(Persistable):"""A PDF document"""@propertydefdata(self)->bytes:"""The raw bytes of the PDF document"""        ...# Code goes here - omitted for brevity@classmethoddefload(cls,name:str):"""Load the file from the local filesystem"""        ...# Code goes here - omitted for brevitydefview(request):"""A web view that handles a GET request for a document"""requested_name=request.qs['name']# We want to validate this!returnPDFDocument.load(requested_name).data

اما نمیتونیم! اگر متود.save() را پیاده سازی نکنیم یک خطا به ما نشان داده میشود :

Can't instantiate abstract class PDFDocument with abstract method save.

این اعصاب خورد کن است. ما واقعا نیازی به پیاده سازی.save() در اینجا نداریم. میتوانیم یک متود بنویسیم که هیچ کاری نکند یا خطایNotImplementedError را برگرداند اما این فقط کد اضافی ای است که باید مراقبش باشیم.

در عین حال، اگر.save() را از کلاس انتزاعی حذف کنیم نیاز خواهیم داشت بعدا برای اینکه کاربران بتوانند اسنادشان را ارسال کنند از نو پیاده سازی کنیم که دوباره مارا به خانه ی اول برمیگرداند.

مشکل اینجاست که ما یکاینترفیس نوشتیم که قابلیت هایی دارد که ما در حال حاظر به آنها یا نیاز نداریم یا استفاده نمیکنیم.

راه حل این است که اینترفیست را به قطعات کوچک تر که کارهای جدا میکنند تکه تکه کنیم.

خوب

importabcclassDataCarrier(metaclass=abc.ABCMeta):"""Carries a data payload"""@propertydefdata(self):        ...classLoadable(DataCarrier):"""Can load data from storage by name"""@classmethod@abc.abstractmethoddefload(cls,name:str):        ...classSaveable(DataCarrier):"""Can save data to storage"""@abc.abstractmethoddefsave(self)->None:        ...classPDFDocument(Loadable):"""A PDF document"""@propertydefdata(self)->bytes:"""The raw bytes of the PDF document"""        ...# Code goes here - omitted for brevity@classmethoddefload(cls,name:str):"""Load the file from the local filesystem"""        ...# Code goes here - omitted for brevitydefview(request):"""A web view that handles a GET request for a document"""requested_name=request.qs['name']# We want to validate this!returnPDFDocument.load(requested_name).data

Dependency Inversion Principle (DIP)

“به انتزاع وابسته شوید، نه جزئیات مشخص.”,Uncle Bob.

تصور کنید میخواستیم که یک وب ویو درست کنیم که پاسخ HTTP ای را برمیگرداند که ردیف هایی از فایل CSV ای که ساختیم برمیگراند. میخواهیم از نویسنده ی CSV ای استفاده کنیم که توسط کتابخانه ی استاندارد پایتون ارائه میشود.

بد

importcsvfromioimportStringIOclassStreamingHttpResponse:"""A streaming HTTP response"""    ...# implementation code goes heredefsome_view(request):rows= (       ['First row','Foo','Bar','Baz'],       ['Second row','A','B','C','"Testing"',"Here's a quote"]   )# Define a generator to stream data directly to the clientdefstream():buffer_=StringIO()writer=csv.writer(buffer_,delimiter=';',quotechar='"')forrowinrows:writer.writerow(row)buffer_.seek(0)data=buffer_.read()buffer_.seek(0)buffer_.truncate()yielddata# Create the streaming response  object with the appropriate CSV header.response=StreamingHttpResponse(stream(),content_type='text/csv')response['Content-Disposition']='attachment; filename="somefilename.csv"'returnresponse

اولین پیاده‌سازی ما حول رابط نویسنده CSV با دستکاری یک شیStringIO (که شبه فایل است) و انجام چندین عملیات سطح پایین به منظور جداسازی ردیف‌ها از نویسنده کار می‌کند.

یک راه بهتر این است که از این واقعیت استفاده کنیم که نویسنده به یک شی با متد.write()‍‍ برای انجام کار ما نیاز دارد.چرا یک شی ساختگی که بلافاصله ردیف جدید ساخته شده را برمی گرداند، به آن منتقل نکنیم تا کلاس ‍StreamingHttpResponse بتواند بلافاصله آن را به کلاینت بازگرداند؟

خوب

importcsvclassEcho:"""An object that implements just the write method of the file-like   interface.   """defwrite(self,value):"""Write the value by returning it, instead of storing in a buffer."""returnvaluedefsome_streaming_csv_view(request):"""A view that streams a large CSV file."""rows= (       ['First row','Foo','Bar','Baz'],       ['Second row','A','B','C','"Testing"',"Here's a quote"]   )writer=csv.writer(Echo(),delimiter=';',quotechar='"')returnStreamingHttpResponse(       (writer.writerow(row)forrowinrows),content_type="text/csv",headers={'Content-Disposition':'attachment; filename="somefilename.csv"'},   )

خیلی بهتر شد، و مثل جادو کار میکند! دلیل اینکه پیاده سازی آن بهتر از قبلیست باید واضخ باشد: کد کمتر و بازدهی بیشتر برای رسیدن به پاسخ یکسان. ما تصمیم گرفتیم که از واقعیت این که کلاس نویسنده به انتزاع.write() از شیء ای که دریافت میکند وابسته است، بدون اینکه به جزئیات سطح پایین عملکرد متود توجه ای کند.این مثال ازارسالی به مستندات جنگو توسط این نویسنده گرفته شده است.

⬆ برگشت به بالا

Don't repeat yourself (DRY)

تلاش کنید که اصلDRY را متوجه بشوید.

بهترین تلاشتان را بکنید که کدی را دوباره ننویسید. تکرار کد بد است چون به این معنیست که هنگام ایجاد تغییر، باید بیشتر از دو جا را تغییر دهید.

تصور کنید که صاحب رستورانی هستید و میخواهید آمار انبارتان را داشته باشید: تمامی گوجه ها، پیاز ها، سیر ها، ادویه ها و... . اگر چندین لیست داشته باشید که آمار اینهارا ثبت کنند، هروقت که غذایی با گوجه سرو کنید باید تمامی این لیست هارا آپدیت کنید. اما اگر فقط یک لیست داشته باشید، فقط یک جا را باید آپدیت کنید!

بیشتر اوقات شما وقتی کد را چندین بار تکرار میکنید که دو چیز یا بیشتر کمی متفاوت داشته باشید، که بسیار شبیه به هم باشند، اما این شباهتشان شمارا به داشتن دو تابع یا بیشتر مجبور میکنند که اکثراً یک کار را انجام میدهند. حذف کردن کد های تکراری به معنی ساختن انتزاعی است که بتواند این کارهارا با یک تابع/ماژول/کلاس انجام دهد.

درست ساختن انتزاع مهم است. انتزاع بد میتواند از کد تکراری هم بدتر باشد! با این، اگر میتوانید یک انتزاع خوب بسازید، انجامش دهید! خودتان را تکرار نکنید، وگرنه مجبور میشوید چندین جا را برای تغییر کوچکی آپدیت کنید.

بد:

fromtypingimportList,Dictfromdataclassesimportdataclass@dataclassclassDeveloper:def__init__(self,experience:float,github_link:str)->None:self._experience=experienceself._github_link=github_link@propertydefexperience(self)->float:returnself._experience@propertydefgithub_link(self)->str:returnself._github_link@dataclassclassManager:def__init__(self,experience:float,github_link:str)->None:self._experience=experienceself._github_link=github_link@propertydefexperience(self)->float:returnself._experience@propertydefgithub_link(self)->str:returnself._github_linkdefget_developer_list(developers:List[Developer])->List[Dict]:developers_list= []fordeveloperindevelopers:developers_list.append({'experience' :developer.experience,'github_link' :developer.github_link            })returndevelopers_listdefget_manager_list(managers:List[Manager])->List[Dict]:managers_list= []formanagerinmanagers:managers_list.append({'experience' :manager.experience,'github_link' :manager.github_link            })returnmanagers_list## create list objects of developerscompany_developers= [Developer(experience=2.5,github_link='https://github.com/1'),Developer(experience=1.5,github_link='https://github.com/2')]company_developers_list=get_developer_list(developers=company_developers)## create list objects of managerscompany_managers= [Manager(experience=4.5,github_link='https://github.com/3'),Manager(experience=5.7,github_link='https://github.com/4')]company_managers_list=get_manager_list(managers=company_managers)

خوب:

fromtypingimportList,Dictfromdataclassesimportdataclass@dataclassclassEmployee:def__init__(self,experience:float,github_link:str)->None:self._experience=experienceself._github_link=github_link@propertydefexperience(self)->float:returnself._experience@propertydefgithub_link(self)->str:returnself._github_linkdefget_employee_list(employees:List[Employee])->List[Dict]:employees_list= []foremployeeinemployees:employees_list.append({'experience' :employee.experience,'github_link' :employee.github_link            })returnemployees_list## create list objects of developerscompany_developers= [Employee(experience=2.5,github_link='https://github.com/1'),Employee(experience=1.5,github_link='https://github.com/2')]company_developers_list=get_employee_list(employees=company_developers)## create list objects of managerscompany_managers= [Employee(experience=4.5,github_link='https://github.com/3'),Employee(experience=5.7,github_link='https://github.com/4')]company_managers_list=get_employee_list(employees=company_managers)

⬆ برگشت به بالا

About

🛁 Clean Code concepts adapted for Python

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python98.8%
  • Makefile1.2%

[8]ページ先頭

©2009-2025 Movatter.jp