【问题标题】:Properly designing Java class hierarchies for code sharing and encapsulation正确设计用于代码共享和封装的 Java 类层次结构
【发布时间】:2012-04-24 23:44:26
【问题描述】:

在布置类层次结构时,我经常发现自己对能够封装功能和共享代码之间的差距感到沮丧。当然,部分问题是缺乏多重继承,但接口有所帮助。在我看来,无法在接口上定义受保护的方法似乎是更大的问题。

标准的解决方案似乎是有一个由受保护的抽象基类实现的公共接口。问题是当我们有以下情况时

public interface Foo {
    public String getName();
}

abstract protected BaseFoo implements Foo {
    abstract protected int getId();

    private String name;
    protected BaseFoo(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }
}

public class ConcreteFoo extends BaseFoo {
    public ConcreteFoo (String name) {
        super(name);
    }

    @Override
    protected int getId() {
        return 4; // chosen by fair dice roll.
                  // guaranteed to be random.
    }
}

// in the foo package with the classes above
public class FooCollection {
    private static Map<Integer, Foo> foos = new HashMap();
    public static void add(Foo foo) {
        synchronized(foos) {
            foos.put(foo.getId(), foo); // can't call foo.getId()
        }
    }
}

// client code, not in the foo package
FooCollection.add(new ConcreteFoo("hello world"));

也就是说,我们将一个封装良好的对象返回给调用者,但是任何获取该对象的方法都需要能够依赖一些内部功能。该内部功能不能成为接口的一部分(这会破坏封装),但要使其成为抽象基类的一部分,我们需要使用强制转换。

我们不能使 Foo 成为一个抽象类,因为其他接口需要扩展它以将可选的正交功能添加到比此处显示的更复杂的层次结构中。

解决这个问题的标准方法是什么?您是否将 getId 添加到 Foo 接口,即使客户端不应该使用它?您是否在 FooCollection.add 中对 BaseFoo 执行了不安全的强制转换?如果您在转换前检查,当类型不匹配时,您会怎么做,即使它们始终应符合所有意图和目的?

任何关于这种情况下的最佳实践的信息都会非常有帮助。

编辑:如果不清楚,这个例子是故意过分简化的。关键是有时您会返回对象的“界面视图”。当那个“接口视图”被传回一个包特定的类时,它被传递给的方法可能需要在其实现中使用内部功能。如何管理内部和公共功能之间的不匹配?

【问题讨论】:

  • 如果你知道你需要getId(),那么你有一个ConcreteFoo的集合,而不是Foo
  • 考虑一下:如果 Base implements Foo 会怎样?
  • 哎呀,这是一个错字。谢谢

标签: java oop design-patterns interface encapsulation


【解决方案1】:

好的,这里有几点:

  1. 与流行观点相反,继承实际上并不是共享代码。您在继承层次结构中创建的是共享一些通用抽象行为的事物的组织;有时它只是起到重用某些代码的效果。

  2. 在过去的几年里,时尚发生了很大的变化,以至于深而复杂的继承层次结构不再被认为是好的形式。一般在Java中。你应该

    • 在实现接口之前使用聚合
    • 使用接口来表达“混入”合同
    • 仅当类描述具有自然继承的事物时才使用继承。
  3. 如果你真的想要多重继承的效果,为你的接口构建实现类,然后聚合它们。

  4. 特别是,通过使用接口和实现类定义类,您可以更轻松地构建测试;如果您的界面是独立的,则为该界面构建一个模拟几乎是微不足道的。

【讨论】:

  • 其实四七点。没有人会想到西班牙的宗教裁判所。
【解决方案2】:

我不知道“最佳”做法,但这里有几个想法。

接口应该将“要做什么”与“如何做”分开。我不认为 getter 和 setter 属于接口。我试着给他们更有意义的签名。

在你的情况下,我认为两个接口没有任何问题:

public interface Nameable {
    String getName();
}

public interface Identifiable {
    int getId();
}

将两者分开;迫使客户只实施他们需要的那些。您将id 作为抽象类的一部分的决定是任意的。将其分开并明确说明可能会有所帮助。

铸造失去了多态性的所有好处。我认为不应该轻易放弃。如果您必须将getId() 移至界面,请执行此操作。如果你可以通过不同的选择来避免它,那就这样做吧。

“最佳”取决于您的上下文。您的简单示例可能并非在所有情况下都是正确的。

【讨论】:

  • 感谢您的回复。我的示例故意非常简单,但也许我过于简单化了。关键是有时您会返回对象的“界面视图”。当那个“接口视图”被传回一个包特定的类时,它被传递给的方法可能需要在其实现中使用内部功能。如何管理内部功能和公共功能之间的不匹配?
  • 如果您打算返回接口引用类型,它必须指向一个对象,该对象具有该引用的用户所需的所有功能,否则您将不得不强制转换。没有神奇的公式。
猜你喜欢
  • 2013-11-21
  • 2011-05-07
  • 2011-04-02
  • 2014-12-06
  • 1970-01-01
  • 1970-01-01
  • 2018-01-08
  • 2015-11-01
  • 2011-02-24
相关资源
最近更新 更多