【问题标题】:General OO question about inheritance and extensibility关于继承和可扩展性的一般 OO 问题
【发布时间】:2011-03-15 20:03:00
【问题描述】:

因此,在单父继承模型中,使代码可扩展以适应未来更改同时保持相同界面的最佳解决方案是什么(我想强调一个事实,即这些更改不能在最初的实施时就知道了,我的问题的主要焦点是探索支持这些变化的最佳机制/模式当它们出现时)?我知道这是一个非常基本的 OO 问题,下面我将举例说明我是如何解决这个问题的,但我想知道是否有更好的解决方案来解决这个常见问题。

这是我一直在做的事情(示例代码是在 Java 中):

一开始,创建了以下两个类和接口:

public class Foo
{
    protected int z;
}

public interface FooHandler
{
    void handleFoo(Foo foo);
}

public class DefaultFooHandler implements FooHandler
{
    @Override
    public void handleFoo(Foo foo)
    {
        //do something here
    }
}

系统仅使用 FooHandler 类型的变量/字段,并且该对象(在本例中为 DefaultFooHandler)创建在几个明确定义的位置(可能有 FooHandlerFactory),以补偿可能发生的任何变化未来。

然后,在未来的某个时候需要扩展 Foo 以添加一些功能。因此,创建了两个新类:

public class ImprovedFoo extends Foo
{
    protected double k;
}

public class ImprovedFooHandler extends DefaultFooHandler
{
    @Override
    public void handleFoo(Foo foo)
    {
        if(foo instanceof ImprovedFoo)
        {
            handleImprovedFoo((ImprovedFoo)foo);
            return;
        }
        if(foo instanceof Foo)
        {
            super.handleFoo(foo);
            return;
        }
    }

    public void handleImprovedFoo(ImprovedFoo foo)
    {
        //do something involving ImprovedFoo
    }
}

在上面的例子中让我畏缩的是if-statements出现在ImprovedFooHandler.handleFoo

有没有办法避免使用if-statementsinstanceof 运算符?

【问题讨论】:

  • 您在寻找访客模式吗? en.wikipedia.org/wiki/Visitor_pattern
  • @Erik,你应该把它作为答案发布)
  • @Stas:那么我必须总结一下模式 - 其他人会这样做:)
  • @Manos ImprovedFoo 扩展 Foo.
  • 第一个问题应该是为什么你不想给 Foo 增加这个功能。您在那里的推理将确定解决问题的正确方法。

标签: java oop design-patterns inheritance extensibility


【解决方案1】:

首先,您编写的代码不起作用。 每次看到instanceofif...else 在一起时要非常小心。这些检查的顺序非常重要。在你的情况下,你永远不会执行handleImpovedFoo。猜猜为什么:)

你有这些instanceof 声明是绝对正常的。有时这是为子类型提供不同行为的唯一方法。 但是在这里你可以使用另一个技巧:使用简单的Map。将 foo-hierarchy 的类映射到 fooHandler-hierarchy 的实例。

Map<Class<? extends Foo>, FooHandler> map ...

map.put( Foo.class, new FooHandler() );
map.put( ImprovedFoo.class, new ImprovedFooHandler() );

Foo foo ...; // here comes an unknown foo 

map.get( foo.getClass() ).handleFoo( foo );

【讨论】:

  • 正如我在其他 cmets 中提到的,我认为这种方法是最好的。尤其是与策略模式相结合时。虽然在这种情况下,传递给适当 FooHandler 的“foo”必须向下转换为该特定 FooHandler 处理的适当类型。你觉得这个声音怎么样?
  • 对于这种沮丧,你无能为力。所以不要担心并使用它。
  • 我希望你一般不怕沮丧^)
  • 同意。我不会说我害怕沮丧,但每次我看到沮丧时,我的直觉都是有问题。有一种方法可以通过使用泛型绕过向下转型,并让每种新类型的 FooHandler 指定它正在处理的 Foo 类型:FooHandler、DefaultFooHandler、ImprovedFooHandler
  • 对于寻找类似问题答案的任何人,也请考虑 Winston Ewert 的解决方案作为替代方案
【解决方案2】:

处理此问题的最佳方法过多地取决于个别情况,无法提供通用解决方案。因此,我将提供一些示例以及如何解决它们。

案例 1:虚拟文件系统

您的代码的客户端实现了虚拟文件系统,使它们能够操作任何类型的资源,这些资源可以看起来像一个文件。他们通过实现以下接口来做到这一点。

interface IFolder
{
     IFolder subFolder(String Name);
     void delete(String filename);
     void removeFolder(); // must be empty
     IFile openFile(String Name);
     List<String> getFiles();
}

在您的软件的下一个版本中,您希望添加删除目录及其所有内容的功能。称之为removeTree。您不能简单地将 removeTree 添加到 IFolder,因为这会破坏 IFolder 的所有用户。而是:

interface IFolder2 implements IFolder
{
     void removeTree();    
}

每当客户端注册一个 IFolder(而不是 IFolder2)时,就注册

new IFolder2Adapter(folder)

而是在整个应用程序中使用 IFolder2。您的大部分代码不应该关心旧版本的 IFolder 支持的差异。

案例 2:更好的字符串

你有一个支持各种功能的字符串类。

class String
{
     String substring(int start, end);
}

您决定在新版本中添加字符串搜索并因此实现:

class SearchableString extends String
{
    int find(String);
}

这太傻了,SearchableString 应该合并到 String 中。

案例 3:形状

你有一个形状模拟,它可以让你得到形状的区域。

class Shape
{
    double Area();
    static List<Shape> allShapes; // forgive evil staticness
}

现在你介绍一种新的形状:

class DrawableShape extends Shape
{
    void Draw(Painter paint);
}

我们可以为 Shape 添加一个默认的空 Draw 方法。但是让 Shape 具有 Draw 方法似乎是不正确的,因为通常不打算绘制形状。绘图确实需要一个 DrawableShapes 列表,而不是提供的 Shapes 列表。实际上,可能 DrawableShape 根本不应该是 Shape。

案例 4:零件

假设我们有一辆汽车:

class Car
{
    Motor getMotor();
    Wheels getWheels();
}

void maintain(Car car)
{
    car.getMotor().changeOil();
    car.getWheels().rotate();
}

当然,你知道在未来的某个地方,总会有人制造出更好的汽车。

class BetterCar extends Car
{
    Highbeams getHighBeams();
}

这里我们可以利用访问者模式。

void maintain(Car car)
{
     car.visit( new Maintainer() );
}

汽车将其所有组件部分传递给 ICarVisitor 接口的调用,从而允许维护者类维护每个组件。

案例 5:游戏对象 我们有一个游戏,可以在屏幕上看到各种对象

class GameObject
{
   void Draw(Painter painter);
   void Destroy();
   void Move(Point point);
}

我们的一些游戏对象需要定期执行逻辑的能力,因此我们创建了:

class LogicGameObject extends GameObject
{
    void Logic();
}

我们如何在所有 LogicGameObjects 上调用 Logic()?在这种情况下,向 GameObject 添加一个空的 Logic() 方法似乎是最好的选择。它完全符合 GameObject 的工作描述,即使它什么也没有,也期望它能够知道要为逻辑更新做什么。

结论

处理这种情况的最佳方法取决于个人情况。这就是为什么我提出了为什么你不想将功能添加到 Foo 的问题。扩展 Foo 的最佳方式取决于您到底在做什么。您看到的 instanceof/if 出现是您没有以最佳方式扩展对象的症状。

【讨论】:

  • 1) 假设有一个现有的代码库并添加了 ImprovementFoo 对象以支持新功能合并并不是一个真正的选择。 2)我不确定为什么你认为适应 Foo 而不是扩展它更好,你能详细说明一下吗? 3)查看我对DarenW的回答的评论。
  • @Andrey,在第二种情况下,ImprovedFoo 仍然存在。这里的假设是 Foo 的旧版本是由外部来源提供的。适配器将 Foo 对象转换为 ImprovementFoo 对象。只有在其他人正在实现您的接口的情况下才有意义。
  • 你为什么要把Foo变成ImprovedFoo?这个想法是保持 Foo 不变,同时添加对 ImprovementFoo 的支持。
  • @Andrey,我已经重写了我的答案,以便更好地解释何时有意义。在某些情况下,您不想再支持原始界面。在这些情况下,适配器是有意义的。
  • 哇。最后一次编辑将您的回复变成了考虑到多种情况的非常彻底的答案,您绝对值得一票。给我一点时间仔细阅读并消化它。
【解决方案3】:

在这种情况下,我通常使用工厂来为我拥有的 Foo 类型获取适当的 FooHandler。在这种情况下,仍然会有一组 ifs,但它们将在工厂中,而不是处理程序的实现。

【讨论】:

  • 为什么不使用访问者模式,正如 Eric 提到的那样?
  • 我不喜欢回调并且我没有得到他需要存储状态和/或可能使用多个处理程序处理单个项目的印象。但是我不是 DP 专家。
  • 这种方法的实现似乎是其他几个答案的组合 - 使用 Map 和 Strategy 模式,Map 对象将包含 Class 对象作为键和适当类型的 FooHandler 作为值。这就是你的意思,对吧?
【解决方案4】:

是的,不要违反 LSP,这就是您在这里所做的。你考虑过策略模式吗?

【讨论】:

  • 对不起。他在哪里试图违反 LSP?
  • 他在哪里询问传递对象的类型。您应该始终能够将 Foo 视为 Foo。
  • 在这种情况下,Foo 完全可以替代ImprovedFoo。他只需要确保所有其他上下文都是如此。
  • 我不确定我是否会称我的代码违反了 LSP。我想要完成的是执行适合所传递参数类型的逻辑。我仍然可以用 ImprovementFoo 代替 Foo。但是,用 Foo 代替 ImprovementFoo 肯定是不可取的,并且肯定会破坏代码......这很好,因为它不违反 LSP。还是我错过了什么?如果与 Map 结合使用,策略模式可能会起作用(正如 pavelrappo 在他的回答中建议的那样)
  • en.wikipedia.org/wiki/Liskov_substitution_principle。无论如何,我发现几乎所有类型的运行时查询都可以确定特定子类的不良代码。
【解决方案5】:

这看起来像是基本多态性的简单案例。给 Foo 一个名为 DontWorryI'llHandleThisMyself() 之类的方法(嗯,除了没有撇号,还有一个更合理的名称)。 FooHandler 只是调用它给出的任何 Foo 的这个方法。 Foo 的派生类可以随意覆盖此方法。问题中的示例似乎是由内而外的。

【讨论】:

  • 我不同意这种方法。在我看来,没有理由向 Foo 添加行为。此外,在 handleFoo 中对 Foo 所做的任何事情都可能需要 Foo 可能不“知道”的其他信息——这将成为一个封装问题
【解决方案6】:

使用访问者模式,你可以做这样的事情,

abstract class absFoo {}
class Foo extends absFoo
{
    protected int z;

}
class ImprovedFoo extends absFoo
{
    protected double k;

}
interface FooHandler {
    void accept(IFooVisitor visitor, absFoo foo);
}
class DefaultFooHandler implements FooHandler
{
    public void accept(IFooVisitor visitor, absFoo foo)
    {
        visitor.visit(this, foo);
    }
    public void handleFoo(absFoo foo) {
        System.out.println("DefaultFooHandler");
    }
 }
class ImprovedFooHandler implements FooHandler
{
    public void handleFoo(absFoo foo)
    {
        System.out.println("ImprovedFooHandler");
    }

    public void accept(IFooVisitor visitor, absFoo foo) {
        visitor.visit(this, foo);
    }

}

interface IFooVisitor {
    public void visit(DefaultFooHandler fooHandler, absFoo foo);
    public void visit(ImprovedFooHandler fooHandler, absFoo foo);
}

class FooVisitor implements IFooVisitor{
    public void visit(DefaultFooHandler fHandler, absFoo foo) {
        fHandler.handleFoo(foo);
    }

    public void visit(ImprovedFooHandler iFhandler, absFoo foo) {
        iFhandler.handleFoo(foo);
    }


}

public class Visitor {
    public static void main(String args[]) {
        absFoo df = new Foo();
        absFoo idf = new ImprovedFoo();

        FooHandler handler = new ImprovedFooHandler();

        IFooVisitor visitor = new FooVisitor();
        handler.accept(visitor, idf);

    }
}

但这并不能保证只有 Foo 可以传递给 DefaultFooHandler。它允许 ImprovementFoo 也可以传递给 DefaultFooHandler。为了克服,可以做类似的事情

class Foo
{
    protected int z;

}
class ImprovedFoo
{
    protected double k;

}

interface FooHandler {
    void accept(IFooVisitor visitor);
}

class DefaultFooHandler implements FooHandler
{
    private Foo iFoo;

    public DefaultFooHandler(Foo foo) {
        this.iFoo = foo;
    }

    public void accept(IFooVisitor visitor)
    {
        visitor.visit(this);
    }
    public void handleFoo() {
        System.out.println("DefaultFooHandler");
    }
 }

class ImprovedFooHandler implements FooHandler
{
    private ImprovedFoo iFoo;

    public ImprovedFooHandler(ImprovedFoo iFoo) {
        this.iFoo = iFoo;
    }

    public void handleFoo()
    {
        System.out.println("ImprovedFooHandler");
    }

    public void accept(IFooVisitor visitor) {
        visitor.visit(this);
    }

}

interface IFooVisitor {
    public void visit(DefaultFooHandler fooHandler);
    public void visit(ImprovedFooHandler fooHandler);
}

class FooVisitor implements IFooVisitor{
    public void visit(DefaultFooHandler fHandler) {
        fHandler.handleFoo();
    }

    public void visit(ImprovedFooHandler iFhandler) {
        iFhandler.handleFoo();
    }


}
public class Visitor {
    public static void main(String args[]) {
        FooHandler handler = new DefaultFooHandler(new Foo());
        FooHandler handler2 = new ImprovedFooHandler(new ImprovedFoo());

        IFooVisitor visitor = new FooVisitor();
        handler.accept(visitor);

        handler2.accept(visitor);

    }
}

【讨论】:

  • 但是我猜你是不允许编辑 Foo 和 DefaultFooHandler 的。如果是这样的话,这个解决方案就没用了。
猜你喜欢
  • 2012-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-19
  • 2011-08-01
  • 1970-01-01
相关资源
最近更新 更多