如何设计具有基类中不可用功能的子类?

时间:2009-04-16 22:48:38

标签: java oop

例如,假设我有一个类Vehicle,我希望有一个子类ConvertibleVehicle,它有额外的方法,如foldRoof(),turboMode(),foldFrontSeats()等。我希望实例化如下

Vehicle convertible = new ConvertibleVehicle()

所以我仍然可以访问常见的方法,如openDoor(),startEngine()等。我如何设计这样的解决方案?

为了澄清我的两个初步解决方案,我不满意的是:

  1. 使用虚拟方法foldRoof(),turboMode(),foldFrontSeats(),我只在ConvertibleVehicle中重写,让它们在其他子类中不执行任何操作
  2. 使用抽象方法foldRoof(),turboMode(),foldFrontSeats()并强制每个子类提供一个实现,即使它在除ConvertibleVehicle以外的所有实例中都是空白的
  3. 上面看起来有点复杂,因为它们都污染了基类,因为我添加了越来越多的子类,每个子类都有自己独特的功能

    在阅读了一些回复之后,我的设计中可能存在某种类型的根本缺陷。假设我有一个类VehicleFleet,它接收车辆并指示他们按如下方式驾驶:

    public VehicleFleet(Vehicle[] myVehicles) {
    
        for (int i=0; i < myVehicles.length; i++) {
            myVehicles[i].drive();
        }
    }
    

    假设这适用于车辆的几十个子类,但对于ConvertibleVehicle我也想在开车前折叠车顶。为此,我将VehicleFleet子类化如下:

    public ConvertibleVehicleFleet(Vehicle[] myVehicles) {
    
        for (int i=0; i < myVehicles.length; i++) {
            myVehicles[i].foldRoof();
            myVehicles[i].drive();
        }
    }
    

    这给我留下了一个凌乱的函数foldRoof()卡在基类中,它实际上不属于哪个,只有在ConvertibleVehicle的情况下才会被覆盖,并且在所有其他情况下都不会执行任何操作。解决方案有效,但似乎非常不优雅。这个问题是否适合更好的架构呢?

    我正在使用Java虽然我希望找到一个可以在任何面向对象语言中工作的通用解决方案,并且我不需要依赖语言特定的怪癖

8 个答案:

答案 0 :(得分:5)

使用Vehicle的任何对象都不应该知道ConvertibleVehicle及其专用方法。在适当的松散耦合的面向对象设计中,Driver只知道Vehicle接口。驱动程序可能会在Vehicle上调用startEngine(),但是要由Vehicle的子类覆盖startEngine()以处理不同的实现,例如转动键和按下按钮。

考虑查看以下两个链接,这些链接应该有助于解释这个概念: http://en.wikipedia.org/wiki/Liskov_substitution_principle http://en.wikipedia.org/wiki/Open/closed_principle

考虑发布一个现实世界的问题,您认为这会导致您在此处描述的两难境地,并且有人会非常乐意展示更好的方法。

答案 1 :(得分:3)

这是一个很好的问题。它意味着你有(或期望有)代码,要求车辆(例如)foldRoof()。这是一个问题,因为大多数车辆不应折叠他们的屋顶。只有知道它处理ConvertibleVehicle的代码才应该调用该方法,这意味着它只是一个应该只在ConvertibleVehicle类中的方法。这样做会更好;一旦你尝试调用Vehicle.foldRoof(),你的编辑就会告诉你它无法完成。这意味着您需要安排代码以便知道您正在处理ConvertibleVehicle,或取消foldRoof()调用。

答案 2 :(得分:3)

我在类似的情况下做到了这一点。

选项A)

如果专门操作与基本操作属于同一序列(例如,ConvertibleVehicle在驱动之前需要为foldRoof),那么只需将专门操作放在基本操作中。

class Vehicle { 
     public abstract void drive();
}

class ConvertibleVehicle { 
     public void drive() { 
         this.foldRoof();
         .... // drive 
     }
     private void foldRoof() { 
         ....
     }
 }

所以驾驶车队的效果将是他们中的一些人在被驾驶之前会折叠他们的车顶。

 for( Vehicle v : vehicleFleet ) { 
      v.drive();
 }

专用方法不在对象公共接口中公开,但在需要时调用。

选项B)

如果专门操作不是同一序列的一部分并且必须在某些“特殊”情况下调用,那么让特定版本的客户端调用那些专门的操作。警告,这不是那么纯粹,也不是低耦合,但是当两个对象(客户端和服务)由相同的“条件”或构建器创建时,大多数时候都可以。

class Vehicle { 
    public void drive() { 
        ....
    }
}
class ConvertibleVehicle extends Vehicle { 
         // specialized version may override base operation or may not.
        public void drive() { 
          ... 
         }

         public void foldRoof() { // specialized operation 
             ...
         }
 }

与上一个示例几乎相同,只有在这种情况下 foldRoof 也是公开的。

不同之处在于我需要一个专门的客户:

// Client ( base handler ) 
public class FleetHandler { 
     public void handle( Vehicle [] fleet ) { 
           for( Vehicle v : fleet ) {  
               v.drive();
            }
     }
}

// Specialized client ( sophisticate handler that is )  
 public class RoofAwareFleetHandler extends FleetHandler { 
      public void handle( Vehicle [] fleet ) { 
           for( Vehicle v : fleet ) { 
              // there are two options.
              // either all vehicles are ConvertibleVehicles (risky) then
              ((ConvertibleVehicles)v).foldRoof();
              v.drive();

              // Or.. only some of them are ( safer ) .
              if( v instenceOf ConvertibleVehicle ) { 
                  ((ConvertibleVehicles)v).foldRoof();
              } 
              v.drive();
            }
       }
  }

instaceof 在那里看起来很丑陋,但它可能会被现代的vm所概括。

这里的要点是只有专门的客户才知道并且可以调用专门的方法。也就是说,只有 RoofAwareFleetHandler 可以在** ConvertibleVehicle **上调用 foldRoof()

最终代码不会改变......

 public class Main { 
     public static void main( String [] args ) { 
         FleetHandler fleetHandler = .....
         Vehicles [] fleet =  ....

          fleetHandler.handle( fleet );
      }
 }

当然,我总是确保fleethandler和车辆阵列兼容(可能使用abstrac工厂或建筑商)

我希望这会有所帮助。

答案 3 :(得分:1)

这正是子类化的作用:添加基类中不存在的功能。

class MyVehicle : public Vehicle {

public: 
  void MyNewFunction()
...

有两种(实际上只是)不同的继承风格:公共和私有,分别反映了Is-A和Has-A关系。使用公共继承,您可以直接向类添加内容。如果我有使用方法Eat()和Walk()的类Animal,我可以创建一个名为Cat的子类,它具有Purr()方法。然后,猫有公共方法Eat,Walk和Purr。

在基于LinkedList的Stack的情况下,我可以在内部说Stack HAS-A LinkedList。因此,我不公开公开基类的任何功能,我将它们保留为私有,并且必须明确地提供我选择的任何公共。列表可能有一个方法Insert(),但对于Stack,我限制了实现并将其重新设置为Push()。没有公开以前的公开方法。

在C ++中,这是由基类之前给出的访问修饰符定义的。上面,我正在使用公共继承。在这里,我使用私有继承:

class MyVehicle : private Engine {

这反映了MyVehicle HAS-An Engine。

最终,子类化将基类中的所有内容都包含在内,并为其添加新内容。

修改 有了这些新信息,似乎你真正在寻找的是接口,正如之前(已投票的)评论所述。这是继承的一个大问题 - 粒度。 C ++的一个主要抱怨是它实现了多重继承(这是实现这一目标的一种选择。)你能具体说明你使用的语言,以便我们能够正确地提出建议吗?

答案 4 :(得分:1)

我认为大多数人都忽略了Delta的问题。在我看来,他/她并没有询问继承是什么。他/她正在询问子类实现的功能,这些功能并不适合基类,并且可能导致后续的混乱。即在层次结构链中推送特定的方法/功能,或者要求子类实现不太自然的功能合同。

还有一个问题是,在每种情况下能否像处理子类一样处理基类是有价值的(避免转换并交替使用它们)。 *编辑 - 这称为Liskov替换原则(感谢提醒我,Kyle)。

答案 5 :(得分:1)

再加上Kyle W. Cartmell的优秀答案,或许可以简化Oscar Reyes的答案......

您可能希望考虑让基类定义一个名为prepareToDrive()的方法,其中继承的类可以在启动之前放置任何需要完成的设置任务。从用户的角度来看,调用drive()将是启动一切的方法,因此我们需要将驱动器重构为“设置”阶段和“开始”阶段。

public class Vehicle {
    protected void prepareToDrive() {
        // no-op in the base class
    }

    protected abstract void go();

    public final void drive() {
        prepareToDrive();
        go();
    }
}

现在,子类必须实现受保护的方法go()(非常糟糕的方法名称,但是你得到了这个想法),这是他们对类驱动进行特定类处理的地方。

现在,您继承的类可能如下所示:

public class ConvertableVehicle extends Vehicle {

    // override setup method
    protected void prepareToDrive() {
        foldRoof();
    }

    protected void go() {
        // however this works
    }

    protected void foldRoof() {
        // ... whatever ...
    }
}

当您遇到TractorTrailerRig类时,此结构也会有所帮助,该类需要确保拖车在可以驱动之前已加载并正确连接。

答案 6 :(得分:0)

Vehicle的用户如何知道它的ConvertibleVehicle?要么他们需要动态强制转换以确保它是正确的,要么你已经在Vehicle中提供了一个方法来获得对象的真实类型。

在第一种情况下,用户已经将ConvertibleVehicle作为动态强制转换的一部分。他们可以使用新的指针/引用来访问ConvertiblVehicle的方法

在第二种情况下,用户使用Vehicle方法之一验证对象类型,他们可以将Vehicle转换为ConvertibleVehicle并使用它。

一般来说,施法是一个坏主意。尝试使用基类指针执行所有操作。你的汽车示例效果不佳,因为方法太低,构建更高级别的虚拟函数。

所有这一切。我需要从基类中获取所有派生类方法。我可以转换为派生类,但它涉及到一个框架,需要更多的努力。古老的格言“所有问题都可以用一层间接来解决”是我如何解决这个问题。我在基类中调用了一个虚拟方法,其中包含我想要调用的函数的“名称”。 'Name'可以是字符串或整数,具体取决于您的需要。它的速度较慢,但​​如果你的类层次结构足够表达,你只需要很少这样做。

答案 7 :(得分:0)

拥有ConvertibleVehicle子类Vehicle并在您描述时添加自己的方法是完全没问题的。那部分设计还可以。你遇到的麻烦就是舰队。 ConvertibleFleet {em>不应该是VehicleFleet的子类。一个例子将告诉你原因。我们假设VehicleFleet是这样的:

public class VehicleFleet {
    // other stuff...

    public void add(Vehicle vehicle) {
        // adds to collection...
    }
}

这非常精细且合理,您可以将Vehicle或其子类添加到VehicleFleet。现在,让我们说我们还有另一种车辆:

public class TruckVehicle extends Vehicle {
    // truck-specific methods...
}

我们也可以将其添加到VehicleFleet,因为它是一种载体。问题是:如果ConvertibleFleetVehicleFleet的子类,这意味着我们还可以将卡车添加到ConvertibleFleet。那是错的。 ConvertibleFleet 是一个合适的子类,因为对其父级(添加卡车)有效的操作对孩子无效。

典型的解决方案是使用类型参数:

public class VehicleFleet<T extends Vehicle> {
    void add(T vehicle) {
        // add to collection...
    }
}

这将允许您定义特定车辆类型的车队。请注意,这也意味着没有“基础”VehicleFleet类可以传递给不关心机队具有哪种车辆的功能。这可以使用另一层基类(或接口)来补救:

public interface VehicleFleetBase {
    Vehicle find(String name);
    // note that 'add' methods or methods that pass in vehicles to the fleet
    // should *not* be in here
}

public class VehicleFleet<T extends Vehicle> {
    void add(T vehicle) {
        // add to collection...
    }

    Vehicle find(String name) {
        // look up vehicle...
    }
}

对于将车辆拉出车队并且不关心它们是什么类型的方法,你可以绕过VehicleFleetBase。需要插入车辆的方法使用安全强类型的VehicleFleet<T>