مشروع: نظام إدارة مكتبة — البرمجة الكائنية
مشروع: نظام إدارة مكتبة
في هذا المشروع تصمم برنامجاً من عدة أصناف تتعاون، وهو الجزء الأصعب والأهم في البرمجة الكائنية: تحديد مسؤولية كل صنف.
المتطلبات
- إضافة كتب (عنوان، مؤلف، رقم 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"))
اختبر فهمك