فصل سوم: تفکر شیءگرا — چگونه مانند یک طراح شیءگرا فکر کنیم؟

Please login to bookmark Close

مقدمه

در فصل قبل سینتکس پایه را یاد گرفتیم: کلاس، شیء، self و سازنده. حالا class نوشتن را بلدید — اما یک مهارت مهم‌تر باقی مانده: کی و چرا کلاس بسازید.

تفاوت یک برنامه‌نویس تازه‌کار با یک طراح، در دانستن سینتکس نیست — هر دو class را بلدند. تفاوت در این است که طراح، پیش از تایپ‌کردن اولین حرف، مسئله را در ذهنش به موجودیت‌ها، مسئولیت‌ها و ارتباط‌ها تجزیه کرده است. در این فصل دقیقاً همین مهارت را تمرین می‌کنیم — و چون سینتکس را در فصل قبل یاد گرفتیم، حالا می‌توانیم آزادانه با کد نشانش دهیم.

یاد می‌گیریم چطور یک مسئله‌ی مبهم دنیای واقعی را به یک طراحی شیءگرا ترجمه کنیم: چطور کلاس‌ها را پیدا کنیم، چطور مسئولیت‌ها را تقسیم کنیم، و چطور از دام‌های رایجی مثل «God Object» فرار کنیم. در پایان، یک سیستم واقعی (مدیریت کتابخانه) را با هم تحلیل می‌کنیم.

چرا سینتکس کافی نیست و باید تفکر شیءگرا را یاد بگیریم؟

یک اتفاق بسیار رایج را تصور کنید. برنامه‌نویسی تازه با class آشنا شده و ذوق‌زده است. به او می‌گویند «یک سیستم ثبت سفارش بنویس.» او فوراً شروع می‌کند:

class OrderSystem:
    def add_customer(self, name): ...
    def remove_customer(self, id): ...
    def create_order(self, customer_id, items): ...
    def calculate_price(self, order_id): ...
    def apply_discount(self, order_id, code): ...
    def process_payment(self, order_id, card): ...
    def send_invoice_email(self, order_id): ...
    def update_inventory(self, item_id, count): ...
    def generate_report(self): ...

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

مشکل این برنامه‌نویس، سینتکس نیست — او class را بلد است. مشکل، ذهنیت است. او یاد نگرفته مسئله را به موجودیت‌های مستقل بشکند.

یادگیری تفکر شیءگرا به شما کمک می‌کند:

  • پیش از کدنویسی، مسئله را درست تحلیل کنید
  • کلاس‌هایی با مسئولیت روشن بسازید، نه انبارهایی از توابع
  • از کلاس‌های غول‌آسا و بی‌هدف (God Object) پرهیز کنید
  • کدی بنویسید که در برابر تغییر، انعطاف داشته باشد
  • طرحی بریزید که دیگران هم بتوانند سریع بفهمند

تمرین ذهنیت: از مسئله تا کلاس

یک استعاره برای شروع: تقسیم مسئولیت

یک رستوران را در نظر بگیرید. اگر یک نفر همه‌ی کارها را انجام دهد — سفارش بگیرد، آشپزی کند، ظرف بشوید، حساب کند — رستوران با اولین شلوغی فرو می‌پاشد. رستوران واقعی کار می‌کند چون هر نقش، مسئولیت مشخصی دارد: پیش‌خدمت سفارش می‌گیرد، آشپز غذا می‌پزد، صندوق‌دار حساب می‌کند. هیچ‌کدام کار دیگری را انجام نمی‌دهد، اما همه با هم تعامل می‌کنند.

طراحی شیءگرا دقیقاً همین است: به‌جای یک موجود همه‌کاره، مجموعه‌ای از موجودیت‌های متخصص می‌سازید که هرکدام یک کار را خوب انجام می‌دهند و از طریق پیام با هم کار می‌کنند. کل هنر شیءگرایی در پاسخ به یک سوال خلاصه می‌شود: «این مسئولیت مال چه کسی است؟»

استراتژی اسم‌ها و فعل‌ها

یک روش عملی و کلاسیک برای پیداکردن کلاس‌ها این است: توصیف مسئله را به زبان طبیعی بنویسید، بعد اسم‌ها و فعل‌ها را جدا کنید.

  • اسم‌ها (مشتری، سفارش، محصول، فاکتور) نامزدهای خوبی برای کلاس یا attribute هستند.
  • فعل‌ها (سفارش‌دادن، پرداختن، محاسبه‌کردن) نامزدهای خوبی برای متد هستند.

مثال: «مشتری یک سفارش ثبت می‌کند. هر سفارش شامل چند محصول است. سیستم قیمت کل را محاسبه می‌کند و یک فاکتور صادر می‌کند.»

اسم‌ها: مشتری، سفارش، محصول، قیمت، فاکتور. فعل‌ها: ثبت‌کردن، شامل‌بودن، محاسبه‌کردن، صادرکردن.

از این تحلیل، یک طرح اولیه بیرون می‌آید:

class Customer:      # noun → class
    def place_order(self, products): ...   # verb "to place" → method

class Order:         # noun → class
    def __init__(self, products):
        self.products = products           # noun "product" → attribute
    def total_price(self): ...             # verb "to compute" → method

class Product:       # noun → class
    def __init__(self, name, price):
        self.name = name
        self.price = price                 # noun "price" → attribute

هشدار مهم: این استراتژی یک نقطه‌ی شروع است، نه یک قاعده‌ی مقدس. هر اسمی کلاس نمی‌شود. «قیمت» در اینجا فقط یک عدد (attribute) شد، نه یک کلاس. قضاوت شما تعیین می‌کند کدام اسم آن‌قدر مهم و پرمسئولیت است که سزاوار کلاس‌شدن باشد. در ادامه معیارش را می‌بینیم.

گام‌های استخراج کلاس از نیازمندی‌ها

می‌توان این فرایند را به چند گام مشخص شکست:

  1. نیازمندی را به زبان ساده بنویس. اگر نتوانی مسئله را با جمله توضیح دهی، هنوز آماده‌ی طراحی نیستی.
  2. اسم‌ها را علامت بزن. فهرست موجودیت‌های کاندیدا را بساز.
  3. اسم‌های تکراری و بی‌اهمیت را حذف کن. آن‌هایی که فقط داده‌ی ساده‌اند (مثل «قیمت») به attribute تبدیل می‌شوند.
  4. فعل‌ها را به موجودیت‌های مربوطه نسبت بده. «چه کسی مسئول این کار است؟»
  5. ارتباط‌ها را مشخص کن. کدام موجودیت به کدام یکی نیاز دارد؟ (مشتری سفارش دارد، سفارش محصول دارد.)
  6. بازبینی کن. آیا کلاسی هست که کارهای بی‌ربط انجام دهد؟ آیا مسئولیتی هست که جایش گم شده؟

تفاوت برنامه‌نویس رویه‌ای و طراح شیءگرا

این تفاوت را در یک جدول ببینیم، چون قلب این فصل است:

برنامه‌نویس رویه‌ایطراح شیءگرا
سوال اول«چه مراحلی باید اجرا شود؟»«چه موجودیت‌هایی درگیرند؟»
واحد فکرتابع و مرحلهشیء و مسئولیت
جای دادهمتغیرهای جدا، پاس‌دادن دستیدرون همان شیء مسئول
در برابر تغییرباید مراحل را دنبال و دستکاری کندموجودیت مربوطه را عوض می‌کند
نتیجه در پروژه‌ی بزرگدرهم‌تنیدگی و شکنندگیماژول‌های مستقل و قابل‌جایگزینی

مفهوم مسئولیت: قلب طراحی خوب

مسئولیت یعنی کاری که یک کلاس متعهد به انجامش است. یک کلاس خوب، یک مسئولیت منسجم دارد — یعنی همه‌ی متدها و داده‌هایش حول یک هدف می‌چرخند.

معیار سنجش: به کلاس نگاه کنید و بپرسید «این کلاس چه دلیلی برای تغییر دارد؟» اگر بیش از یک دلیل کاملاً متفاوت پیدا کردید، احتمالاً بیش از یک مسئولیت دارد. مثلاً کلاسی که هم منطق سفارش را نگه می‌دارد و هم ایمیل می‌فرستد، دو دلیل مستقل برای تغییر دارد: تغییر قوانین سفارش، و تغییر سیستم ایمیل. این دو باید جدا شوند. (این ایده، هسته‌ی اصل Single Responsibility در فصل هشتم است.)

دام بزرگ: God Object

God Object («شیء خدا») کلاسی است که می‌خواهد همه‌کار انجام دهد و همه‌چیز را بداند. همان OrderSystem ابتدای فصل، یک God Object است.

چرا بد است؟

# anti-pattern: a class that does everything
class Shop:
    def __init__(self):
        self.customers = []
        self.orders = []
        self.inventory = {}
        self.payments = []

    def add_customer(self, name): ...
    def calculate_tax(self, order): ...
    def charge_credit_card(self, number): ...
    def send_email(self, to, body): ...
    def render_html_report(self): ...
    def backup_database(self): ...

این کلاس چند مشکل جدی دارد.

  • اول، هر تغییری در هر گوشه‌ی سیستم (نرخ مالیات، درگاه پرداخت، قالب ایمیل) یعنی دست‌زدن به همین یک فایل — که به‌سرعت به هزاران خط می‌رسد.
  • دوم، تست‌کردنش کابوس است؛ برای تست محاسبه‌ی مالیات هم باید کل این هیولا را بسازید.
  • سوم، دو نفر نمی‌توانند همزمان رویش کار کنند بی‌آنکه به هم بخورند.
  • چهارم، استفاده‌ی مجدد ممکن نیست؛ نمی‌توانید فقط «بخش پرداخت» را در پروژه‌ی دیگری ببرید.

راه‌حل، شکستن آن به موجودیت‌های متخصص است:

class Customer: ...
class Order: ...
class TaxCalculator: ...
class PaymentProcessor: ...
class EmailService: ...
class ReportRenderer: ...

هرکدام یک مسئولیت، یک دلیل برای تغییر، و امکان تست و استفاده‌ی مستقل.

تشخیص: کلاس یا تابع؟

هر مسئله‌ای کلاس نمی‌خواهد. گاهی یک تابع ساده، تمیزترین راه‌حل است. این معیارها کمک می‌کنند:

نشانه‌های اینکه کلاس مناسب است:

  • داده‌ای دارید که با رفتار گره خورده و باید در طول زمان حالتش را نگه دارد (مثلاً موجودی یک سبد خرید که آیتم‌ها به آن اضافه و کم می‌شود).
  • چند نمونه‌ی مستقل از یک مفهوم می‌خواهید (هزار مشتری، هرکدام با داده‌ی خودش).
  • می‌خواهید چند پیاده‌سازی قابل‌جایگزین از یک رفتار داشته باشید (چند نوع درگاه پرداخت).

نشانه‌های اینکه تابع کافی است:

  • یک ورودی می‌گیرید، یک خروجی می‌دهید، و حالتی برای نگه‌داشتن نیست (مثلاً تبدیل واحد دما).
  • منطق، بی‌حالت (Stateless) و مستقل است.
# a function is enough here — stateless, simple input/output
def celsius_to_fahrenheit(c: float) -> float:
    return c * 9 / 5 + 32

# a class is needed here — it has state and changes over time
class ShoppingCart:
    def __init__(self):
        self.items = []
    def add(self, product):
        self.items.append(product)
    def total(self):
        return sum(p.price for p in self.items)

قاعده‌ی سرانگشتی: اگر داده و رفتار باید با هم زندگی کنند و حالت نگه دارند، کلاس؛ اگر فقط یک تبدیل بی‌حالت است، تابع.

حالت (State) در برابر رفتار (Behavior)

هر شیء دو چیز دارد:

  • حالت (State): داده‌ای که شیء در خود نگه می‌دارد — مقدارهایی که در طول زمان تغییر می‌کنند. مثلاً موجودی حساب، آیتم‌های سبد خرید. حالت در attributeها زندگی می‌کند.
  • رفتار (Behavior): کارهایی که شیء می‌تواند انجام دهد و معمولاً حالتش را می‌خوانند یا تغییر می‌دهند. رفتار در متدها زندگی می‌کند.
class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance      # State

    def deposit(self, amount):      # Behavior
        self.balance += amount      # behavior changes the state

    def withdraw(self, amount):     # Behavior
        if amount > self.balance:
            raise ValueError("insufficient balance")
        self.balance -= amount

قدرت شیءگرایی دقیقاً در همین کنار هم بودن است: deposit نیازی به این ندارد که کسی موجودی را به آن پاس بدهد؛ خودش از طریق self.balance به حالتش دسترسی دارد. این همان چیزی است که کد رویه‌ای فاقدش بود.

تمرین عملی: تحلیل یک سیستم مدیریت کتابخانه

بیایید همه‌ی این‌ها را روی یک مسئله‌ی واقعی پیاده کنیم. نیازمندی، به زبان ساده:

«یک کتابخانه می‌خواهد سیستمی داشته باشد که در آن اعضا بتوانند کتاب امانت بگیرند و برگردانند. هر کتاب ممکن است چند نسخه داشته باشد. کتابخانه باید بداند هر نسخه الان دست کیست و هر عضو چند کتاب امانت گرفته. یک عضو نمی‌تواند بیش از سه کتاب همزمان امانت بگیرد.»

گام ۱ و ۲ — اسم‌ها را علامت بزنیم: کتابخانه، عضو، کتاب، نسخه، امانت.

گام ۳ — پالایش: «کتاب» (عنوان، نویسنده) با «نسخه» (یک جلد فیزیکی مشخص از آن کتاب) فرق دارد. این تمایز مهم است: کتابخانه سه نسخه از یک کتاب دارد، و هر نسخه جداگانه امانت داده می‌شود. پس هر دو کلاس می‌شوند.

گام ۴ — فعل‌ها را نسبت بدهیم:

  • امانت‌گرفتن و برگرداندن → به Member مربوط است (عضو این کار را می‌کند)، اما Library هماهنگش می‌کند.
  • «دست کیست» → حالت Copy (نسخه).
  • «چند کتاب امانت گرفته» → حالت Member.

گام ۵ — ارتباط‌ها: کتابخانه شامل کتاب‌ها و اعضاست. هر کتاب چند نسخه دارد. هر نسخه ممکن است به یک عضو امانت داده شده باشد.

طرح جمع‌بندی‌شده، به‌شکل درختی:

Library (هماهنگ‌کننده‌ی کل سیستم)
├── books: list[Book]
│     └── Book (عنوان، نویسنده)
│           └── copies: list[Copy]
│                 └── Copy (شناسه، وضعیت، امانت‌گیرنده)
└── members: list[Member]
      └── Member (نام، فهرست نسخه‌های امانت‌گرفته)

قوانین کسب‌وکار:
  • هر عضو حداکثر ۳ نسخه‌ی همزمان
  • یک نسخه یا آزاد است یا دست یک عضو

حالا این طرح را به کد ترجمه می‌کنیم. توجه کنید که هر کلاس فقط مسئولیت خودش را دارد:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.copies = []          # which copies this book has

class Copy:
    def __init__(self, copy_id, book):
        self.copy_id = copy_id
        self.book = book
        self.borrowed_by = None   # state: who holds it (None means free)

    def is_available(self):
        return self.borrowed_by is None

class Member:
    MAX_BOOKS = 3

    def __init__(self, name):
        self.name = name
        self.borrowed = []        # state: which copies are borrowed

    def can_borrow(self):
        return len(self.borrowed) < self.MAX_BOOKS

class Library:
    def __init__(self):
        self.books = []
        self.members = []

    def borrow(self, member, copy):
        # the library coordinates the rules
        if not copy.is_available():
            raise ValueError("this copy is not available")
        if not member.can_borrow():
            raise ValueError("this member's loan limit is full")
        copy.borrowed_by = member
        member.borrowed.append(copy)

    def return_copy(self, member, copy):
        copy.borrowed_by = None
        member.borrowed.remove(copy)

به چند تصمیم طراحی دقت کنید.

  • اول، قاعده‌ی «آیا این نسخه آزاد است؟» درون خود Copy زندگی می‌کند (is_available)، نه در Library — چون این دانش مال نسخه است.
  • دوم، قاعده‌ی «آیا این عضو ظرفیت دارد؟» درون Member است (can_borrow)، چون این دانش مال عضو است.
  • سوم، Library فقط هماهنگ می‌کند: از هرکدام سوال می‌پرسد و اگر همه‌چیز درست بود، امانت را ثبت می‌کند. این توزیع درست مسئولیت است.

مقایسه کنید با نسخه‌ی God Object که همه‌ی این قوانین را در یک متد بزرگ Library.borrow می‌ریخت و مستقیم به داده‌ی همه دست می‌زد. نسخه‌ی ما تغییرپذیرتر است: اگر فردا سقف امانت به کتاب عضو خاص بستگی داشت، فقط Member.can_borrow را عوض می‌کنیم.

خلاصه

از مسئله تا کلاس — گردش کار تفکر شیءگرا
════════════════════════════════════════════
  ۱. مسئله را به زبان ساده بنویس
        │
        ▼
  ۲. اسم‌ها را پیدا کن ──► کاندیدای کلاس/attribute
        │
        ▼
  ۳. فعل‌ها را پیدا کن ──► کاندیدای متد
        │
        ▼
  ۴. بپرس: «این مسئولیت مال کیست؟»
        │
        ▼
  ۵. ارتباط‌ها را مشخص کن (کی به کی نیاز دارد)
        │
        ▼
  ۶. بازبینی: God Object نساخته‌ای؟
مفهومنکته‌ی کلیدی
استراتژی اسم/فعلاسم‌ها کلاس، فعل‌ها متد — اما با قضاوت
مسئولیتهر کلاس یک دلیل منسجم برای تغییر
God Objectکلاس همه‌کاره؛ ضدالگو، باید شکسته شود
کلاس یا تابع؟حالت‌دار و پرنمونه → کلاس؛ بی‌حالت → تابع
State در برابر Behaviorداده در attribute، رفتار در متد
نقش هماهنگ‌کنندهیک کلاس می‌تواند فقط بقیه را هماهنگ کند

نکته‌ی طلایی: طراحی خوب، «کشف» می‌شود نه «تحمیل». اولین طرح شما کامل نخواهد بود، و این کاملاً طبیعی است. طراحی شیءگرا یک فرایند تکرارشونده است: می‌سازید، به مشکل می‌خورید، بازآرایی (Refactor) می‌کنید. آنچه در این فصل یاد گرفتید، قطب‌نماست نه نقشه‌ی دقیق.

پرسش‌های مصاحبه

سوال‌های این فصل معمولاً در قالب «طراحی زنده» می‌آیند، نه تعریف — آماده‌ی وایت‌بورد باشید.

۱. «برای فلان سیستم (مثلاً پارکینگ)، کلاس‌هایش را استخراج کن.» — مصاحبه‌گر فرایند را می‌سنجد نه جواب را: بلندبلند فکر کنید؛ اول اسم‌ها (کاندیدای کلاس/attribute)، بعد فعل‌ها (کاندیدای متد)، بعد مسئولیت‌ها و ارتباطات. و حتماً جایی بگویید «این اسم، کلاس نمی‌شود چون حالت و رفتار مستقل ندارد؛ attribute است» — همین جمله شما را از حفظ‌کارها جدا می‌کند.

۲. «God Object چیست و چطور جلویش را می‌گیرید؟» — کلاسی که همه‌کاره است: چند محور تغییر بی‌ربط، وابستگی همه به آن، تست‌ناپذیر. پیشگیری: سوال «این کلاس چند دلیل برای تغییر دارد؟» و شکستن بر اساس مسئولیت. اشاره کنید که God Object معمولاً «تدریجی» متولد می‌شود — با هر «حالا این متد را هم همین‌جا بگذار».

۳. «کی اصلاً نباید کلاس ساخت؟» — وقتی نه حالتی هست نه هویتی؛ فقط ورودی→خروجی. تابع ساده خواناتر و تست‌پذیرتر است. جواب ممتاز یک قدم جلوتر می‌رود: «کلاس تک‌متده با __init__ معمولاً بوی تابع گم‌شده می‌دهد.»

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

Please login to bookmark Close
نظرات

دیدگاهتان را بنویسید

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

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

تنظیمات

انتخاب زبان
تغییر تم