【问题标题】:From sub class to super class and back again从子类到超类再回来
【发布时间】:2020-06-18 07:31:47
【问题描述】:

所以想象一下,

给定一个类层次结构:

S (abstract)
A extends S
B extends S

假设我们有一个连接组件链来处理 A 和 B 类型的对象。例如: K1.handle(A) -> K2.handle(A) -> K3.handle(A) 意思是 K1 接受 A 并用 A 调用 K2,它用 A 调用 K3。

同样我们有: K1.handle(B) -> K2.handle(B) -> K3.handle(B)

事情是K2的逻辑不依赖于S的子类型。所以我们可以用K2.handle(S)替换K2.handle(A)和K2.handle(B)。但问题是在某些时候我们需要对实际对象类型进行类型检查才能调用正确的 K3 方法。

如果不执行检查实际对象的类型/属性的ifswitch 等语句,如何解决这个问题?

这是我需要研究的访问者模式吗?

举一个具体的例子。假设 K1 是一个 REST 控制器,A 和 B 具有不同的 POST 端点。它反序列化有效负载并验证数据,然后将对象传递给 K2 进行进一步处理。 K2 中包含的所有步骤(以及它调用的其他组件)都只适用于 S 类型的对象。但在 K3 的最后,我们需要知道 S 的实际类型。

【问题讨论】:

  • "K2 的逻辑不依赖于 S 的子类型" --- 确实如此,因为调用K3.handle() 是该逻辑的一部分,所以它确实依赖于S 的子类型。
  • 这个问题过于抽象,无法提供一个好的答案。常见模式包括让框架解析处理程序(如 Spring ApplicationEvent)或使用责任链。
  • 只要在S中定义了K3调用的方法,那么A和B都可以正常工作。

标签: java design-patterns dispatch


【解决方案1】:

但在 K3 的最后,我们需要知道 S 的实际类型。

所以K2.handle() 中的所有逻辑都在调用K3.handle() 之前。

另外,由于K3.handle(A)K3.handle(B) 不能是K3.handle(S),根据查询的措辞,看起来K3.handle() 是一个重载方法,即使问题中没有明确说明。

这样做的一种方法是将通用逻辑移至内部辅助方法:

public class K2 {
    public void handle(A a) {
        handleInternal(a);
        K3.handle(a);
    }
    public void handle(B b) {
        handleInternal(b);
        K3.handle(b);
    }
    private void handleInternal(S s) {
        // K2 logic here
    }
}

或者您可以使用方法引用,允许对 K3 的调用嵌入到 K2 逻辑中,而不仅仅是在末尾:

public class K2 {
    public void handle(A a) {
        handleInternal(a, K3::handle); // method reference to K3.handle(A)
    }
    public void handle(B b) {
        handleInternal(b, K3::handle); // method reference to K3.handle(B)
    }
    private <T extends S> void handleInternal(S s, Consumer<T> k3_handle) {
        // Some K2 logic here
        k3_handle.accept(s);
        // More K2 logic here
    }
}

【讨论】:

    【解决方案2】:

    给定示例类:

    class S {}
    
    class A extends S {
        public int getAInfo() {
            return 1;
        }
    }
    
    class B extends S {
        public int getBInfo() {
            return 2;
        }
    }
    

    我们可以创建一个利用有界类型参数的接口K,如下所示:

    @FunctionalInterface
    interface K<T extends S> {
        void handle(T s);
    
        @SafeVarargs
        static <FT extends S> K<FT> compose(K<FT> finalK, K<? super FT>... ks) {
            return s -> {
                Stream.of(ks).forEach(k -> k.handle(s));
                finalK.handle(s);
            };
        }
    }
    

    然后我们可以使用K.construct 来创建K 的新实例,它将在Ks 的整个管道中转发sKs 的这个管道的结构使得除了最后一个k 之外的所有都是通用处理程序,接受S 的任何实例。只有最后一个会接受S 的特定子类的实例,即FT(“final type”的缩写)。

    有了这个K 定义,下面的所有语句都会编译:

    K<S> k1 = s -> System.out.println("K1 handled.");
    K<S> k2 = s -> System.out.println("K2 handled.");
    K<A> k3A = a -> System.out.println(a.getAInfo());
    K<B> k3B = b -> System.out.println(b.getBInfo());
    
    K.compose(k3A, k1, k2).handle(new A()); // Propagates instance of A through pipeline
    K.compose(k3B, k1, k2).handle(new B()); // Propagates instance of B through pipeline
    

    然而,管道中所有Ks 的临时多态性仍然保留,不是最后一个k,因为这两个语句也可以编译:

    k1.handle(new S());
    k2.handle(new S());
    

    我在这个解决方案中看到的一个缺点是,与其他答案不同,K 处理程序必须严格按顺序执行,并且对下一个k 处理程序的调用不能发生在前一个的中间。通过我仍然认为您可以扩展这种类型的设计以支持它。

    【讨论】:

      【解决方案3】:

      是的,这是访问者模式。

      访问者是

      interface K {
          void handle(A a);
          void handle(B b);
      }
      

      K1K2K3 实现它。

      S 需要的是一个带有实现的方法visit(K)

      class A implements S {
          public void visit(K visitor) {
              visitor.handle(this); // calls the handle(A) method
          }
      }
      class B implements S {
          public void visit(K visitor) {
              visitor.handle(this); // calls the handle(A) method
          }
      }
      

      您的列表(即您的链)只是一个 List&lt;K&gt; 与类似的东西

      List<K> handlers = Arrays.asList(new K1(), new K2(), new K3());
      S sample = new A();
      for (K visitor : handlers) {
          sample.visit(visitor);
      }
      sample = new B();
      for (K visitor : handlers) {
          sample.visit(visitor);
      }
      

      这是running version

      【讨论】:

      • 应该是sample.visit(visitor),因为visit是在S下定义的?
      • @MarioIshac 是的,已更正,谢谢。可能还有其他小问题,因为我只是把它输入下来,在 ideone 添加了一个可运行的版本。
      【解决方案4】:

      我从来没有真正欣赏过访问者模式,所以这里有一个使用函数组合和 Java 泛型的替代方案。我认为您不需要使用这种方法向现有类或接口添加方法。

      您可以使用 K3.handle(A)Consumer&lt;A&gt; 的事实,您可以使用 Function&lt;A,A&gt; handleA = a -&gt; k3.handle(a); return a; 轻松将其转换为 Function&lt;A,A&gt;。这将返回类型为A 的输入元素,然后您可以将其compose() 与其他函数k2::handlek1::handle 结合使用。您不能将其作为单行上的链式调用来执行,因为这会破坏 Java 的类型推断,但这是可行的:

      K k1 = new K1();
      K k2 = new K2();
      // construct A chain
      K3A specificA = new K3A();
      Function<A, A> handleA = a -> specificA.handle(a);
      handleA = handleA.compose(k2::handle);
      handleA = handleA.compose(k1::handle);
      

      在这种情况下,

      class K {
          <T> T handle(T item);
      }
      class K3A {
          A handle(A item) { ... }
      }
      

      但如果 handle() 方法是 void,您可以将它们包装到 lambdas 中,就像我上面描述的那样。

      不确定您是否觉得这更具可读性/可维护性,但我还是非常不喜欢访客,所以我不得不尝试。

      这里是running sample,这里是fork with the wrapped version(这正是您提出的IIUC的情况,SAB 或任何Ks 中没有任何代码更改) .

      【讨论】:

      • 我将对此进行试验,但我不喜欢需要有人知道 k1、k2 和 k3 存在以及它们如何链接的事实。这将与 Spring 注入服务调用服务的当前架构背道而驰。
      • 例如,在这种方法中,我的 REST API 端点 (K1) 需要具有数据库访问权限 (K3) 的实例。
      • @Raipe 好吧,也有人需要填写您的示例中的列表,不是吗?我在 Spring 中做这种事情的方式是有一个 @Configuration 提供单个 K 方法和一个创建组合结束对象(即构造链)的方法。
      猜你喜欢
      • 2011-12-18
      • 1970-01-01
      • 1970-01-01
      • 2013-10-17
      • 2011-04-05
      • 1970-01-01
      • 1970-01-01
      • 2018-05-09
      • 2012-04-08
      相关资源
      最近更新 更多