پاسخها بهترتیب پرسشهای فایل
03-oo-thinking-exercise.md. جایی که راهحل دیگری هم ممکن بود، بخش راه دیگر اضافه شده تا زاویههای مختلف را ببینید.
تمرین ۱ – پرسشهای مفهومی
۱. هر دو class را بلدند؛ پس تفاوت در دانش سینتکس نیست. تفاوت در ذهنیت است: طراح شیءگرا پیش از تایپ اولین حرف، مسئله را در ذهنش به موجودیتها، مسئولیتها و ارتباطها تجزیه کرده است. تازهکار بلافاصله شروع به نوشتن توابع میکند و در نهایت به «یک مشت تابع داخل یک کلاس» میرسد که فقط ظاهرش شیءگراست. بهبیان دیگر، تازهکار میپرسد «چه مراحلی را کد کنم؟» ولی طراح میپرسد «چه موجودیتهایی درگیرند و مسئولیت هرکدام چیست؟».
۲. در یک رستوران اگر یک نفر همهی کارها را انجام دهد (سفارش، آشپزی، ظرفشویی، حساب)، با اولین شلوغی فرومیپاشد. رستوران واقعی کار میکند چون هر نقش مسئولیت مشخصی دارد و همه با هم تعامل میکنند. طراحی شیءگرا هم همین است: بهجای یک موجود همهکاره، مجموعهای از موجودیتهای متخصص. کل هنر شیءگرایی در پاسخ به این یک سوال خلاصه میشود: «این مسئولیت مال چه کسی است؟»
۳. یک روش عملی برای پیداکردن کلاسها: توصیف مسئله را به زبان طبیعی بنویس، بعد اسمها و فعلها را جدا کن.
- اسمها (مشتری، سفارش، فاکتور) کاندیدای کلاس یا attribute هستند.
- فعلها (سفارشدادن، پرداختن، محاسبهکردن) کاندیدای متد هستند.
۴. چون هر اسمی سزاوار کلاسشدن نیست. «قیمت» یک اسم است، اما آنقدر ساده و کممسئولیت است که فقط یک عدد (attribute) میشود، نه یک کلاس مستقل. اگر کورکورانه هر اسم را کلاس کنیم، دهها کلاس بیخاصیت میسازیم. قضاوت شما تعیین میکند کدام اسم آنقدر مهم و پرمسئولیت است که کلاس شود. استراتژی اسم/فعل نقطهی شروع است تا فهرست کاندیداها را بسازید، اما پالایش نهایی با شماست.
۵. «مسئولیت» یعنی کاری که یک کلاس متعهد به انجامش است. یک کلاس خوب، یک مسئولیت منسجم دارد؛ یعنی همهی متدها و دادههایش حول یک هدف میچرخند. سوال کلیدی برای سنجش: «این کلاس چه دلیلی برای تغییر دارد؟» اگر بیش از یک دلیل کاملاً متفاوت پیدا کردید (مثلاً کلاسی که هم منطق سفارش را نگه میدارد و هم ایمیل میفرستد → دو دلیل مستقل: تغییر قوانین سفارش، و تغییر سیستم ایمیل)، احتمالاً بیش از یک مسئولیت دارد و باید شکسته شود.
تمرین ۲ – God Object و تشخیص کلاس/تابع
۶. God Object («شیء خدا») کلاسی است که میخواهد همهکار انجام دهد و همهچیز را بداند. مشکلاتش:
- تمرکز تغییر: هر تغییری در هر گوشهی سیستم (مالیات، درگاه پرداخت، قالب ایمیل) یعنی دستزدن به همین یک فایل که بهسرعت هزاران خط میشود.
- تستناپذیری: برای تست یک بخش کوچک، باید کل آن هیولا را بسازید.
- ناممکنبودن کار تیمی: دو نفر نمیتوانند همزمان رویش کار کنند بیآنکه به هم بخورند.
- عدم استفادهی مجدد: نمیتوانید فقط «بخش پرداخت» را جدا کنید و در پروژهی دیگری ببرید.
۷. نشانههای کلاس:
- دادهای دارید که با رفتار گره خورده و باید حالتش را در طول زمان نگه دارد (مثل سبد خرید).
- چند نمونهی مستقل از یک مفهوم میخواهید (هزار مشتری، هرکدام با دادهی خودش).
- میخواهید چند پیادهسازی قابلجایگزین از یک رفتار داشته باشید (چند نوع درگاه پرداخت).
نشانههای تابع:
- یک ورودی میگیرید، یک خروجی میدهید و حالتی برای نگهداشتن نیست (تبدیل واحد دما).
- منطق، بیحالت (Stateless) و مستقل است.
قاعدهی سرانگشتی: اگر داده و رفتار باید با هم زندگی کنند و حالت نگه دارند → کلاس؛ اگر فقط یک تبدیل بیحالت است → تابع.
۸.
- حالت (State): دادهای که شیء در خود نگه میدارد و در طول زمان تغییر میکند (موجودی حساب، آیتمهای سبد). State در attributeها زندگی میکند.
- رفتار (Behavior): کارهایی که شیء میتواند انجام دهد و معمولاً حالتش را میخوانند یا تغییر میدهند. Behavior در متدها زندگی میکند.
قدرت شیءگرایی در این است که رفتار (مثل deposit) نیازی ندارد کسی موجودی را به آن پاس بدهد؛ خودش از طریق self.balance به حالت دسترسی دارد.
تمرین ۳ – کدنویسی و تحلیل
۹. تحلیل سیستم پارکینگ:
اسمها: کاربر، پارکینگ، خودرو، جایپارک، شماره، مدتزمان، هزینه، رسید. فعلها: پارککردن، محاسبهکردن (هزینه)، صادرکردن (رسید).
پالایش — کدام کلاس، کدام attribute:
| نامزد | نتیجه | چرا |
|---|---|---|
| کاربر (User) | کلاس | موجودیت مستقل، چند نمونه دارد |
| پارکینگ (ParkingLot) | کلاس | هماهنگکنندهی کل سیستم |
| جایپارک (Slot) | کلاس | حالت دارد (خالی/اشغال)، رفتار دارد |
| خودرو (Car) | کلاس (یا attribute ساده) | اگر فقط پلاک مهم است میتواند attribute باشد |
| شماره (number) | attribute | فقط یک عدد روی Slot |
| مدتزمان (duration) | attribute | یک مقدار عددی |
| هزینه (fee) | attribute یا خروجی متد | نتیجهی محاسبه است |
| رسید (Receipt) | کلاس (یا attribute) | اگر ساختار دارد، کلاس |
یک طرح ممکن:
class Slot: # important, stateful noun → class
def __init__(self, number):
self.number = number # simple noun → attribute
self.occupied = False # state: free or occupied
def is_free(self): # verb → method
return not self.occupied
class ParkingLot: # coordinator → class
def __init__(self):
self.slots = []
def park(self, car, slot): ... # verb "to park"
def calculate_fee(self, duration): ... # verb "to compute"
def issue_receipt(self): ... # verb "to issue"
نکته: مهم نیست جواب شما دقیقاً همین باشد. مهم این است که «شماره» و «مدتزمان» را attribute دیدید و «جایپارک» را بهخاطر حالتدار بودنش کلاس دیدید.
۱۰. بازآرایی BlogSystem به کلاسهای متخصص. هر کلاس یک مسئولیت:
class User: # managing user identity
def write_post(self, text): ...
class Post: # the post itself and its data
def __init__(self, text):
self.text = text
class HtmlRenderer: # only conversion to HTML
def render(self, post): ...
class EmailService: # only sending email
def send(self, user, msg): ...
class PostRepository: # only save/load from the database
def save(self, post): ...
class ReadingTimeCalculator: # only computing reading time
def estimate(self, text): ...
هر مشکل سیستم حالا در یک کلاس کوچک و مستقل زندگی میکند: تغییر قالب HTML فقط HtmlRenderer را دست میزند، تغییر دیتابیس فقط PostRepository را.
راه دیگر: اگر پروژه کوچک است، شاید calculate_reading_time نیازی به یک کلاس کامل نداشته باشد و یک تابع سادهی بیحالت کافی باشد (def reading_time(text): ...). این دقیقاً همان «کلاس یا تابع؟» است — محاسبهی زمان مطالعه بیحالت است، پس تابع هم انتخاب درستی است. اینجا Tradeoff بین «یکدستی همهچیز کلاس باشد» و «سادگی تابع» وجود دارد.
۱۱.
- الف) تابع. تبدیل ارز با نرخ ثابت بیحالت است: ورودی میگیرد، خروجی میدهد، چیزی برای نگهداشتن نیست.
- ب) کلاس. سبد خرید حالت دارد (فهرست آیتمها) که در طول زمان تغییر میکند.
- ج) تابع. میانگینگرفتن یک تبدیل بیحالت ورودی→خروجی است.
- د) کلاس. بازیکن چند حالت بههممرتبط دارد (جان، امتیاز، موقعیت) که در طول بازی تغییر میکنند و رفتارها آنها را میخوانند/مینویسند.
۱۲. کلاس Counter:
class Counter:
def __init__(self):
self.value = 0 # State: the counter's state
def increment(self): # Behavior
self.value += 1 # behavior changes the state
def reset(self): # Behavior
self.value = 0
استفاده:
c = Counter()
c.increment()
c.increment()
print(c.value) # 2
c.reset()
print(c.value) # 0
- State:
self.value— تنها دادهای که شیء نگه میدارد. - Behavior:
incrementوreset— متدهایی که حالت را تغییر میدهند.
راه دیگر (پذیرش مقدار اولیهی دلخواه و گام دلخواه): میتوان انعطاف بیشتری داد:
class Counter:
def __init__(self, start=0, step=1):
self.value = start
self.step = step
def increment(self):
self.value += self.step
def reset(self):
self.value = 0
این نسخه همچنان یک مسئولیت دارد (شمارش)، اما قابلتنظیمتر است. Tradeoff: کمی پیچیدگی بیشتر در ازای انعطاف بیشتر — اگر واقعاً به گام دلخواه نیاز ندارید، نسخهی ساده بهتر است (کد را ساده نگه دارید تا وقتی نیاز واقعی پیدا شود).
تمرین ۴ – مقایسه و تحلیل
۱۳.
| برنامهنویس رویهای | طراح شیءگرا | |
|---|---|---|
| سوال اول | «چه مراحلی باید اجرا شود؟» | «چه موجودیتهایی درگیرند؟» |
| واحد فکر | تابع و مرحله | شیء و مسئولیت |
| جای داده | متغیرهای جدا، پاسدادن دستی | درون همان شیء مسئول |
(دو ردیف دیگر فصل: «در برابر تغییر» → رویهای باید مراحل را دنبال و دستکاری کند، شیءگرا موجودیت مربوطه را عوض میکند؛ «نتیجه در پروژهی بزرگ» → رویهای به درهمتنیدگی میرسد، شیءگرا به ماژولهای مستقل.)
۱۴. این توزیع درست است چون هر دانش، جایی زندگی میکند که مالکش است:
- «آیا این نسخه آزاد است؟» دانشی است که مال خود
Copyاست (نسخه میداند دست کسی هست یا نه) →is_availableدرCopy. - «آیا این عضو ظرفیت دارد؟» دانشی است که مال
Memberاست →can_borrowدرMember. - «امانتدادن» یک هماهنگی بین چند موجودیت است: باید از نسخه و عضو سوال بپرسد و اگر همهچیز درست بود ثبت کند →
borrowدرLibrary.
اگر همه را در یک متد بزرگ Library.borrow میریختیم، Library به دادهی داخلی همه دست میزد و تبدیل به یک God Object میشد. آنوقت اگر فردا قانون «سقف امانت» تغییر میکرد، باید در دل Library دنبالش میگشتیم؛ اما در طرح درست، فقط Member.can_borrow را عوض میکنیم و بس. توزیع مسئولیت، تغییر را محلی و امن میکند.
تمرین ۵ – پرسش تحلیلی
۱۵. این جمله بیشازحد سادهانگارانه است. استراتژی اسم/فعل فقط یک نقطهی شروع برای ساختن فهرست کاندیداهاست، نه یک قاعدهی خودکار. اگر هر اسم را کلاس کنیم:
- دهها کلاس بیخاصیت میسازیم که فقط یک دادهی سادهاند (مثل «قیمت» یا «شماره») و پیچیدگی بیمورد اضافه میکنند.
- ممکن است اسمهایی که در واقع دو روی یک سکهاند را جدا کنیم یا برعکس.
نسخهی اصلاحشده: «اسمها را بهعنوان کاندیدا علامت بزن، بعد با قضاوت پالایش کن؛ اسمی که حالت و رفتار منسجم دارد کلاس میشود، اسمی که فقط یک مقدار ساده است attribute میشود.» طراحی همیشه به قضاوت انسانی نیاز دارد، نه اجرای مکانیکی یک دستور.
۱۶. منظور این است که طراحی خوب یکبار و کامل از ذهن بیرون نمیآید؛ در طول کارکردن با مسئله کشف میشود. اولین طرح شما تقریباً همیشه ناقص است، و این کاملاً طبیعی است. طراحی شیءگرا یک فرایند تکرارشونده است: میسازید، به مشکل میخورید، بازآرایی (Refactor) میکنید، بهتر میشود.
پیام برای تازهکار: فلجشدن از ترس نداشتن طرح کامل اولیه اشتباه است. آنچه در این فصل یاد گرفتید یک «قطبنما» است که جهت را نشان میدهد، نه «نقشهی دقیقی» که هر پیچوخم را از پیش بداند. با یک طرح معقول شروع کنید، کد بزنید، و اجازه دهید مسئله طراحی بهتر را به شما نشان دهد.