在Java中,是否可以创建一个流畅可扩展的类层次结构,其中方法可以以任何顺序调用?

3
在Java中,是否可以创建一个可扩展的类层次结构,其方法是流畅的并且可以以任何顺序调用?是的!请参见下面的答案,即使对于现有的类,当您无法访问源代码时,只要方法是流畅的,也可以提供此功能!
我正在改装一个现有的层次结构,并希望使用工厂或至少是通用构造函数和(最终)不可变的生成器模式(JB P.14)。设置字段的方法返回void - 最好让它们返回通用的T - 这样我们将获得执行方法链接的能力(它们现在都调用super)。
目标:
1. 避免在每个类中创建静态getFactory()方法。 2. 简单的方法签名。 3. 创建一个通用的工厂方法,但会在编译时捕获问题。 4. 在发生错误时获得编译时错误而不是运行时错误。
根据要求,非通用代码非常简单,但不起作用。
public class A {
    private String a = null;
    protected A setA(String a){
        this.a = a;
        return this;//<== DESIRE THIS TO BE CHAINABLE
    }
    protected static A factory(){
       return new A();
    }
}  

.

public class B extends A {
    private String b = null;
    protected Foo setB(String b){
        this.b = b;
        return this;//<== DESIRE THIS TO BE CHAINABLE
    }
    protected static B factory(){
        return new B();
    }
}

现在,调用者可以尝试调用B.factory().setA("a").setB("b")//无法编译

但是这样不能编译,因为setA()返回的是A而不是B。你可以通过覆盖B中的setA()方法,调用setB()并返回B而不是A来使其工作。避免为每个方法委托是关键。我只想要一个可扩展的链式类方法组,可以以任何顺序调用。B.getFactory().B("b").A("a")显然可以工作。


1
你能发布非泛型版本吗?这样更容易看出你试图解决的问题。 - David Moles
不太确定Java是否支持,但有些编程语言允许使用Shadow函数。因此,Bar可以遮蔽setA并返回一个Bar而不是Foo。新的setA只需调用基类中的函数。 - the_lotus
如果你想调用setB(),你需要知道该项属于类型B而不是A。因此,对我来说,最简单的解决方案似乎是忘记继承。你为什么认为你需要它呢? - flup
1
还要看一下 Coplien 的“奇妙递归模板模式”。 - flup
显示剩余6条评论
4个回答

3
我相信可以使用泛型来实现这个功能... 语法比期望的稍微复杂一些...
以下是客户端代码...
    B<B> b = B.factoryB();
    b.setA("a").setB("b");

    A<A> ba = A.factoryA();

    ba.setA("a");

顶层(真实)类
public  class A<S extends A> extends Chained<S> {
    private String a = null;

    protected A() {
        }

    public S setA(String a) {
        this.a = a;
        return me();
    }
    public static A<A> factoryA() {
        return new A<A>();
    }
}

示例子类

public  class B<S extends B> extends A<S> {
    private String b = null;

    B() {
    }

    public S setB(String b) {
        this.b = b;
        return me();
    }
    public static B<B> factoryB() {
        return new B<B>();

    }

}

助手

public  abstract class Chained<S extends Chained> {
    // class should be extended like:
    // ... class A<S extends A> extends Chained<S>

    public Chained() {
    }

    public final S me() {
        return (S) this;
    }
}

这并不完美,并且可以被故意破坏(如果你真的想这么做)。


1
如果您选择递归边界,应避免使用原始类型,例如Chained <S extends Chained <S>> - Paul Bellora
除非你按照Paul所说的去做,否则这个方法在第一次调用后就不会真正起作用。例如c.setA("a").setA("a").set("c");不会编译,因为第二个setA给你的是一个A而不是一个C。但如果你按照Paul所说的去做,它就能工作了。然而,你根本不需要使用Chained,A可以只是public class A<S extends A<S>> {。至少受保护的最终S me() { return (S)this; }将适用于所有子类。 - ggb667
我可以提供一些额外的东西吗?引用this可以作为参数传递给protected final S me(A<S> ref),然后与me()方法的this进行比较 - 如果它们不匹配,则可以抛出异常,这将防止奇怪的重复模板模式中的一个漏洞(传递错误的对象或null)。例如:protected final S me(A<S> type) { if( this!= ref ) { throw new IllegalArgumentException(ref==null?"Null received (required this).":"Type passed in was not this." } return (S)this; } - ggb667
我看不到在没有未经类型化警告的情况下从工厂构造或返回适当类型的S的方法。有什么办法可以做到吗? - ggb667
我相信你是对的,你需要将它从B转换为B<B>。除非你使用B<B<B<B<B>>>>,这样你需要多少就可以用多少... :( - Alan Spencer

3
我的惊喜和满足之答案是肯定的。我自己回答了这个问题:(参见链接)如果方法调用返回所讨论的类的实例,你可以通过一些努力来做到这点(参见下面的chainable)。我还发现了一种更容易的方法,如果你可以编辑顶层源代码:

在顶层类(A)中:

protected final <T> T a(T type) {
    return type
}

假设C继承自B,B继承自A。
调用:
C c = new C();
//Any order is fine and you have compile time safety and IDE assistance.
c.setA("a").a(c).setB("b").a(c).setC("c");

示例1和3是使现有类层次结构变得流畅并允许以任意顺序调用方法的方法(但您无法访问或更改源代码)。 WAY2是一个例子,您可以访问源代码,并希望调用尽可能简单。

完整的SSCCE:

import static java.lang.System.out;

public class AATester {
    public static void main(String[] args){

        //Test 1:
        for(int x: new int[]{ 0, 1, 2 } ){
            A w = getA(x);
            //I agree this is a nasty way to do it... but you CAN do it.
            Chain.a(w.setA("a1")).a(w instanceof C ? ((C) w).setC("c1") : null );
            out.println(w);
        }

        //Test for WAY 2: Hope this wins Paul Bellora's approval 
        //for conciseness, ease of use and syntactic sugar.
        C c = new C();
        //Invoke methods in any order with compile time type safety!
        c.setA("a2").a(c).setB("b2").a(c).set("C2");
        out.println(w);

        //Example 3, which is Example 1, but where the top level class IS known to be a "C"
        //but you don't have access to the source and can't add the "a" method to the 
        //top level class.  The method invocations don't have to be as nasty as Example 1.
        c = new C();
        Chain.a(c.setA("a3")).a(c.setB("b3")).a(c.setC("c3"));//Not much larger than Example 2.
        out.println(w);
    }
    public static getA(int a){//A factory method.
        A retval;//I don't like multiple returns.
        switch(a){
            case 0:  retval = new A(); break;
            case 1:  retval = new B(); break;
            default: retval = new C(); break;
        }
        return retval;
    }
}

测试A类

public class A {
   private String a;
   protected String getA() { return a; }

   //WAY 2 - where you have access to the top level source class.
   protected final <T> T a(T type) { return type; }//This is awesome!       

   protected A setA(String a) { this.a=a; return this; }//Fluent method
   @Override
   public String toString() {
      return "A[getA()=" + getA() + "]";
   }
}

测试类B

public class B extends A {
   private String b;
   protected String getB() { return b; }
   protected B setB(String b) { this.b=b; return this; }//Fluent method
   @Override
   public String toString() {
      return "B[getA()=" + getA() + ", getB()=" + getB() + "]\n  " 
      + super.toString();
  }
}

测试类 C

public class C extends B {
   private String c;
   protected String getC() { return c; }
   protected C setC(String c) { this.c=c; return this; }//Fluent method
   @Override
   public String toString() {
      return "C [getA()=" + getA() + ", getB()=" + getB() + ", getC()=" 
             + getC() + "]\n  " + super.toString();
   }
}

链表类
/**
 * Allows chaining with any class, even one you didn't write and don't have 
 * access to the source code for, so long as that class is fluent.
 * @author Gregory G. Bishop ggb667@gmail.com (C) 11/5/2013 all rights reserved. 
 */
public final class Chain {
   public static <K> _<K> a(K value) {//Note that this is static
      return new _<K>(value);//So the IDE names aren't nasty
   }
}

链表的辅助类。
/** 
 * An instance method cannot override the static method from Chain, 
 * which is why this class exists (i.e. to suppress IDE warnings, 
 * and provide fluent usage). 
 *
 * @author Gregory G. Bishop ggb667@gmail.com (C) 11/5/2013 all rights reserved.
 */
final class _<T> {
   public T a;//So we may get a return value from the final link in the chain.
   protected _(T t) { this.a = t }//Required by Chain above
   public <K> _<K> a(K value) {
      return new _<K>(value);
   }
}

输出:

A [get(A)=a]
B [get(A)=a, getB()=null]
  A [getA()=a]
C [getA()=a, getB()=null, getC()=c)]
  B [get(A)=a, getB()=null]
  A [get(A)=a]

证毕。 :)

我以前从未见过有人这样做,我认为这可能是一项新的、有潜力的技术。

关于“像Elvis使用”的问题,它只有1或2行,而不是8行或更多。

Book b = null; 
Publisher p = null; 
List books = null; 
String id = "Melnibone的埃尔里克";
books = Chain.a(b = findBook(id)).a(b != null ? p = b.getPublisher() : null) .a(p != null ? p.getPublishedBooks(): null).a;
out.println(books==null ? null : Arrays.toString(books.toArray()));

与:

Book b = null; 
Publisher p = null; 
List books = null; 
String id = "Melnibone的埃尔里克";
b = findBook(id); Array[] books = null; if( b != null ) { p = b.getPublisher(); if(p != null) { books = p.getPublishedBooks(); } }
out.println(books==null ? null : Arrays.toString(books.toArray()));

没有NPE,如果链条成功完成,你将获得由“Melnibone的埃尔里克”出版商发行的所有图书(即“Ace”出版社出版的所有图书),否则你将得到一个空值。


4
嗯,这是对空值、三元运算符和流畅方法的有趣滥用。不幸的是,这给调用者带来了相当可怕的语法负担。“流畅调用”本来就是语法糖,所以将其与如此多的检查和转换混杂在一起似乎有点自相矛盾。如果我不是代码的作者,我会很难维护任何使用这种模式的严肃代码。不过,这是一个有趣的想法——谢谢分享。 - Paul Bellora
1
更低级别的:请注意,(w instanceof C ? (C) w : null ) == null ? null : ((C) w).setC("c") 可以简化为 (w instanceof C ? ((C) w).setC("c") : null ) (instanceof 检查是否为 null)。你的示例中还有一些编译器错误:getA 需要一个返回类型,B 应该扩展 AC 也是如此,还有一些缺少分号。我只是指出这些问题,因为它看起来像一个 SSCCE。 - Paul Bellora
1
嗯,第二种和第三种方法肯定更简洁。第三种对我来说似乎并不是很“流畅”,因为它依赖于对a的嵌套调用 - 编译其示例需要在结尾添加更多括号以展示嵌套性。第二种有趣地利用了a方法,从本质上讲将更具体类型的自身返回并继续链式操作 - 它最接近流畅的状态。 - Paul Bellora
1
然而,你所有的方法都依赖于流畅接口类型在本地声明。我觉得真正的流畅接口根本不需要声明,例如String s = new StringBuilder().append("asdf").append(42).append("qwerty").toString();或者使用你的分层示例,new C().setA("").setC("")。我认为关键是:如果它被本地声明了,为什么不放弃流畅性并编写多个语句呢?尽管如此,这是一个有趣的思想实验 - 谢谢你让我参与。 - Paul Bellora
1
一个流畅的接口应该读起来更像是 same(publisher()).as(Book.with(title("Elric of Melnibone"))。或者是完全不同的东西。但你可以比使用getter方法链式调用做得更好。 - flup
显示剩余3条评论

0
如果源代码可访问,我会通过扩展Alan编写的内容来添加补充类,以隐藏泛型同时允许继承和非常紧凑的语法。BaseA和BaseB构成了层次结构,而A和B则隐藏了泛型。
BaseA
 +- A
 +- BaseB
     +- B


public class BaseA<S extends BaseA<?>> {
    private String a = null;

    protected BaseA() {
    }

    @SuppressWarnings("unchecked")
    public S setA(String a) {
        this.a = a;
        return (S) this;
    }

}

public class A extends BaseA<A> {
    public static A factoryA() {
        return new A();
    }
}

public class BaseB<S extends BaseB<?>> extends BaseA<S> {
    private String b = null;

    protected BaseB() {
    }

    @SuppressWarnings("unchecked")
    public S setB(String b) {
        this.b = b;
        return (S) this;
    }

}

public class B extends BaseB<B> {
    public static B factoryB() {
        return new B();
    }
}

public class Main {
    public static void main(String[] args) {
        B.factoryB().setA("").setB("").setB("").setA("").setA("");
    }
}

-1

流畅接口是与您已有的常规命令-查询方法不同的关注点。关注点分离使得将它们分开成为一个好主意。

由于您已经拥有现有的代码层次结构:编写一个流畅的外观,为您完成繁琐的工作。

另请参见Martin Fowler:特定领域语言,4.2:需要解析层。


虽然这是可以做到的,但每个类中都需要基本上复制所有内容,而其他类还会添加额外的方法,因此随着代码中每个级别添加额外的方法,这种解决方案会崩溃。 - ggb667
我已经更新了一些更详细的解释,说明为什么这是一个好主意。 - flup

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接