24

在这个简单的案例中什么是好的设计:

假设我有一个基类 Car 的方法FillTank(Fuel fuel),其中燃料也是一个基类,它有几个叶类、柴油、乙醇等。

在我的叶子车类DieselCar.FillTank(Fuel fuel)上,只允许使用某种类型的燃料(这并不奇怪:))。现在这是我关心的问题,根据我的界面,每辆车都可以加满任何燃料,但这对我来说似乎是错误的,在每个FillTank()实现中检查输入燃料的类型是否正确,如果没有抛出错误或其他东西。

我怎样才能将这种情况重新设计为更准确的情况,甚至可能吗?如何设计一个基类作为输入而不得到这些“奇怪结果”的基本方法?

4

9 回答 9

28

使用通用基类(如果您的语言支持(以下是 C#)):

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

基本上,这强制任何从 car 继承的类来指定它使用的燃料类型。此外,Car该类施加了一个限制,该限制TFuel必须是抽象Fuel类的某个子类型。

假设我们有一些Diesel简单的类:

public class Diesel : Fuel
{
    ...
}

还有一辆只使用柴油的汽车:

public DieselCar : Car<Diesel>
{
     public override void FillTank(Diesel fuel)
     {
          //perform diesel fuel logic here.
     }
}
于 2010-02-18T14:05:02.607 回答
12

单独的面向对象编程不能很好地处理这个问题。您需要的是通用编程(此处显示的 C++ 解决方案):

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

那么您的柴油车只是一辆特定的汽车,Car<Diesel>.

于 2010-02-18T14:03:06.620 回答
11

如果汽车类型和燃料类型之间存在严格的界限,那么FillTank()Car类就没有任何意义,因为知道您拥有汽车并不能告诉您使用哪种燃料。因此,为了确保编译时的正确性, FillTank()应该在子类中定义,并且应该只采用Fuel有效的子类。

但是,如果您有不想在子类之间重复的公共代码怎么办?然后为子类的函数调用的基类编写一个受保护的方法。 FillingTank()同样的事情Fuel

但是,如果您有一辆使用多种燃料(例如柴油或汽油)运行的神奇汽车怎么办?然后那辆车成为两者的子类,DieselCar并且GasCar您需要确保将Car其声明为虚拟超类,这样您就不会在一个对象中有两个Car实例。DualFuelCar填充油箱应该只需很少或无需修改即可工作:默认情况下,您将同时获得DualFuelCar.FillTank(GasFuel)and DualFuelCar.FillTank(DieselFuel),为您提供按类型重载的功能。

但是,如果您不希望子类具有FillTank()功能怎么办?然后您需要切换到运行时检查并执行您认为必须做的事情:进行子类检查Fuel.type,如果不匹配,则抛出异常或返回错误代码(首选后者)。在 C++ 中,RTTIdynamic_cast<>是我推荐的。在 Python 中,isinstance().

于 2010-02-18T14:19:21.697 回答
1

为此可以使用双重调度:在填充之前接受一些燃料请注意,在不直接支持它的语言中,您会引入依赖项

于 2010-02-18T14:05:57.077 回答
1

听起来您只是想限制进入柴油车的燃料类型。就像是:

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 */);

本质上,您在这里所做的是允许 aCar具有特定的燃料类型。它还允许您创建支持任何类型的汽车Fuel(机会将是一件好事!)。但是,对于您的 DieselCar,您只需从 car 派生一个类并将其限制为Diesel仅使用燃料。

于 2010-02-18T14:14:55.047 回答
0

使用is运算符检查接受的类,您可以在构造函数中抛出异常

于 2010-02-18T14:00:17.710 回答
0

我认为接受的方法是在你的基类中有一个方法,如果“叶子”汽车不覆盖它,它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
    }
}
于 2010-02-18T14:01:45.947 回答
0

在类似 CLOS 的系统中,您可以执行以下操作:

(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"

正如stefaanv所提到的,这实际上是 Common Lisp 的双重调度版本。

于 2010-02-18T14:26:27.153 回答
0

你可以扩展你原来的 Car 界面

interface Car {
    drive();
}

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

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

}

于 2010-02-18T19:34:08.890 回答