| Issue 45: | support chained method calls where possible | |
| 2 people starred this issue and may be notified of changes. | Back to list |
I am wondering whether I could extend the capabilities of , so that we could build chained calls as Functions? In my mind, I think it seems possible. Example of what it would look like:
public class MyClass1 {
public MyClass2 getMC2() {...}
}
public class MyClass2 {
public String getStringVal() {...}
}
MyClass1 myClass1Stub = callsTo(MyClass1.class);
Function<MyClass1,String> chained = functionFor(myClass1Stub.getMC2().getStringVal());
The obvious limitations are that the return-type of the first method call would itself have to be proxyable (non-primitive, non-final, etc.). This leads to some questions about whether or not to try this:
I don't see a way for Funcito to detect improper calls on non-proxyable return types. In those cases, the proxy handlers (i.e. CglibMethodInterceptor, etc.) would have to continue to return null, and the chained call would result in a NPE. Or else the proxy handler could use Objenesis to return some "default" instance of the non-proxyable return type, and then the chained call would have some unpredictable side-effects *and* you could silently get an incorrect Function. (Trying to build Functions around expressions that invoke Java language operators already has this problem of silent failures.)
So I am not sure whether either of those options is viable and worth the potential usage confusion. Up till now, Funcito has been developed with a goal of being as helpful as possible in detecting or preventing mis-use of the API. But I don't see a way around this potential mis-use if we implement limited chaining support.
Feb 3, 2012
Project Member
#1
kandpwel...@gmail.com
Labels:
-Type-Defect Type-Enhancement
Mar 15, 2012
And I would especially love if such a chained call could be null-safe.
Mar 15, 2012
Null safe in what way? Are you referring to the first call in the chain returning null, or are you referring to the above-mentioned possibility of having non-proxyable (i.e. final) return types from the first call? In the above example above, if .getMC2() returned null, how would you expect chained.apply(someClass1) to respond? I would think I would have to make it throw some sort of RuntimeException (probably FuncitoException). Otherwise, you could end up with a Function that didn't truly reflect the actual effect of the chained method calls. We could create a .safe() method to append to the resulting Function that captures FuncitoException (or a specific subset), and has the Function return null. Possible -- but still kind of ugly in my mind because you are really altering the meaning of the Function to map to something quite different than the mere method call chain. We would then also have to weight the advantages of the "Funcito way" at that point against the "standard way" of anonymous inner classes. Would it really feel all that more simple to call: MyClass1 myClass1Stub = callsTo(MyClass1.class); Function<MyClass1,String> chained = functionFor(myClass1Stub.getMC2().getStringVal()).safe(); I don't know... *maybe* it would. Let me know what you think, or if I was on the wrong track in answer to your proposal. That might also belong as an enhancement request to Guava or the other FP-libs, to have a transformation from a Function to a NPE-safe version of the Function, although that would be more general (and potentially more unpredictable) than the case of specifically getting a NPE in a well-defined chain.
Mar 15, 2012
Yeah, you nailed it quite right. I was thinking along this track. I often miss the "generous" behaviour of java EL in "real" Java programming.
e.g. a Chained Property Call like myObject.getX().getY().getZ() (where all get...() calls might return null) is very expensive to write in "Null-Safe" and Type-Safe java. Something like
if(myObject == null) return null;
X x = myObject.getX();
if(x == null) return null;
Y y = x.getY();
if(y == null) return null;
Z z = y.getZ();
return z;
in EL, however, it looks easy like in
#{myObject.x.y.z}
which returns null, whenever myObject, x, y or z are null.
I would love funcito to add some of this simplicity to my Java code. Maybe something along the way you proposed?
Apr 9, 2012
Another consideration: this suggested addition obviously breaks the Law Of Demeter. I'm not sure how religious I am about keeping that "law", especially with methods that are so ubiquitous that they are almost part of the language (e.g., "toString()" ). But there would definitely be detractors of this feature who see it as encouraging more "smelly" code. I could certainly document this clearly (as Mockito does with features that they say should have limited use, etc.), but it is still worth considering whether or not to proceed, in light of violating this principle.
Apr 9, 2012
That is a legitimate argument and I quite agree that there are many cases where such requirements show bad design. However, in my current usecase the objects I am operating on are TOs (TransferObjects) that have been genereated by jax-ws. In this case, the benefit of re-using small objects in the webservice interface outweighs the cost of breaking the Law of Demeter. I could also think of other cases where this might be the case: JPA entities, Hibernate objects, ... almost all cases where you store data in a composition of value objects.
Apr 10, 2012
I just did some more digging into opinions and extrapolation of LoD, and I concluded that there are plenty of valid exceptions. For instance, ubiquitous operations on things like Strings and primitive wrappers, DSLs, fluent interfaces, frameworks like Hibernate (or JAX-WS) that pretty much "force" the creation of POJO hierarchies/graphs... lots of other cases. That doesn't mean that LoD can be ignored, but there are plenty of cases which call for violating some of its more simplistic and less-nuanced definitions ("only one dot").
May 31, 2012
Hopefully, no technical issues will prevent this from working, but I have started working towards making this happen. I hope to get it in 1.1, but we will see.
Status:
Started
May 31, 2012
w00t!! *fingers crossed*
Jun 12, 2012
Cautiously optimistic... I don't quite have it working yet, but almost all of the pieces are in place to work with cglib. Even after I have proven this is possible, there will be a lot of test code to write (I know... I should be writing test first), and then I need to add this to javassist and Java dynamic proxies, and more tests. But I am now betting on this making it into 1.1
Labels:
Milestone-R1.1
Jun 14, 2012
Even more optimistic... I have it working for Guava Functions in Cglib, and maybe also for Javassist. Should be extremely easy to port to the other FP-frameworks, and relatively easy to add Java Dynamic Proxy proxying support of chains (not just Cglib and Javassist). There is still a whole lot of testing to do. Simon, if you want to to help me flush out problems by testing *very early* private versions, let me know. I probably won't release anything publicly as Beta since I haven't even finalized the feature set for 1.1.
Jun 15, 2012
That sounds very promising! Of course I will help by alpha-testing this new feature! :)
Jun 15, 2012
Just a note for the functionaljava part: F<A,B> has a convenient andThen method to compose functions. So I feel this feature less required for that framework.
Jun 15, 2012
True, Robin, but I think it would be pretty ugly to have to wrap 2 methods independently with Funcito and then join them w/ functionaljava andThen, compared to simply building the already chained call: F<A,B> myFunc = fFor(callsTo(A.class).methodX().methodB()); versus F<A,X> aToXFunc = fFor(callsTo(A.class).methodX()); F<X,B> xToBFunc = fFor(callsTo(X.class).methodB()); F<A,B> myFunc = aToXFunc.andThen(xToBFunc); So I think Functionaljava users will find it quite useful as well.
Jun 18, 2012
Simon (or anybody else), there is an alpha build available at https://funcito.ci.cloudbees.com/job/funcito/lastSuccessfulBuild/artifact/build/libs/funcito-SNAPSHOT-103_18-Jun-2012.jar (binary jar) and https://funcito.ci.cloudbees.com/job/funcito/lastSuccessfulBuild/artifact/build/libs/funcito-src-SNAPSHOT-103_18-Jun-2012.jar (source jar). The main thing I am interested in testing is the method chaining. As mentioned in my notes above, the main limitation is that any interim method calls before the last one have to have a declared return type which is proxyable (i.e., not final or primitive). Thus even if an interim method call in the chain returns a non-proxyable type (such as String) which *does* have an alternate interface that is proxyable (String implements CharacterSequence), the chain is *still* not supported. I have not yet added in support for Java Dynamic Proxy method chaining. It won't be hard to do so, but I wanted to get this out there quickly. So for now you have to use cglib or javassist. I will let you know when Java Dynamic Proxy support is in. I know I don't have all of the unit tests I want, but this was a *massive* refactoring to make chaining work, so it was hard to see all of the tests I wanted to have before I made every change. The other major feature besides method-chaining is wrapping of methods with arguments. The caveat in this case is that we only support arity-1 functional types, so static argument values have to be bound in when the wrapping is being done. I for sure will *not* be considering functional types with arity > 1 and deferred argument binding in this release (1.1) -- *maybe* a future release. Finally, I have switched warnings from System.error to JUL logging. Questions? I'm happy to answer, but first try looking at the tests in FuncitoGuavaFunction_UT.java and FuncitoGuavaPredicate_UT.java for examples of the new features.
Jun 20, 2012
I found another limitation in chaining: return types that are generic type variables may not be chained because of type-erasure. Currently, I broke even existing (i.e. non-chained) method wrappers around methods that have generic type variables return types. See FuncitoGuavaPredicate_UT.java (https://code.google.com/p/funcito/source/browse/test/org/funcito/FuncitoGuavaPredicate_UT.java) test for an example test that is broken right now (currently annotated with @Ignore). But I think there is a way to restore that functionality. But because of the erasure, we will never get to chain on the end of a Type Variable return type, such as: class List<T> { T get(int); } The runtime sees that as "Object get(int);" with a return type of Object, so when I go to proxy the return type to support chaining, we get a proxied Object rather than a proxied List, which causes a class cast exception even when we *aren't* doing chaining. I will have to detect that the return type is a TypeVariable, and return null (like I used to) rather than proxying the return object. This should fix the non-chained instance. Chained instances would result in a NPE, and I will have to document this.
Jun 22, 2012
... I misstated the above a bit, where I said "The runtime sees that as "Object get(int);" with a return type of Object, so when I go to proxy the return type to support chaining, we get a proxied Object rather than a proxied List..." The last part of that should have said: "we get a proxied Object.class rather than a proxied <T>.class." And I fixed the bug I described above for non-chained invocation returning a pure generic type <T>. Chaining with generic intermediate return types will still never work.
Jul 19, 2012
done
Status:
CodeComplete
Jul 19, 2012
split off "null-safe" version as Issue 58 .
Jul 24, 2012
(No comment was entered for this change.)
Status:
Done
|