面向对象设计问题

24

在这个简单的情况下,什么是好的设计:

假设我有一个基类Car,其中有一个方法FillTank(Fuel fuel), 其中fuel也是一个基类,有几个子类,例如柴油、乙醇等。

在我的叶子车类DieselCar.FillTank(Fuel fuel)中只允许使用某种类型的燃料(没有惊喜:))。 现在我的问题是,根据我的接口,每辆汽车都可以用任何燃料加油,但我觉得这是错误的,在每个FillTank()实现中检查输入的燃料类型是否正确,如果不正确则抛出错误或其他异常。

如何重新设计这样的情况以更准确地解决这个问题,这是可能的吗? 如何设计一个采用基类作为输入的基本方法而不会得到这些“奇怪的结果”?

9个回答

28

使用通用的基类(如果您的语言支持的话(下面是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.
     }
}

为什么要使用继承而不是组合? - Paco
+1 我也这么做了 :) -- 关于“为什么要使用继承”:因为柴油“是一种”燃料,而柴油车“是一辆”汽车。继承并不邪恶,我不知道为什么人们倾向于这样想。看看Java和.Net;两者都在很多地方使用继承。我猜你可以避免让柴油车成为汽车,而是实现IFuelable<T>,但这实际上取决于此示例中汽车的属性。 - cwap
@cwap - 继承并不是邪恶的,只是存在一定的风险。但这可能不是可以在评论中解决的问题。 :) 如果您想知道为什么人们推荐组合而不是继承,那么有许多关于此的SO问题,例如https://dev59.com/inRC5IYBdhLWcg3wP-n5,以及许多关于.net的好文章。例如,http://blogs.msdn.com/steverowe/archive/2008/04/28/prefer-composition-over-inheritance.aspx,http://www.artima.com/lejava/articles/designprinciples4.html。 - Jeff Sternal
@Jeff Sternal 我同意这个讨论可能不适合在评论中进行,但我觉得我必须指出抽象类比接口具有明显的优势,因为抽象基类可以通过添加新功能(可重写方法)来扩展,而不会破坏现有的子类。这对于接口来说并非如此。 - Klaus Byskov Pedersen
@klausbyskov - 当然可以,我并不是打算阻止你回答Paco!毕竟,他的评论与您的建议有关 - 如果通用类型是一种选择,则我认为这是最好的答案。 - Jeff Sternal
这是许多问题的好解决方案,但需要注意的是,这也意味着不再有可以多态使用的“汽车”基类。 - munificent

12

仅靠面向对象编程不能很好地解决这个问题。你需要的是泛型编程(C++ 解决方案如下):

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

你的柴油车只是一种特定的汽车,Car<Diesel>


11
如果车辆类型和燃料类型之间存在硬性边界,那么 FillTank() 就没有在基础的 Car 类中的业务,因为只知道你有一车并不能告诉你它使用什么燃料。因此,为了确保在 编译时的正确性,FillTank() 应该在子类中定义,并且只应该使用适用的 Fuel 的子类。
但是,如果您有不想在子类之间重复的公共代码呢? 那么就在基类中编写一个protectedFillingTank() 方法,让子类的方法调用它。对于Fuel也是一样。
但是,如果你有一辆可以使用多种燃料(例如柴油或汽油)的神奇汽车呢? 然后,这辆车就会成为 DieselCarGasCar 的两个子类,并且需要确保将 Car 声明为虚拟超类,以便在 DualFuelCar 对象中不会有两个Car 实例。加油应该可以直接奏效,几乎不需要修改:默认情况下,您将获得两个按类型重载的函数 DualFuelCar.FillTank(GasFuel)DualFuelCar.FillTank(DieselFuel)
但是,如果您不想在子类中有一个FillTank() 函数呢? 那么您需要切换到运行时检查,并执行您认为必须执行的操作:使子类检查 Fuel.type,如果存在不匹配,则抛出异常或返回错误代码(后者更好)。在C++中,我建议使用RTTI和dynamic_cast<>。在Python中,使用 isinstance()

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

本质上,您在此处所做的是允许Car具有特定的燃料类型。它还允许您创建一辆支持任何类型Fuel的汽车(这将是一件好事!)。但是,在您的情况下,DieselCar,您只需从汽车派生一个类并限制其仅使用Diesel燃料。


1

可以使用双重分派来实现:在加油之前接受一些燃料。请注意,在不直接支持它的语言中,您需要引入依赖关系。


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
    }
}

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"

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


0

您可以扩展您的原始Car接口

interface Car {
    drive();
}

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

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

}


那么问题在于,如果我有一辆汽车列表,我不能在不知道是哪辆汽车的情况下给所有汽车加满油箱。我认为我遇到的问题归结为这个问题。在这种情况下,您不应该在基类上公开填充油箱方法,因为这没有意义。 - Marcus

0

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接