سؤال

ما هو التصميم الجيد في هذه الحالة البسيطة:

لنفترض أن لدي سيارة فئة أساسية مع طريقة FillTank(Fuel fuel) حيث يكون الوقود أيضًا فئة أساسية تحتوي على العديد من فصول الأوراق ، الديزل ، الإيثانول ، إلخ.

على صف سيارتي الورقية DieselCar.FillTank(Fuel fuel) لا يُسمح سوى بنوع معين من الوقود (لا مفاجآت هناك :)). الآن هنا هو قلقي ، وفقًا لواجهتي ، يمكن أن يتم تزيين كل سيارة بأي وقود ، لكن هذا يبدو خطأ بالنسبة لي ، في كل شيء FillTank() تحقق من تنفيذ وقود الإدخال للنوع الصحيح وإذا لم يكن رمي خطأ أو شيء من هذا القبيل.

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

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

المحلول

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

ولكن ماذا لو كان لديك رمز مشترك لا تريد أن تكرره بين الفئات الفرعية؟ ثم تكتب محمي FillingTank() طريقة للفئة الأساسية التي تستدعيها وظيفة الفئة الفرعية. نفس الشيء ينطبق Fuel.

ولكن ماذا لو كان لديك بعض السيارات السحرية التي تعمل على العديد من الوقود ، يقول الديزل أو الغاز؟ ثم تصبح تلك السيارة فئة فرعية لكليهما DieselCar و GasCar وتحتاج إلى التأكد من ذلك Car يُعلن أنه من الفئة الافتراضية حتى لا يكون لديك اثنان Car حالات في أ DualFuelCar هدف. يجب أن يعمل ملء الخزان مع تعديل ضئيل أو معدوم: بشكل افتراضي ، ستحصل على كليهما DualFuelCar.FillTank(GasFuel) و DualFuelCar.FillTank(DieselFuel), ، مما يمنحك وظيفة محملة بشكل زائد.

ولكن ماذا لو كنت لا تريد أن يكون للفئة الفرعية FillTank() وظيفة؟ ثم تحتاج إلى التبديل إلى مدة العرض التحقق وافعل ما كنت تعتقد أنه عليك: إجراء فحص الفئة الفرعية Fuel.type وإما رمي استثناء أو إرجاع رمز الخطأ (تفضل الأخير) إذا كان هناك عدم تطابق. في C ++ ، RTTI و dynamic_cast<> هي ما أوصي به. في بيثون ، isinstance().

نصائح أخرى

استخدم فئة قاعدة عامة (إذا كانت لغتك تدعمها (أدناه هي C#)):

public abstract class Car<TFuel>  where TFuel : Fuel
{
    public abstract void FillTank(TFuel fuel);
}

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

لنقول أن لدينا بعض الفصل Diesel وهو بسيط:

public class Diesel : Fuel
{
    ...
}

وسيارة تعمل فقط على الديزل:

public DieselCar : Car<Diesel>
{
     public override void FillTank(Diesel fuel)
     {
          //perform diesel fuel logic here.
     }
}

لا يمكن للبرمجة الموجهة نحو الكائن وحدها التعامل مع هذه المشكلة بشكل جيد. ما تحتاجه هو البرمجة العامة (حل C ++ الموضح هنا):

template <class FuelType>
class Car
{
public:
  void FillTank(FuelType fuel);
};

سيارتك الديزل هي مجرد سيارة معينة ، Car<Diesel>.

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

يبدو أنك تريد فقط تقييد نوع الوقود الذي يدخل سيارة الديزل الخاصة بك. شيء مثل:

public class Fuel
{
    public Fuel()
    {
    }
}

public class Diesel: Fuel
{
}

public class Car<T> where T: Fuel
{
    public Car()
    {
    }

    public void FillTank(T fuel)
    {
    }
}

public class DieselCar: Car<Diesel>
{
}

سيفعل الخدعة على سبيل المثال

var car = new DieselCar();
car.FillTank(/* would expect Diesel fuel only */);

في الأساس ما تفعله هنا هو السماح Car للحصول على أنواع وقود محددة. كما يتيح لك إنشاء سيارة تدعم أي نوع من Fuel (ستكون الفرصة شيئًا جيدًا!). ومع ذلك ، في حالتك ، الديزل ، يمكنك فقط استخلاص فصل من السيارة وتقييدها على استخدامها Diesel الوقود فقط.

استخدم ال is المشغل للتحقق من الفئات المقبولة ، ويمكنك رمي استثناء في المُنشئ

أعتقد أن الطريقة المقبولة هي الحصول على ملف ValidFuel(Fuel f) الطريقة في الفصل الأساسي الذي يلقي نوعًا ما NotImplementedException (لغات مختلفة لها مصطلحات مختلفة) إذا لم تتجاوزها السيارات "الورقة".

FillTank يمكن أن يكون بعد ذلك بالكامل في الفصل الأساسي والاتصال ValidFuel لمعرفة ما إذا كان صالحًا.

public class BaseCar {
    public bool ValidFuel(Fuel f) {
        throw new Exception("IMPLEMENT THIS FUNCTION!!!");
    }

    public void FillTank(Fuel fuel) {
        if (!this.ValidFuel(fuel))
             throw new Exception("Fuel type is not valid for this car.");
        // do what you'd do to fill the car
    }
}

public class DieselCar:BaseCar {
    public bool ValidFuel(Fuel f) {
        return f is DeiselFuel
    }
}

في نظام يشبه الإغلاق ، يمكنك أن تفعل شيئًا كهذا:

(defclass vehicle () ())
(defclass fuel () ())
(defgeneric fill-tank (vehicle fuel))
(defmethod fill-tank ((v vehicle) (f fuel)) (format nil "Dude, you can't put that kind of fuel in this car"))

(defclass diesel-truck (vehicle) ())
(defclass normal-truck (vehicle) ())
(defclass diesel (fuel) ())
(defmethod fill-tank ((v diesel-truck) (f diesel)) (format nil "Glug glug"))

إعطائك هذا السلوك:

CL> (fill-tank (make-instance 'normal-truck) (make-instance 'diesel))
"Dude, you can't put that kind of fuel in this car"
CL> (fill-tank (make-instance 'diesel-truck) (make-instance 'diesel))
"Glug glug"

التي ، في الحقيقة ، هي نسخة Lisp الشائعة من Double Dispatch ، كما ذكرها ستيفانف.

يمكنك تمديد واجهة سيارتك الأصلية

interface Car {
    drive();
}

interface DieselCar extends Car {
    fillTank(Diesel fuel);
}

interface SolarCar extends Car {
    chargeBattery(Sun fuel);
}

}

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