Software Architecture

التوفيق بين البرمجة الكائنية والوظيفية: تطبيق مبادئ SOLID في البرمجة الوظيفية

عند النظر الأول، تبدو نماذج البرمجة الكائنية (OOP) والبرمجة الوظيفية (FP) متعاكستين تمامًا. تعتمد OOP على الحالة القابلة للتغيير والإرث والتغليف، بينما تناصر FP عدم القابلية للتغيير والدوال النقية وتحويلات البيانات. ومع ذلك، الهدف الأساسي لكليهما هو نفسه: بناء برمجيات قابلة للصيانة والتوسع والاختبار. توفر مبادئ SOLID، التي صيغت في الأصل للبرمجة الكائنية، إطارًا قويًا لهذا الهدف. عند تطبيقها بحكمة، تتوافق بشكل مفاجئ مع المفاهيم الوظيفية، مما يوفر نهجًا هجينًا يعظم نقاط القوة في العالمين.

مبدأ المسؤولية المفردة (SRP): دالة واحدة، مهمة واحدة

في البرمجة الكائنية، يحدد مبدأ المسؤولية المفردة (SRP) أن يجب أن يكون للفئة سبب واحد فقط للتغيير. في البرمجة الوظيفية، يتحول هذا إلى الحفاظ على الدوال صغيرة ومركزة على تحويل واحد. بدلاً من إنشاء فئة كبيرة تحتوي على عدة طرق قد تتغير لأسباب مختلفة، نقوم بتفكيك المنطق إلى دوال نقية صغيرة.

// سيئ: دالة تقوم بالكثير من المهام
function processOrder(order) {
    validateOrder(order);
    calculateTax(order);
    sendEmail(order.customer);
    updateDatabase(order);
}

// جيد: دوال صغيرة ومركزة ونقية
const validateOrder = (order) => order.items.length > 0;
const calculateTax = (amount) => amount * 0.08;
const createInvoice = (order, tax) => ({ ...order, tax });

function processOrder(order) {
    if (!validateOrder(order)) throw new Error("Invalid Order");
    const tax = calculateTax(order.total);
    const invoice = createInvoice(order, tax);
    // يتم التعامل مع الآثار الجانبية بشكل منفصل أو عبر المونادات
    return invoice;
}

من خلال ضمان أن تقوم كل دالة بمهمة واحدة بشكل جيد، نضمن أن التغييرات في منطق الضريبة لن تكسر منطق التحقق. هذه الوحدة النمطية هي تفسير وظيفي لمبدأ المسؤولية المفردة (SRP).

مبدأ المفتوح/المغلق (OCP): توسيع السلوك دون تعديل

ينص مبدأ المفتوح/المغلق (OCP) على أن الكيانات يجب أن تكون مفتوحة للتوسيع ومغلقة للتعديل. في البرمجة الوظيفية، نحقق ذلك من خلال الدوال من الرتبة الأعلى وأنماط الاستراتيجية المطبقة كمعاملات دالة. بدلاً من إنشاء فئة فرعية من فئة `Shape` لإضافة `Square` جديد، نمرر دالة حساب كحجة.

// تتطلب نهج البرمجة الكائنية تعديل الفئة الأساسية أو إنشاء فئات فرعية
// نهج البرمجة الوظيفية: استخدام التركيب ومعاملات الدالة

const areaCalculator = (shape) => {
    switch (shape.type) {
        case 'circle':
            return Math.PI * shape.radius ** 2;
        case 'square':
            return shape.side ** 2;
        case 'rectangle':
            return shape.width * shape.height;
        default:
            throw new Error("Unknown shape");
    }
};

// لإضافة 'triangle'، تقوم بتعديل دالة الحاسبة.
// للالتزام الصارم بمبدأ OCP، يمكننا استخدام جدول بحث:

const areaStrategies = {
    circle: (s) => Math.PI * s.radius ** 2,
    square: (s) => s.side ** 2,
    triangle: (s) => 0.5 * s.base * s.height // توسعة جديدة
};

const getArea = (shape) => areaStrategies[shape.type](shape);

بينما لا يزال جدول البحث يتطلب تعديلًا لإضافة مفاتيح، فإنه يعزل التغيير في تعريف البيانات بدلاً من المنطق الأساسي. تتضمن تقنيات البرمجة الوظيفية الأكثر تقدمًا تمرير دالة الاستراتيجية نفسها كاعتماد، مما يجعل دالة `getArea` غير واعية تمامًا بالأشكال المحددة التي تعالجها.

مبدأ استبدال ليسكوف (LSP): أمان النوع والعقود

يضمن مبدأ استبدال ليسكوف (LSP) أنه إذا كان `S` نوعًا فرعيًا من `T`، فيمكن استبدال `T` بـ `S` دون تغيير صحة البرنامج. في اللغات الوظيفية، غالبًا ما يتم تحقيق LSP من خلال أنظمة الأنواع القوية وأنواع البيانات الجبرية. إذا كانت دالة تتوقع `Animal` وقمت بتمرير `Dog`، يجب أن تستمر الدالة في العمل بشكل صحيح لأن `Dog` يحقق المتطلبات الهيكلية أو الاسمية لـ `Animal`.

// مثال TypeScript يوضح LSP عبر الترميز الهيكلي
interface Animal {
    name: string;
    eat(): void;
}

function feed(animal: Animal) {
    console.log(`Feeding ${animal.name}`);
    animal.eat();
}

class Dog implements Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    eat(): void {
        console.log(`${this.name} is eating kibble`);
    }
    // يمكن تمرير Dog إلى feed() بأمان
}

في البرمجة الوظيفية النقية، نتجنب مصائد الإرث بالاعتماد على التركيب وقيود النوع الصريحة. إذا كانت الدالة `f: A -> B`، فإن أي دالة تقبل `A` وتعيد `B` هي بديل صالح، شريطة أن تلتزم بنفس قيود الآثار الجانبية.

مبدأ فصل الواجهات (ISP): واجهات دقيقة

يقترح مبدأ فصل الواجهات (ISP) ألا يُجبر أي عميل على الاعتماد على طرق لا يستخدمها. في البرمجة الكائنية، يتم تحقيق ذلك بإنشاء واجهات أصغر. في البرمجة الوظيفية، بما أننا لا نستخدم الواجهات بنفس الطريقة، نحقق ISP من خلال تفكيك الدوال وحقن الاعتمادات للدوال المحددة بدلاً من الكائنات الكبيرة.

بدلاً من تمرير كائن `UserManager` كبير يحتوي على 20 طريقة حيث لا يتم استخدام سوى طريقتين، نمرر الدوال المحددة المطلوبتين:

// سيئ: اقتران بكائن كبير
function renderProfile(userManager: UserManager) {
    const user = userManager.getUser();
    const photos = userManager.getPhotos();
    // ...
}

// جيد: اقتران ضعيف عبر دوال محددة
function renderProfile(
    getUser: () => User,
    getPhotos: (userId: string) => Photo[]
) {
    const user = getUser();
    const photos = getPhotos(user.id);
    // ...
}

يضمن هذا النهج أن `renderProfile` تعتمد فقط على السلوك الدقيق الذي تتطلبه، مما يجعلها أسهل في الاختبار والتعديل.

مبدأ عكس الاعتماد (DIP): التجريد فوق التنفيذ

ينص مبدأ عكس الاعتماد (DIP) على أن الوحدات العليا يجب ألا تعتمد على الوحدات الدنيا؛ بل يجب أن تعتمد كلتاهما على التجريدات. في البرمجة الوظيفية، نحقق ذلك بالاعتماد على الأنواع (التجريدات) وحقن التطبيقات المحددة (الدوال) وقت التشغيل. هذا هو جوهر تطوير البرمجيات الموجه بالاختبار (TDD) في اللغات الوظيفية.

// وحدة عليا تعتمد على تجريد (نوع)
type DataStore = {
    save: (item: Item) => Promise;
    fetch: (id: string) => Promise;
};

function processItem(store: DataStore, item: Item) {
    // منطق يعتمد فقط على واجهة DataStore
    return store.save(item);
}

// وحدات دنيا (تطبيقات)
const memoryStore: DataStore = {
    save: async (item) => { /* ... */ },
    fetch: async (id) => { /* ... */ }
};

const dbStore: DataStore = {
    save: async (item) => { /* ... */ },
    fetch: async (id) => { /* ... */ }
};

// استخدام الدالة العليا مع تجريدات مختلفة
processItem(memoryStore, item1);
processItem(dbStore, item2);

من خلال تعريف `DataStore` كاختصار نوع أو واجهة، نخلق تجريدًا. تعتمد دالة `processItem` على هذا التجريد، وليس على التطبيق المحدد. يسمح لنا هذا باستبدال مخزن البيانات للاختبار أو الإنتاج دون تعديل منطق الأعمال.

الخلاصة: نهج هجين لبنية قوية

تطبيق مبادئ SOLID في البرمجة الوظيفية لا يتعلق بإجبار أنماط البرمجة الكائنية على كود البرمجة الوظيفية، بل يتعلق بالاعتراف بأهداف التصميم الكامنة: الوحدة النمطية، قابلية الاختبار، وقابلية الصيانة. من خلال الاستفادة من الدوال النقية وعدم القابلية للتغيير وأنظمة الأنواع القوية، يمكننا تحقيق روح SOLID مع الاستمتاع بفوائد الكود الوظيفي. النتيجة هي برمجيات أسهل في الاستدلال والاختبار والتوسيع، بغض النظر عن النموذج الذي نختار التركيز عليه.

عند تصميم نظامك التالي، فكر في هذه المبادئ ليس كقواعد صارمة، بل كمنارات إرشادية تساعدك على بناء مكونات تعمل معًا بتناغم. سواء كنت تستخدم JavaScript أو TypeScript أو Scala أو Rust، يبقى الفكرة الأساسية: صمّم للتغيير، وسيبقى كودك مرناً في مواجهة المتطلبات المتطورة.

Share: