بناء مكتبة صغيرة
Build a Small Library
بناء مكتبة صغيرة — Build a Small Library
وصلنا إلى المختبر. لقد تعلمت ما هي الوحدة وما هي الحزمة وكيف تستورد منهما. الآن حان وقت التفكير كمهندس برمجيات حقيقي: كيف تُقرر ماذا تضع في أي ملف؟ ما الذي تُعيد تصديره؟ وكيف تبني واجهة نظيفة يسعد مستخدموها باستخدامها؟
هذا المختبر يأخذك خلال تصميم mathkit — مكتبة بسيطة للعمليات الحسابية والإحصائية. لن تُنشئ الملفات في هذا المتصفح (Pyodide بيئة ملف واحد)، لكنك ستُطبّق كل المفاهيم بالضبط كما يجري في مشروع حقيقي — فقط عبر تعريف الدوال مباشرة وتجربتها، مع رسم الهيكل أمامك.
السيناريو
تعمل على أداة تحليل درجات طلاب. تريد بناء حزمة mathkit يستطيع زملاؤك استيرادها وقول:
from mathkit import add, subtract, mean, median, variance
دون أن يعرفوا أين يوجد كل دالة داخلياً. هذا هو الهدف: واجهة نظيفة تُخفي التنظيم الداخلي.
هيكل mathkit
هكذا سيبدو المشروع الكامل:
my_project/
├── main.py # البرنامج الذي يستخدم المكتبة — consumer
└── mathkit/ # الحزمة الرئيسية — the package
├── __init__.py # واجهة المكتبة + إعادة التصدير — public API
├── arithmetic.py # دوال الجمع والطرح — basic arithmetic
└── stats.py # دوال الإحصاء — statistical functions
ما يوجد في كل ملف
mathkit/arithmetic.py — يحمل العمليات الأساسية:
# mathkit/arithmetic.py
def add(a, b):
"""جمع عددين — Add two numbers."""
return a + b
def subtract(a, b):
"""طرح عددين — Subtract two numbers."""
return a - b
بسيط وواضح. هذه الوحدة تعرف شيئاً واحداً: الحساب الأساسي. لا تعرف شيئاً عن الإحصاء أو قواعد البيانات أو واجهة المستخدم.
mathkit/stats.py — يحمل العمليات الإحصائية:
# mathkit/stats.py
import math # نستخدم math من المكتبة القياسية — use stdlib
def mean(values):
"""المتوسط الحسابي — Arithmetic mean of a list."""
return sum(values) / len(values)
def median(values):
"""الوسيط — Middle value of a sorted list."""
sorted_vals = sorted(values)
n = len(sorted_vals)
mid = n // 2
if n % 2 == 1:
return sorted_vals[mid] # عدد فردي — odd count
return (sorted_vals[mid - 1] + sorted_vals[mid]) / 2 # عدد زوجي — even count
def variance(values):
"""تباين المجتمع — Population variance."""
avg = mean(values)
return sum((x - avg) ** 2 for x in values) / len(values)
لاحظ كيف تستخدم stats.py دالة mean الموجودة في نفس الوحدة — هذا صحيح تماماً. لكنها لا تستورد من arithmetic.py لأن العمليتين مستقلتان.
mathkit/__init__.py — الواجهة العامة:
# mathkit/__init__.py
# نُعيد تصدير ما يحتاجه المستخدم — re-export the public API
from mathkit.arithmetic import add, subtract
from mathkit.stats import mean, median, variance
# نُعرّف ما يُصدَّر عند import * — define what's exported on star-import
__all__ = ["add", "subtract", "mean", "median", "variance"]
هذا الملف هو بوابة الحزمة. يُقرر ما الذي يراه المستخدم. إذا قررت لاحقاً نقل mean إلى وحدة مختلفة، يكفي تعديل هذا الملف — كل كود المستخدمين يبقى كما هو.
main.py — كيف يستخدم المستخدم الحزمة:
# main.py
from mathkit import add, mean, variance
scores = [75, 82, 91, 68, 95, 73]
print(f"المجموع: {add(75, 82)}")
print(f"المتوسط: {mean(scores):.2f}")
print(f"التباين: {variance(scores):.2f}")
لماذا هذا التقسيم؟
السؤال الحقيقي في تصميم الوحدات ليس “هل يعمل الكود؟” بل “هل سيكون الكود قابلاً للصيانة والتطوير؟”
مبدأ المسؤولية الواحدة (Single Responsibility): arithmetic.py تعرف الجمع والطرح. stats.py تعرف الإحصاء. لو أضفت لاحقاً geometry.py للأشكال الهندسية، ستفعل ذلك دون لمس الملفات الموجودة. هذا التصميم يجعل كل تغيير مركّزاً في مكان واضح.
إخفاء التعقيد: مستخدم المكتبة لا يحتاج أن يعرف أن mean في stats.py وadd في arithmetic.py. يكتب فقط from mathkit import mean, add ويمضي. هذا ما يُسمى بـ abstraction — تُخفي التعقيد خلف واجهة بسيطة.
المرونة في إعادة الهيكلة: لو قررت يوماً تقسيم stats.py إلى stats/central_tendency.py وstats/dispersion.py، يكفي تعديل __init__.py ولا شيء آخر. المستخدمون لن يلاحظوا الفرق.
قابلية الاختبار: يمكنك اختبار arithmetic.py بمعزل تام عن stats.py. لو كسر تعديل في الإحصاء، تختبر فقط stats.py. هذا يُقلل وقت التصحيح بشكل كبير.
متى تُقسّم ومتى لا؟
ليس كل وحدتين منفصلتين تستحقان حزمة. قاعدة عملية: إذا كان الكود سيُعاد استخدامه في أكثر من مشروع، أو كانت وحداتك تحمل مسؤوليات واضحة ومختلفة، ضعها في حزمة. إذا كان مشروعك ملفاً أو ملفين، لا داعي للتعقيد.
الأخطاء الشائعة في التنظيم:
- مجلد
utilsيحمل كل شيء: عندما لا تعرف أين تضع الدالة، تضعها فيutils. هذا المجلد يكبر حتى يصبح مستنقعاً. الحل: اسأل “ما المسؤولية الوحيدة لهذا الملف؟” ثم سمّه بناءً عليها. - ملف واحد لكل دالة: إنشاء ملف منفصل لكل دالة هو إفراط في التقسيم يُصعّب التنقل. كتلة وظيفية واحدة = ملف واحد.
- تصدير كل شيء: ليس كل ما تكتبه يجب أن يكون في
__init__.py. صدّر فقط ما تريد مستخدمك أن يعتمد عليه. كل اسم مُصدَّر هو وعد بالاستقرار.
تحديات المختبر
الآن طبّق ما تعلمته. كل تحدٍّ يُطبّق جزءاً من mathkit.
التحدي 1: دالة mean
التحدي 2: دالة median
التحدي 3: دالة variance
التحدي 4: تصميم الوحدات
هذا التحدي يختبر فهمك لأين تضع كل دالة في mathkit. أكمل الدوال لتُرجع اسم الوحدة الصحيح لكل دالة:
مكتبتك في العمل
لتكتمل الصورة، إليك ما سيبدو عليه كود main.py حين تُشغّل المكتبة كاملة على ملف بيانات حقيقي. هذا تجميع لكل ما بنيناه في تحديات واحدة:
خلاصة المختبر
صممت اليوم حزمة كاملة من الصفر. الأهم ليس حفظ الأوامر، بل فهم الأسئلة التي تطرحها على نفسك عند كل قرار:
قبل إنشاء ملف: ما المسؤولية الواحدة التي سيحملها هذا الملف؟ هل سيظل اسمه واضحاً بعد ستة أشهر؟
قبل تصدير دالة من __init__.py: هل يحتاج مستخدم الحزمة هذه الدالة فعلاً؟ أم أنها تفصيل داخلي؟
قبل كتابة import: هل الاستيراد المطلق أوضح هنا؟ أم أن الاستيراد النسبي يجعل الحزمة أكثر قابلية للنقل؟
هذه الأسئلة هي ما يُفرّق بين مطور يكتب كوداً يعمل، ومطور يكتب كوداً يعمل ويظل قابلاً للصيانة والنمو.