In java, can one create a fluent extensible class hierarchy with methods that can be invoked in any order?

后端 未结 4 1548
忘了有多久
忘了有多久 2021-02-06 17:07

Can one create an extensible class hierarchy in java whose methods are fluent and can be invoked in any order? (YES! see answer below), even for existing classes wh

相关标签:
4条回答
  • 2021-02-06 17:38

    A fluent interface is a different concern from the normal set of command-query methods that you already have. Separation of concerns makes it a good idea to separate them.

    Since you have an existing hierarchy of code: Write a fluent facade that does the dirty work for you.

    See also Martin Fowler: Domain-Specific Languages, 4.2: The need for a Parsing Layer.

    0 讨论(0)
  • 2021-02-06 17:42

    I believe there is a way to do this with generics... Syntax is a little less clean than the desired...

    Here is the client code...

        B<B> b = B.factoryB();
        b.setA("a").setB("b");
    
        A<A> ba = A.factoryA();
    
        ba.setA("a");
    

    Top level (real) class

    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>();
        }
    }
    

    Example Subclass

    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>();
    
        }
    
    }
    

    Helper

    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;
        }
    }
    

    It's far from perfect and can be made not to work (if you really wanted to)

    0 讨论(0)
  • 2021-02-06 17:48

    The answer (to my surprise and satisfaction) is YES. I answered this question myself: You can do this with a little work if the method invocations return instances of the class in question (see chainable below). I also found an even easier way do this if you can edit the top level source:

    In the top level class (A):

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

    Assuming C extends B and B extends A.

    Invoking:

    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");
    

    Example 1 and 3 are ways to make a existing class hierarchy fluent and to allow methods to be called in any order so long as the existing classes are fluent (but you don't have access to or can't change the source). WAY2 is an example where you do have access to the source, and want the calls to be as simple as possible.

    Full 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;
        }
    }
    

    Test class 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() + "]";
       }
    }
    

    Test class 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();
      }
    }
    

    Test Class 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();
       }
    }
    

    The Chain class

    /**
     * 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
       }
    }
    

    Chain's helper class.

    /** 
     * 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);
       }
    }
    

    Output:

    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]
    

    QED. :)

    I've not ever seen anyone do this; I think it could be a new and potentially valuable technique.

    P.S. With regard to the "elvis like usage", it is 1 or 2 lines vs 8 or more.

    Book b = null; 
    Publisher p = null; 
    List books = null; 
    String id = "Elric of 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()));
    

    vs:

    Book b = null; 
    Publisher p = null; 
    List books = null; 
    String id = "Elric of 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()));
    

    No NPE, and if the chain completes you get all the books published by the publisher of "Elric of Melnibone" (i.e. all the books "Ace" publishers has published), and if not you get a null.

    0 讨论(0)
  • 2021-02-06 17:59

    If source code is accessible, by extending what Alan wrote, I would add supplementary classes to hide generics while allowing inheritance and very compact syntax. BaseA and BaseB do the hierarchy while A and B do hide the generics.

    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("");
        }
    }
    
    0 讨论(0)
提交回复
热议问题