保护模板模式中的钩子

Protecting hooks in template pattern

我正在 Java 中实现模板模式。让我们假设以下代码:

public abstract class A {

  public final void work() {
    doPrepare();
    doWork();
  }

  protected abstract void doPrepare();

  protected abstract void doWork();
}

public final class B extends A {

  @Override 
  protected abstract void doPrepare() { /* do stuff here */ }

  @Override 
  protected abstract void doWork() { /* do stuff here */ }
}

public final class Main {

  public static void main(String[] args) {
    B b = new B();
    b.work();
  }
}

我认为这是一个很容易不小心调用 b.doWork() 而不是总是调用 b.work() 的问题。如果可能的话,"hide" 钩子最优雅的解决方案是什么?

您确实在使用模板模式。对于来自外部用户的 "hide" 详细信息,您可以做两件主要的事情:

1) 了解您的 access modifiers:

                   Access Levels  
Modifier    | Class | Package | Subclass | World | 
--------------------------------------------------
public      | Y     |  Y      | Y        | Y     |  
protected   | Y     |  Y      | Y        | N     |  
no modifier | Y     |  Y      | N        | N     |  
private     | Y     |  N      | N        | N     |  

除非有坚定的黑客通过反射欺负他们的方式来绕过这些保护,这些可以防止临时编码人员意外调用最好远离他们的方法。编码人员将对此表示赞赏。没有人愿意使用杂乱无章的 API.

目前 b.doWork()protected。所以它只会在你的 main() 中可见,如果你的 main() 在 class A 中(不是),subclass B(它是不),或者在同一个包中(不清楚,你没有post任何关于你的代码所在包的线索)。

这引出了一个问题,你想保护谁不被看到 b.doWork()?如果您正在创建的包只是您正在开发的东西,那么它可能还不错。如果许多人在这个包中四处走动,与许多其他 classes 混在一起,那么他们不太可能非常熟悉 classes AB。在这种情况下,您不希望所有内容都挂在每个人都能看到的地方。在这里最好只允许 class 和 subclass 访问。但是没有允许 subclass 访问但也不允许包访问的修饰符。只要你是 subclassing protected 就是你能做的最好的。考虑到这一点,不要创建包含大量 classes 的包。保持包裹紧凑和集中。这样一来,大多数人将在看起来不错且简单的程序包之外工作。

简而言之,如果您想看到 main() 无法调用 b.doWork(),请将 main() 放入不同的包中。

package some.other.package

public final class Main {
...
}

2) 下一条建议的价值将很难用您当前的代码来理解,因为它是关于解决只有在扩展您的代码时才会出现的问题。但它确实与保护人们不看到像 b.doWork()

这样的东西的想法密切相关

What does "program to interfaces, not implementations" mean?

在您的代码中,这意味着如果 main 看起来像这样会更好:

public static void main(String[] args) {
  A someALikeThingy = new B(); // <-- note use of type A
  someALikeThingy.work();      //The name is silly but shows we've forgotten about B 
                               //and know it isn't really just an A :)
}

为什么?使用类型 A 与类型 B 有什么关系? A 更接近于 接口 。请注意,这里我不仅仅指使用关键字 interface 时得到的结果。我的意思是类型 B 是类型 A 的具体实现,如果我不是绝对必须针对 B 编写代码,我真的宁愿不写,因为谁知道什么奇怪的非 A东西B了。这在这里很难理解,因为 main 中的代码短小精悍。此外,AB 的 public 接口 并没有什么不同。他们都只有一个 public 方法 work()。想象一下 main() 又长又复杂,想象一下 B 有各种非 A 的方法。现在假设您需要创建一个您希望 main 处理的 class C。看到 main 被输入 B 而 main 中的每一行都知道 B 只是一些简单的 A 之类的东西,这难道不是令人欣慰的吗?除了 new 之后的部分,这可能是正确的,并且可以让您免于对 main() 做任何事情,而是更新 new。确实,这不是为什么使用强类型语言有时会很好吗?

<咆哮>
如果您认为即使在 new 之后用 B() 更新那部分工作量太大,您并不孤单。这种特别的小痴迷让我们 dependency injection movement 产生了一堆框架,这些框架实际上更多地是关于自动化对象构造,而不是任何构造函数都可以让你做的简单注入。

更新:

我从你的评论中看出你不愿意创建小而紧的包。在那种情况下,考虑放弃基于继承的模板模式,转而采用基于组合的策略模式。你仍然会得到多态性。它只是不会免费发生。更多的代码编写和间接。但是你甚至可以在 运行 时间改变状态。

这似乎违反直觉,因为现在 doWork() 必须是 public。那是什么样的保护?那么现在您可以将 B 完全隐藏在 AHandler 后面,甚至隐藏在 AHandler 里面。这是一些人性化的外观 class,它包含曾经是您的模板代码但只公开 work()A 可以只是一个 interface,所以 AHandler 仍然不知道它有一个 B。这种方法在依赖注入人群中很受欢迎,但肯定没有普遍的吸引力。有些人每次都盲目地这样做,这让很多人感到恼火。你可以在这里读更多关于它的内容: Prefer composition over inheritance?

您可以做的最简单和更明显的事情是使用另一个包中的 A -> B 层次结构。

如果由于某种原因你不能这样做,那么你可以使用一个接口,让你的 B class 包装实际上来自 [=15= 的 class ].像这样:

public interface Worker {

    void work();
}

public abstract class A implements Worker {

    @Override
    public final void work() {
        doPrepare();
        doWork();
    }

    protected abstract void doPrepare();

    protected abstract void doWork();
}

public static class B implements Worker {  // does not extend A, but implements Worker

    private final A wrapped = new A() { // anonymous inner class, so it extends A

        @Override
        protected void doPrepare() {
            System.out.println("doPrepare");
        }

        @Override
        protected void doWork() {
            System.out.println("doWork");
        }
    };

    @Override
    public void work() { // delegate implementation to descendant from A
        this.wrapped.work();
    }
}

这样,受保护的方法仍然隐藏在从 A 扩展的匿名内部 class 中,它是 B 的私有最终成员。关键是 B 没有扩展 A,而是通过委托给匿名内部 class 来实现 Worker 接口的 work() 方法。因此,即使 Bwork() 方法是从属于同一包的 class 调用的,受保护的方法也不会可见。

B b = new B();
b.work(); // this method is accessible via the Work interface

输出:

doPrepare
doWork