الهياكل (Structs): تصميم أنواع بيانات مخصصة (مثل: كائن يمثل موظف أو منتج)


الهياكل (Structs): تصميم أنواع بيانات مخصصة (مثل: كائن يمثل موظف أو منتج)

ماذا سنتعلم اليوم؟ سنتعمق في الهياكل (Structs) لتصميم أنواع بيانات مخصصة، مما يتيح لنا تمثيل كائنات معقدة مثل الموظفين أو المنتجات بكفاءة داخل العقود الذكية.

سنقوم ببناء عقد ذكي في Solidity لإدارة المنتجات، حيث يمثل كل منتج هيكلاً مخصصاً يحمل خصائصه.

مقدمة: لماذا الهياكل (Structs)؟

في عالم البرمجة، وخاصة في العقود الذكية، نحتاج غالباً لتمثيل كائنات أكثر تعقيداً من مجرد أرقام أو نصوص. على سبيل المثال، قد نحتاج لتمثيل موظف بـ (اسم، عمر، راتب، قسم) أو منتج بـ (معرف، اسم، سعر، كمية). هنا يأتي دور الهياكل (Structs).

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

ملاحظة تقنية: الهياكل في Solidity تشبه الكلاسات في لغات مثل JavaScript أو C++ من حيث تجميع البيانات، لكنها لا تدعم سلوكيات (مثل الدوال داخلها مباشرة) بالطريقة نفسها في العقود الذكية، بل تركز على تخزين البيانات.

الخطوة 1: تعريف هيكل البيانات (Struct) للمنتج

لنبدأ بتعريف هيكل Product. سيحتوي هذا الهيكل على المعرف، الاسم، السعر، والكمية لكل منتج. سنضع هذا التعريف داخل العقد الذكي الخاص بنا.

الكود:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ProductManager {
    // تعريف الهيكل (Struct) المسمى 'Product'
    // هذا الهيكل سيحتوي على خصائص كل منتج
    struct Product {
        uint id;          // معرف المنتج، رقم فريد غير سالب
        string name;      // اسم المنتج، سلسلة نصية
        uint price;       // سعر المنتج (بالوحدة الأصغر، مثلاً السنت)
        uint quantity;    // الكمية المتوفرة من المنتج
    }

    // سيتم إضافة المزيد من الكود هنا في الخطوات التالية
}

الشرح:

  • يبدأ تعريف الهيكل بالكلمة المفتاحية struct متبوعة باسم الهيكل (Product).
  • داخل الأقواس المعقوفة {}، نحدد المتغيرات (أو الحقول) التي ستكون جزءاً من هذا الهيكل، مع تحديد نوع كل متغير.

الخطوة 2: تخزين الهياكل وإنشاء المنتجات

بعد تعريف هيكل Product، نحتاج إلى طريقة لتخزين هذه المنتجات في العقد الذكي. سنستخدم mapping لربط معرف فريد لكل منتج بهيكل Product الخاص به. ثم سنضيف دالة لإضافة منتجات جديدة.

الكود:

// ... (الكود السابق)

contract ProductManager {
    struct Product {
        uint id;
        string name;
        uint price;
        uint quantity;
    }

    // تعريف mapping لتخزين المنتجات.
    // كل معرف (uint) سيرتبط بكائن Product.
    mapping(uint => Product) public products;
    
    // متغير لتتبع معرف المنتج التالي المتاح، يبدأ من 1.
    uint public nextProductId = 1;

    /**
     * @dev دالة لإضافة منتج جديد إلى المخزن.
     * @param _name اسم المنتج.
     * @param _price سعر المنتج.
     * @param _quantity الكمية المتوفرة.
     */
    function addProduct(string memory _name, uint _price, uint _quantity) public {
        // إنشاء كائن Product جديد وتعيين قيمه
        // ثم تخزينه في الـ mapping باستخدام nextProductId كمعرف له.
        products[nextProductId] = Product(nextProductId, _name, _price, _quantity);
        
        // زيادة معرف المنتج التالي لضمان فرادته.
        nextProductId++;
    }
}

الشرح:

  • mapping(uint => Product) public products;: ينشئ خريطة (map) حيث المفتاح هو uint (معرف المنتج) والقيمة هي هيكل Product. كلمة public تجعل هذا الـ mapping متاحاً للقراءة من خارج العقد.
  • uint public nextProductId = 1;: متغير عام يتتبع المعرف الفريد للمنتج التالي الذي سيتم إضافته.
  • دالة addProduct: تأخذ خصائص المنتج كمدخلات، تنشئ كائناً جديداً من نوع Product، وتخزنه في الـ products mapping، ثم تزيد nextProductId.

الخطوة 3: استرجاع وتحديث بيانات المنتجات

الآن بعد أن أصبح بإمكاننا إضافة المنتجات، نحتاج إلى دوال لاسترجاع تفاصيل منتج معين وتحديث بياناته (مثل الكمية).

الكود:

// ... (الكود السابق)

contract ProductManager {
    struct Product {
        uint id;
        string name;
        uint price;
        uint quantity;
    }

    mapping(uint => Product) public products;
    uint public nextProductId = 1;

    function addProduct(string memory _name, uint _price, uint _quantity) public {
        products[nextProductId] = Product(nextProductId, _name, _price, _quantity);
        nextProductId++;
    }

    /**
     * @dev دالة لاسترجاع تفاصيل منتج معين بناءً على معرفه.
     * @param _id معرف المنتج المراد استرجاعه.
     * @return (id, name, price, quantity) تفاصيل المنتج.
     */
    function getProduct(uint _id) public view returns (uint, string memory, uint, uint) {
        // استرجاع كائن Product من الـ mapping.
        // 'storage' يعني أننا نعمل مباشرة على البيانات المخزنة في البلوك تشين.
        Product storage p = products[_id];
        
        // إرجاع حقول الهيكل كقيم فردية.
        return (p.id, p.name, p.price, p.quantity);
    }

    /**
     * @dev دالة لتحديث كمية منتج موجود.
     * @param _id معرف المنتج المراد تحديثه.
     * @param _newQuantity الكمية الجديدة للمنتج.
     */
    function updateProductQuantity(uint _id, uint _newQuantity) public {
        // التحقق من أن معرف المنتج موجود وصالح.
        require(_id < nextProductId && _id > 0, "Product ID does not exist.");
        
        // تحديث حقل 'quantity' للمنتج المحدد في الـ mapping.
        products[_id].quantity = _newQuantity;
    }
}

الشرح:

  • دالة getProduct: تأخذ _id كمدخل، تسترجع كائن Product المرتبط بهذا المعرف من الـ products mapping، ثم تعيد حقوله الفردية. استخدام view يعني أن هذه الدالة لا تعدل حالة العقد ولا تكلف رسوماً (gas) عند التنفيذ (فقط رسوم قراءة).
  • دالة updateProductQuantity: تأخذ _id و _newQuantity. تتحقق أولاً من وجود المنتج باستخدام require، ثم تقوم بتحديث حقل quantity في الهيكل المخزن. هذه الدالة تعدل حالة العقد وتتطلب رسوماً.

الكود النهائي الكامل

هذا هو الكود الكامل للعقد الذكي ProductManager الذي يستخدم الهياكل (Structs) لإدارة المنتجات:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ProductManager {
    // تعريف الهيكل (Struct) المسمى 'Product'
    // هذا الهيكل سيحتوي على خصائص كل منتج
    struct Product {
        uint id;          // معرف المنتج، رقم فريد غير سالب
        string name;      // اسم المنتج، سلسلة نصية
        uint price;       // سعر المنتج (بالوحدة الأصغر، مثلاً السنت)
        uint quantity;    // الكمية المتوفرة من المنتج
    }

    // تعريف mapping لتخزين المنتجات.
    // كل معرف (uint) سيرتبط بكائن Product.
    mapping(uint => Product) public products;
    
    // متغير لتتبع معرف المنتج التالي المتاح، يبدأ من 1.
    uint public nextProductId = 1;

    /**
     * @dev دالة لإضافة منتج جديد إلى المخزن.
     * @param _name اسم المنتج.
     * @param _price سعر المنتج.
     * @param _quantity الكمية المتوفرة.
     */
    function addProduct(string memory _name, uint _price, uint _quantity) public {
        // إنشاء كائن Product جديد وتعيين قيمه
        // ثم تخزينه في الـ mapping باستخدام nextProductId كمعرف له.
        products[nextProductId] = Product(nextProductId, _name, _price, _quantity);
        
        // زيادة معرف المنتج التالي لضمان فرادته.
        nextProductId++;
    }

    /**
     * @dev دالة لاسترجاع تفاصيل منتج معين بناءً على معرفه.
     * @param _id معرف المنتج المراد استرجاعه.
     * @return (id, name, price, quantity) تفاصيل المنتج.
     */
    function getProduct(uint _id) public view returns (uint, string memory, uint, uint) {
        // استرجاع كائن Product من الـ mapping.
        // 'storage' يعني أننا نعمل مباشرة على البيانات المخزنة في البلوك تشين.
        Product storage p = products[_id];
        
        // إرجاع حقول الهيكل كقيم فردية.
        return (p.id, p.name, p.price, p.quantity);
    }

    /**
     * @dev دالة لتحديث كمية منتج موجود.
     * @param _id معرف المنتج المراد تحديثه.
     * @param _newQuantity الكمية الجديدة للمنتج.
     */
    function updateProductQuantity(uint _id, uint _newQuantity) public {
        // التحقق من أن معرف المنتج موجود وصالح.
        require(_id < nextProductId && _id > 0, "Product ID does not exist.");
        
        // تحديث حقل 'quantity' للمنتج المحدد في الـ mapping.
        products[_id].quantity = _newQuantity;
    }
}

النتيجة المتوقعة

عند نشر هذا العقد على شبكة البلوك تشين (مثل شبكة تطوير محلية أو Testnet)، يمكنك التفاعل معه كالتالي:

  1. استدعاء addProduct("لابتوب", 120000, 10): سيضيف منتجاً جديداً باسم "لابتوب"، بسعر 120000 (مثلاً 1200 دولار إذا اعتبرنا 100 وحدة كسنت)، وكمية 10. سيتم تعيين معرف المنتج 1 تلقائياً.
  2. استدعاء addProduct("ماوس", 2500, 50): سيضيف منتجاً آخر باسم "ماوس"، بسعر 2500، وكمية 50. سيتم تعيين معرف المنتج 2 تلقائياً.
  3. استدعاء getProduct(1): سيعيد لك تفاصيل المنتج الأول: (1, "لابتوب", 120000, 10).
  4. استدعاء updateProductQuantity(1, 8): سيقوم بتحديث كمية "لابتوب" إلى 8.
  5. استدعاء getProduct(1) مرة أخرى: سيعيد لك تفاصيل المنتج الأول المحدثة: (1, "لابتوب", 120000, 8).

هذا يوضح كيف تسمح لنا الهياكل بإنشاء أنواع بيانات مخصصة لتمثيل كائنات معقدة وإدارتها بكفاءة داخل العقود الذكية.