Kodax

مشروع: نظام إدارة مكتبة — البرمجة الكائنية

مشروع: نظام إدارة مكتبة

في هذا المشروع تصمم برنامجاً من عدة أصناف تتعاون، وهو الجزء الأصعب والأهم في البرمجة الكائنية: تحديد مسؤولية كل صنف.

المتطلبات

- إضافة كتب (عنوان، مؤلف، رقم ISBN) وعدة نسخ من الكتاب.

- تسجيل أعضاء.

- استعارة نسخة وإرجاعها، مع حد أقصى 3 كتب لكل عضو.

- منع استعارة كتاب لا توجد منه نسخة متاحة.

- عرض الكتب المتأخرة.

تحديد الأصناف

اكتب الأسماء في المتطلبات وفكّر: أيها "أشياء" لها بيانات وسلوك؟

| الصنف | البيانات | المسؤولية |

|---|---|---|

| Book | العنوان، المؤلف، ISBN | وصف الكتاب فقط (dataclass) |

| Member | الاسم، الكتب المستعارة | معرفة هل يستطيع الاستعارة |

| Loan | الكتاب، العضو، تاريخ الإرجاع | هل الإعارة متأخرة؟ |

| Library | المخزون، الأعضاء، الإعارات | تنسيق العمليات وتطبيق القواعد |

لاحظ التركيب: Library تحتوي كتباً وأعضاء وإعارات، ولا ترث من أي منها.

أخطاء مخصصة

class LibraryError(Exception): ...
class NotAvailableError(LibraryError): ...
class LimitReachedError(LibraryError): ...

وراثة الأخطاء من صنف أب واحد تسمح بالتقاطها كلها معاً except LibraryError أو كلٌّ على حدة.

قرارات تصميم في الكود

- المخزون قاموس {isbn: عدد النسخ المتاحة}: البحث بالـ ISBN مباشر.

- التاريخ يُمرَّر إلى borrow و overdue بدل استدعاء date.today() داخلها: هذا يجعل الاختبار ممكناً بتواريخ ثابتة.

- Member.can_borrow خاصية محسوبة، فالقاعدة في مكان واحد.

- Library هي الوحيدة التي تعدّل المخزون؛ لا أحد يغيّر _stock من الخارج (تغليف).

اختبر تصميمك بالأسئلة

- لو أضفنا "حجز كتاب"، أي صنف يتغير؟

- لو تغير الحد الأقصى للاستعارة للأعضاء المميزين؟ (فكر: خاصية في Member، أو صنف PremiumMember يرث منه.)

أفكار للتوسعة

- غرامة تأخير لكل يوم.

- حفظ البيانات في JSON (من دورة هياكل البيانات).

- البحث عن الكتب بالمؤلف.

ما الذي أتقنته؟

تحويل متطلبات مكتوبة إلى أصناف بمسؤوليات واضحة، واستخدام التغليف والتركيب والأخطاء المخصصة و dataclasses معاً. الدورة التالية "Python المتقدم" تكمل بالـ decorators والمولّدات والاختبارات والبرمجة غير المتزامنة.

from dataclasses import dataclass, field
from datetime import date, timedelta

class LibraryError(Exception):
    pass

class NotAvailableError(LibraryError):
    pass

class LimitReachedError(LibraryError):
    pass

@dataclass(frozen=True)
class Book:
    isbn: str
    title: str
    author: str

@dataclass
class Member:
    name: str
    max_books: int = 3
    loans: list = field(default_factory=list)

    @property
    def can_borrow(self):
        return len(self.loans) < self.max_books

@dataclass
class Loan:
    book: Book
    member: Member
    due: date

    def is_overdue(self, today):
        return today > self.due

class Library:
    LOAN_DAYS = 14

    def __init__(self):
        self._books = {}
        self._stock = {}
        self._loans = []

    def add_book(self, book, copies=1):
        self._books[book.isbn] = book
        self._stock[book.isbn] = self._stock.get(book.isbn, 0) + copies

    def available(self, isbn):
        return self._stock.get(isbn, 0)

    def borrow(self, member, isbn, today):
        if not member.can_borrow:
            raise LimitReachedError(f"{member.name} وصل للحد الأقصى")
        if self.available(isbn) == 0:
            raise NotAvailableError("لا توجد نسخة متاحة")
        loan = Loan(self._books[isbn], member, today + timedelta(days=self.LOAN_DAYS))
        self._stock[isbn] -= 1
        member.loans.append(loan)
        self._loans.append(loan)
        return loan

    def give_back(self, loan):
        loan.member.loans.remove(loan)
        self._loans.remove(loan)
        self._stock[loan.book.isbn] += 1

    def overdue(self, today):
        return [loan for loan in self._loans if loan.is_overdue(today)]

library = Library()
library.add_book(Book("111", "مقدمة ابن خلدون", "ابن خلدون"), copies=1)
library.add_book(Book("222", "تعلم Python", "منى"), copies=2)

sara = Member("سارة")
start = date(2026, 3, 1)

first = library.borrow(sara, "111", start)
print("استعارت:", first.book.title, "حتى", first.due)

try:
    library.borrow(Member("علي"), "111", start)
except LibraryError as error:
    print("مرفوض:", error)

print("متأخرة بعد 20 يوماً:", [l.book.title for l in library.overdue(start + timedelta(days=20))])
library.give_back(first)
print("النسخ المتاحة الآن:", library.available("111"))
اختبر فهمك