【问题标题】:OOP design problemOOP设计问题
【发布时间】:2011-01-18 08:15:58
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: oop car-analogy


    【解决方案1】:

    使用泛型基类(如果您的语言支持(以下为 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.
         }
    }
    

    【讨论】:

    • 为什么是继承而不是组合?
    • +1 我也做了什么 :) -- 对于“为什么继承”:因为 Diesel“是-a”燃料,而 DieselCar “是-a”汽车。继承不是邪恶的,我不知道为什么人们倾向于这样想。看看java和.Net;两者都在各处使用继承。我想您可以避免将 DieselCar 变成汽车,而是实现 IFuelable,但这实际上取决于此示例中 Car 的属性。
    • @cwap - 继承并不邪恶,它只是带有一定的风险。但这可能不是可以在评论中解决的问题。 :) 如果你想知道为什么人们推荐组合而不是继承,那么有很多关于这个的问题,比如stackoverflow.com/questions/361371/…,以及 .net 上的很多好文章。例如,blogs.msdn.com/steverowe/archive/2008/04/28/…、artima.com/lejava/articles/designprinciples4.html。
    • @Jeff Sternal 我同意这个讨论可能不属于 cmets,但我觉得我必须指出抽象类比接口有明显的好处,因为抽象基类可以扩展在不破坏现有子类的情况下具有新功能(可覆盖的方法)。接口不是这种情况。
    • @klausbyskov - 绝对的,我并不想阻止你回答 Paco! (毕竟,他的评论与您的​​提议有关——如果可以选择泛型,我认为这是最好的答案。)
    【解决方案2】:

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

    interface Car {
        drive();
    }
    
    interface DieselCar extends Car {
        fillTank(Diesel fuel);
    }
    
    interface SolarCar extends Car {
        chargeBattery(Sun fuel);
    }
    

    }

    【讨论】:

    • 那么问题是,如果我有一个汽车清单,我无法在不知道是哪辆车的情况下为所有汽车加满油箱。我认为我遇到的问题可以归结为这个问题。在这种情况下,您不应在基类上公开填充罐方法,因为它没有意义。
    【解决方案3】:

    在类似 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 的双重调度版本。

    【讨论】:

      【解决方案4】:

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

      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,您只需从 car 派生一个类并将其限制为仅使用 Dieselfuel。

      【讨论】:

        【解决方案5】:

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

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

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

        但是如果您不希望子类具有FillTank() 函数怎么办?然后你需要切换到运行时检查并做你认为你必须做的事情:让子类检查Fuel.type,如果有则抛出异常或返回错误代码(首选后者)不匹配。在 C++ 中,我会推荐 RTTI 和 dynamic_cast&lt;&gt;。在 Python 中,isinstance()。

        【讨论】:

          【解决方案6】:

          我认为可接受的方法是在您的基类中使用 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
              }
          }
          

          【讨论】:

            【解决方案7】:

            double dispatch 可用于此:在加油前接受一些燃料。请注意,在不直接支持它的语言中,您会引入依赖项

            【讨论】:

              【解决方案8】:

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

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

              那么您的柴油车就是一辆特定的汽车,Car&lt;Diesel&gt;。

              【讨论】:

                【解决方案9】:

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

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2010-11-27
                  • 1970-01-01
                  • 2010-09-18
                  • 2013-11-02
                  • 1970-01-01
                  • 2011-03-07
                  相关资源
                  最近更新 更多