لماذا يصر معظم مهندسي النظام على البرمجة الأولى للواجهة؟

StackOverflow https://stackoverflow.com/questions/48605

سؤال

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

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

هل كانت مفيدة؟

المحلول

البرمجة على واجهة تعني احترام "العقد" الذي تم إنشاؤه باستخدام تلك الواجهة.وهكذا إذا كان لديك IPoweredByMotor واجهة لديها start() الطريقة، الطبقات المستقبلية التي تنفذ الواجهة، سواء كانت MotorizedWheelChair, Automobile, ، أو SmoothieMaker, ، عند تنفيذ أساليب تلك الواجهة، قم بإضافة المرونة إلى نظامك، لأن قطعة واحدة من التعليمات البرمجية يمكنها تشغيل محرك العديد من أنواع الأشياء المختلفة، لأن كل ما تحتاج قطعة واحدة من التعليمات البرمجية إلى معرفته هو أنها تستجيب لـ start().لا يهم كيف أنها تبدأ، فقط أنهم يجب أن تبدأ.

نصائح أخرى

سؤال عظيم.سأحيلك إلى جوش بلوخ في جافا الفعالة, الذي يكتب (البند 16) لماذا يفضل استخدام الواجهات على الفئات المجردة.بالمناسبة، إذا لم يكن لديك هذا الكتاب، أوصي به بشدة!وفيما يلي ملخص لما يقول:

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

ماذا عن ميزة الفصول المجردة التي توفر التنفيذ الأساسي؟يمكنك توفير فئة تنفيذ هيكلية مجردة مع كل واجهة.يجمع هذا بين مزايا كل من الواجهات والفئات المجردة.توفر التطبيقات الهيكلية مساعدة في التنفيذ دون فرض القيود الشديدة التي تفرضها الفئات المجردة عندما تكون بمثابة تعريفات للنوع.على سبيل المثال، إطار المجموعات يحدد النوع الذي يستخدم الواجهات، ويوفر تنفيذًا هيكليًا لكل منها.

توفر البرمجة للواجهات العديد من الفوائد:

  1. مطلوب لأنماط نوع GoF، مثل نمط الزائر

  2. يسمح للتطبيقات البديلة.على سبيل المثال، قد توجد تطبيقات متعددة لكائنات الوصول إلى البيانات لواجهة واحدة تلخص محرك قاعدة البيانات قيد الاستخدام (قد يقوم كل من AccountDaoMySQL وAccountDaoOracle بتنفيذ AccountDao)

  3. قد تقوم الفئة بتنفيذ واجهات متعددة.لا تسمح Java بالوراثة المتعددة للفئات المحددة.

  4. تفاصيل تنفيذ الملخصات.قد تتضمن الواجهات طرق واجهة برمجة التطبيقات العامة فقط، مما يؤدي إلى إخفاء تفاصيل التنفيذ.تشمل المزايا واجهة برمجة التطبيقات العامة الموثقة بشكل نظيف والعقود الموثقة جيدًا.

  5. تُستخدم بكثرة في أطر حقن التبعية الحديثة، مثل http://www.springframework.org/.

  6. في Java، يمكن استخدام الواجهات لإنشاء وكلاء ديناميكيين - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html.يمكن استخدام هذا بشكل فعال للغاية مع أطر عمل مثل Spring لتنفيذ البرمجة الموجهة نحو الجانب.يمكن للجوانب إضافة وظائف مفيدة جدًا إلى الفئات دون إضافة تعليمات برمجية Java مباشرة إلى تلك الفئات.تتضمن أمثلة هذه الوظيفة التسجيل والتدقيق ومراقبة الأداء وترسيم المعاملات وما إلى ذلك. http://static.springframework.org/spring/docs/2.5.x/reference/aop.html.

  7. التطبيقات الوهمية، اختبار الوحدة - عندما تكون الفئات التابعة عبارة عن تطبيقات للواجهات، يمكن كتابة فئات وهمية تقوم أيضًا بتنفيذ تلك الواجهات.يمكن استخدام الفئات الوهمية لتسهيل اختبار الوحدة.

أعتقد أن أحد أسباب تخلي المطورين عن الفئات المجردة إلى حد كبير قد يكون سوء فهم.

عندما عصابة الأربعة كتب:

البرنامج إلى واجهة وليس تنفيذ.

لم يكن هناك شيء مثل واجهة Java أو C#.كانوا يتحدثون عن مفهوم الواجهة الموجهة للكائنات، الذي تمتلكه كل فئة.يذكر ذلك إريك جاما في هذه المقابلة.

أعتقد أن اتباع جميع القواعد والمبادئ بشكل آلي دون تفكير يؤدي إلى صعوبة القراءة والتنقل والفهم والحفاظ على قاعدة التعليمات البرمجية.يتذكر:أبسط شيء يمكن أن ينجح.

كيف ذلك؟

لأن هذا ما تقوله جميع الكتب.مثل أنماط GoF، يرى العديد من الأشخاص أنها جيدة عالميًا ولا يفكرون أبدًا فيما إذا كان هذا هو التصميم الصحيح حقًا أم لا.

كيف تعرف كل العلاقات بين الكائنات التي ستحدث داخل تلك الواجهة؟

أنت لا تفعل، وهذه مشكلة.

إذا كنت تعرف هذه العلاقات بالفعل ، فلماذا لا تمتد فئة مجردة فقط؟

أسباب عدم تمديد فئة مجردة:

  1. لديك تطبيقات مختلفة جذريًا وإنشاء فئة أساسية لائقة أمر صعب للغاية.
  2. أنت بحاجة إلى حرق فئتك الأساسية الوحيدة من أجل شيء آخر.

إذا لم ينطبق أي منهما، فاستمر واستخدم فئة مجردة.سيوفر لك الكثير من الوقت.

أسئلة لم تطرحها:

ما هي الجوانب السلبية لاستخدام الواجهة؟

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

هل أنا حقا بحاجة سواء؟

في معظم الوقت، لا.فكر جيدًا قبل إنشاء أي تسلسل هرمي للكائنات.هناك مشكلة كبيرة في لغات مثل Java وهي أنها تجعل من السهل جدًا إنشاء تسلسلات هرمية ضخمة ومعقدة للكائنات.

خذ بعين الاعتبار المثال الكلاسيكي الذي ورثته LameDuck من Duck.يبدو سهلا، أليس كذلك؟

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

تعني البرمجة إلى واجهة احترام "العقد" الذي تم إنشاؤه باستخدام تلك الواجهة

هذا هو الشيء الوحيد الذي يساء فهمه فيما يتعلق بالواجهات.

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

وهذا الاعتقاد الخاطئ منتشر على نطاق واسع لدرجة أنه يعتبره كثير من الناس حكمة تقليدية.ومع ذلك، فمن الخطأ.

لذلك هذا البيان في البروتوكول الاختياري

تقريبا كل كتاب جافا قرأت محادثات حول استخدام الواجهة كوسيلة لمشاركة الحالة والسلوك بين الكائنات

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

يمكنك افتراض أن الأشخاص سينفذون واجهة لتوفير نوع السلوك الذي يشير إليه اسم أساليبها، ولكن هذا لا يشبه الشيء نفسه.ولا يضع أي قيود على الإطلاق على وقت استدعاء هذه الأساليب (على سبيل المثال، يجب استدعاء Start قبل Stop).

هذا البيان

مطلوب لأنماط نوع GoF، مثل نمط الزائر

غير صحيح أيضًا.يستخدم كتاب GoF واجهات صفرية تمامًا، لأنها لم تكن سمة من سمات اللغات المستخدمة في ذلك الوقت.لا يتطلب أي من الأنماط واجهات، على الرغم من أن البعض يمكنه استخدامها.IMO، نمط المراقب هو النمط الذي يمكن أن تلعب فيه الواجهات دورًا أكثر أناقة (على الرغم من أن النمط يتم تنفيذه عادةً باستخدام الأحداث في الوقت الحاضر).في نمط الزائر، تكون الحالة دائمًا تقريبًا هي أن فئة الزائر الأساسية التي تنفذ السلوك الافتراضي لكل نوع من العقد التي تمت زيارتها مطلوبة، IME.

أنا شخصياً أعتقد أن الإجابة على هذا السؤال تكمن في ثلاثة جوانب:

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

  2. يركز مستخدمو Java بشدة على استخدام أطر العمل، والتي يتطلب الكثير منها (بحق) فئات لتنفيذ واجهاتها

  3. كانت الواجهات هي أفضل طريقة للقيام ببعض الأشياء قبل تقديم الأدوية العامة والتعليقات التوضيحية (السمات في C#).

تعد الواجهات ميزة لغوية مفيدة جدًا، ولكن يتم إساءة استخدامها كثيرًا.تشمل الأعراض ما يلي:

  1. يتم تنفيذ الواجهة بواسطة فئة واحدة فقط

  2. تطبق الفئة واجهات متعددة.غالبًا ما يتم وصفها على أنها ميزة للواجهات، وهذا يعني عادةً أن الفصل المعني ينتهك مبدأ فصل الاهتمامات.

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

كل هذه الأشياء هي روائح كودية، IMO.

إنها إحدى طرق الترويج الفضفاضة اقتران.

مع الاقتران المنخفض، لن يتطلب التغيير في إحدى الوحدات تغييرًا في تنفيذ وحدة أخرى.

والاستخدام الجيد لهذا المفهوم هو نمط المصنع مجردة.في مثال ويكيبيديا، تنتج واجهة GUIFactory واجهة زر.قد يكون مصنع الخرسانة هو WinFactory (ينتج WinButton)، أو OSXFactory (ينتج OSXButton).تخيل أنك تكتب تطبيق واجهة المستخدم الرسومية (GUI) وعليك أن تبحث في جميع مثيلاته OldButton الصف وتغييرها إلى WinButton.ثم في العام المقبل، تحتاج إلى إضافة OSXButton إصدار.

في رأيي، ترى هذا كثيرًا لأنها ممارسة جيدة جدًا يتم تطبيقها غالبًا في المواقف الخاطئة.

هناك العديد من المزايا للواجهات المتعلقة بالفئات المجردة:

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

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

أفترض (مع @eed3s9n) أن الهدف من ذلك هو الترويج للاقتران السائب.أيضًا، بدون الواجهات، يصبح اختبار الوحدة أكثر صعوبة، حيث لا يمكنك محاكاة الكائنات الخاصة بك.

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

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

هناك بعض الإجابات الممتازة هنا، ولكن إذا كنت تبحث عن سبب ملموس، فلا تبحث سوى عن اختبار الوحدة.

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

interface IRepository<T> { T Get(string key); }

class TaxRateRepository : IRepository<TaxRate> {
    protected internal TaxRateRepository() {}
    public TaxRate Get(string key) {
    // retrieve an TaxRate (obj) from database
    return obj; }
}

خلال التعليمات البرمجية، استخدم النوع IRepository بدلاً من TaxRateRepository.

يحتوي المستودع على مُنشئ غير عام لتشجيع المستخدمين (المطورين) على استخدام المصنع لإنشاء مثيل للمستودع:

public static class RepositoryFactory {

    public RepositoryFactory() {
        TaxRateRepository = new TaxRateRepository(); }

    public static IRepository TaxRateRepository { get; protected set; }
    public static void SetTaxRateRepository(IRepository rep) {
        TaxRateRepository = rep; }
}

المصنع هو المكان الوحيد الذي تتم فيه الإشارة إلى فئة TaxRateRepository مباشرةً.

لذلك تحتاج إلى بعض الفئات الداعمة لهذا المثال:

class TaxRate {
    public string Region { get; protected set; }
    decimal Rate { get; protected set; }
}

static class Business {
    static decimal GetRate(string region) { 
        var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
        return taxRate.Rate; }
}

وهناك أيضًا تطبيق آخر لـ IRepository - النموذج:

class MockTaxRateRepository : IRepository<TaxRate> {
    public TaxRate ReturnValue { get; set; }
    public bool GetWasCalled { get; protected set; }
    public string KeyParamValue { get; protected set; }
    public TaxRate Get(string key) {
        GetWasCalled = true;
        KeyParamValue = key;
        return ReturnValue; }
}

نظرًا لأن الكود المباشر (فئة الأعمال) يستخدم مصنعًا للحصول على المستودع، ففي اختبار الوحدة تقوم بتوصيل MockRepository لـ TaxRateRepository.بمجرد إجراء الاستبدال، يمكنك ترميز القيمة المرجعة وجعل قاعدة البيانات غير ضرورية.

class MyUnitTestFixture { 
    var rep = new MockTaxRateRepository();

    [FixtureSetup]
    void ConfigureFixture() {
        RepositoryFactory.SetTaxRateRepository(rep); }

    [Test]
    void Test() {
        var region = "NY.NY.Manhattan";
        var rate = 8.5m;
        rep.ReturnValue = new TaxRate { Rate = rate };

        var r = Business.GetRate(region);
        Assert.IsNotNull(r);
        Assert.IsTrue(rep.GetWasCalled);
        Assert.AreEqual(region, rep.KeyParamValue);
        Assert.AreEqual(r.Rate, rate); }
}

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

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

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

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

في الأساس، على الرغم من ذلك، أستخدم الواجهات بشكل أساسي بسبب قيود الميراث الفردي في Java.إذا كان يجب أن يرث تطبيقي من فئة مجردة ليتم استخدامها عن طريق استدعاء التعليمات البرمجية، فهذا يعني أنني أفقد المرونة اللازمة للوراثة من شيء آخر على الرغم من أن ذلك قد يكون أكثر منطقية (على سبيل المثال.لإعادة استخدام التعليمات البرمجية أو التسلسل الهرمي للكائنات).

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

مشروب باطل عام (القهوة يومًا ما) {

}

لنفترض الآن أنك تريد استخدام نفس الطريقة تمامًا، لكن قم بتمرير كائن hotTea.حسنًا، لا يمكنك ذلك.لقد قمت للتو بترميز هذه الطريقة أعلاه لاستخدام كائنات القهوة فقط.ربما يكون ذلك جيدًا، وربما يكون ذلك سيئًا.الجانب السلبي لما ورد أعلاه هو أنه يقيدك بشكل صارم بنوع واحد من الكائنات عندما ترغب في تمرير جميع أنواع الكائنات ذات الصلة.

باستخدام واجهة، على سبيل المثال IHotDrink،

واجهة IHotDrink { }

وإعادة كتابة الطريقة المذكورة أعلاه لاستخدام الواجهة بدلاً من الكائن،

مشروب باطل عام (ihotdrink somedrink) {

}

يمكنك الآن تمرير كافة الكائنات التي تقوم بتنفيذ واجهة IHotDrink.بالتأكيد، يمكنك كتابة نفس الطريقة التي تفعل الشيء نفسه مع معلمة كائن مختلفة، ولكن لماذا؟أنت فجأة تحتفظ بالكود المتضخم.

الأمر كله يتعلق بالتصميم قبل البرمجة.

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

إذا كنت قد انغمست مباشرة في البرمجة وأدركت في منتصف الطريق أنك تفتقد شيئًا ما، فمن الصعب جدًا إصلاحه.

يمكنك رؤية هذا من منظور Perl/python/Ruby :

  • عندما تقوم بتمرير كائن كمعلمة إلى طريقة، فإنك لا تمرر نوعه، فأنت تعلم فقط أنه يجب أن يستجيب لبعض الأساليب

أعتقد أن اعتبار واجهات جافا بمثابة تشبيه لذلك من شأنه أن يفسر ذلك بشكل أفضل.أنت لا تمرر نوعًا حقًا، بل تمرر شيئًا يستجيب لطريقة (سمة، إذا صح التعبير).

أعتقد أن السبب الرئيسي لاستخدام الواجهات في Java هو تقييد الميراث الفردي.يؤدي هذا في كثير من الحالات إلى تعقيدات غير ضرورية وتكرار التعليمات البرمجية.نلقي نظرة على السمات في سكالا: http://www.scala-lang.org/node/126 السمات هي نوع خاص من الفئات المجردة، لكن يمكن للفصل توسيع العديد منها.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top