In a previous blog post I've talked about the importance of method references and generic functional interfaces. Today I'll go through using those principles to make existing bad APIs more usable, by converting them from inheritance-based into composable. That makes for far less code written and better reusability, and expands the tools that can be used on the new classes.
I'm trying to improve on format and content. In this entry several code blocks are peppered with comments. //
comments will represent opinions or questions, while /* */
represent absent pieces of code. If you have any feedback on either style or substance, please contact me on twitter.
Usual suspects
You may have noticed how sometimes when starting using a new library or component it's somehow a bit more terse than expected. You have to write extra code, create a set of inherited classes, call and be called from them, and get into the mindset of the component creator.
For an example I propose a fictitious library created by Manolitov84 in Githoob that claims it'll help you deal with user drags on a view:
public abstract class ItemTouchHelper {
protected final View view;
public ItemTouchHelper(View view) {
this.view = view;
}
public abstract boolean interceptDrags();
public abstract UserDrag interceptUserDrags(UserDrag drag);
public abstract void onMeasurement(View view);
public abstract void onDrag(int x, int y);
private boolean intercept = false;
public final void onDragOperation(){
UserDrags userDrags = GlobalDrags.getInstance().getLatest();
if (interceptDrags()) {
userDrags = interceptUserDrags(userDrags);
}
onMeasurement(view);
onDrag(userDrags.x, userDrags.y);
}
}
The API, in my opinion and this blog's, contains several pain points. Firstly, it uses inheritance for its API thus requiring one new class per use on a different part of an app. It uses a pair of methods interceptDrags
and interceptUserDrags
to express whether one of them should be called. There is also one method onMeasurement
that's supposed to cause modifications on the internal state of ItemTouchHelper
without providing a context of what's expected. Lastly, it requires a continuation callback whenever a new drag operation happens, called onDrag()
. This continuation makes me need to bring other external classes as fields, having to deal with understanding their fields, methods, and lifecycles.
public class PictureTouches extends ItemTouchHelper {
@Override public boolean interceptDrags() {
return true;
}
@Override public UserDrag interceptUserDrags(UserDrag drag) {
/* Operate on user drags, or not */
}
@Override public void onMeasurement() {
// What are we expected to do here?
// What methods do we have available?
// At this point you open auto completion
// to find the methods and view field
}
@Override public void onDrag(int x, int y) {
// Finally, my callback. Now what?
// I either have to continue execution in that other class
userActivity.onUserDragged(x, y);
// Or expose their elements and act directly on them
userActivity.getUserView().animateToPosition(x, y);
}
// Why does this class have to retain an instance of any other class?
public UserProfileActivity myActivity;
public ItemTouchHelper(UserProfileActivity activity) {
super(activity.getUserView());
this.myActivity = activity;
}
}
If I were presented with a non-documented version of the class, or didn't know about what happens inside onDragOperation(), it'd be difficult to discern the usage of this class. While documentation always helps, our intent is to reduce friction for future programmers by providing an API that's easier to use. For that intent I propose several strategies.
Divide and conquer
We know that single method interfaces (SMI) and @FunctionalInterface
are our friends in Java 8 and Kotlin, and meanwhile in Java 6/7 we have RxJava FuncN and ActionN to fill the gaps. You can review them on the previous blog post about functions.
The first pass we'll do on the class is to remove all inheritance-based methods and split them into several interfaces and group them by use. This has the benefits of making the class have a single purpose, yet still respect the open/close principle.
public interface OnInterceptDrags {
boolean interceptDrags();
UserDrag interceptUserDrags(UserDrag drag);
}
public interface OnMeasurement {
void onMeasurement();
}
public interface OnDragCallback {
void onDrag(int x, int y);
}
And we can iterate a bit on these to remove other issues.
Firstly, we'll make OnInterceptDrags
a SMI by returning a Pair
of a boolean
to check for execution, and an Func1<UserDrag, UserDrag>
to be called to transform the value if the first element of the pair is true.
public interface OnInterceptDrags {
Pair<Boolean, Func1<UserDrag, UserDrag>>
interceptUserDrags(UserDrag drag);
}
Secondly we'll add context to the measurement function.
public interface OnMeasurement {
void onMeasurement(View view, UserDrags userDrags);
}
OnDragCallback
is fine for now, but we'll come back to it in a following blog entry.
Finally, find the correspondence between and the generic interfaces as follows:
OnInterceptDrags
becomesFunc1<UserDrag, Pair<Boolean, Func1<UserDrag, UserDrag>>
OnMeasurement
becomesAction1<ItemTouchHelper>
OnDragCallback
becomesAction2<Integer, Integer>
And the reimplementation of our API is complete:
public final class ItemTouchHelper {
private final View view;
private final Func1<UserDrag, Pair<Boolean, Func1<UserDrag, UserDrag>>> drags;
private final Action2<View, UserDrags> measurement;
private final Action2<Integer, Integer> cb;
private final GlobalDrags dragState;
public ItemTouchHelper(
View view,
Action2<Integer, Integer> cb,
Func1<UserDrag, Pair<Boolean, Func1<UserDrag, UserDrag>>> drags,
Action2<View, UserDrags> measurement) {
this.view = view;
this.callback = cb;
this.drags = drags;
this.measurement = measurement;
this.dragState = GlobalDrags.getInstance();
}
public void onDragOperation(){
UserDrags userDrags = dragState.getLatest();
Pair<Boolean, Action1<UserDrags>> drags
= drags.interceptDrags(userDrags);
if (drags.first) {
drags.second.call(userDrags);
}
measurement.call(view, userDrags);
cb.call(userDrags.x, userDrags.y);
}
}
This change already makes the API more reusable, because it reduces the amount of friction required to use it. The helper doesn't depend on passing extra fields to do the results, and it can be created from a simple constructor calls.
public class UserProfileActivity {
ItemTouchHelper pictureTouches
= new ItemTouchHelper(
mUserView,
this::onUserDragged(),
userDrag -> Pair.with(true, this::modifyDrags()),
this::onMeasurement()
));
/* Rest of your Activity implementation */
}
A spoonful of sugar
Because our API now only depends only on constructor parameters, we can make it even better! How? By removing parameters we don't even want or need. You can take two approaches to this: builder pattern or partial application.
A builder requires using empty methods as the default, never nulls! In RxJava you have Actions.empty() for that.
public final class Builder {
private Func1<UserDrag, Pair<Boolean, Func1<UserDrag, UserDrag>>> drags
= (userDrag) -> Pair.with(false, (userDrag) -> userDrag);
private Action2<View, UserDrags> measurement
= (view, drags) -> {}; // Or Actions.empty()
private final Action2<Integer, Integer> cb;
private final View view;
public Builder(View view, Action2<Integer, Integer> callback){
this.cb = callback;
this.view = view;
}
public Builder measures(Action2<View, UserDrags> ms) {
this.measurement = ms;
}
public Builder drags(
Func1<UserDrag, Pair<Boolean, Func1<UserDrag, UserDrag>>> drags) {
this.drags = drags;
}
public ItemTouchHelper build(){
return new ItemTouchHelper(
view, cb, drags, measurement);
}
}
// Usage
ItemTouchHelper pictureTouches
= new ItemTouchHelperBuilder(mUserView, this::onUserDragged())
.build();
Partial application is available because constructors are method references too. If you are on Java 6/7 you have to create a factory method first.
// Only in Java 6/7
Func4<View, Action2<Integer, Integer>, Func1<UserDrag, Pair<Boolean, Func1<UserDrag, UserDrag>>>, Action2<View, UserDrags>, ItemTouchHelper> create() {
return (view, cb, drags, measurement) ->
new ItemTouchHelper(view, drags, measurement, cb);
}
Func2<View, Action2<Integer, Integer>, ItemTouchHelper> FACTORY
= RxPartialApplication
.applyEnd(
ItemTouchHelper::new(), // Or create()
(userDrag) -> Pair.with(false, (userDrag) -> userDrag)),
(view, drags) -> {});
// Usage
ItemTouchHelper pictureTouches
= FACTORY.call(mUserView, this::onUserDragged());
This is a massive improvement in lines of code! See how the long class PictureTouches at the top that covered a single case is now reduced to a single line of code that can be repeated any number of times through the app for all cases.
Mind the gap
The new API is looking brilliant, you may think, but I cannot modify the existing class so I'll have to deal with it :sad: :sad: :((( Fright not, as for that we have the Adapter pattern, which is a write-once approach to convert from old systems into new ones. I'll reproduce the adapter class for this example below:
private final class BetterItemTouchHelper extends ItemTouchHelper {
private final View view;
private final Func0<Boolean> dragsAvailable;
private final Func1<UserDrag, Pair<Boolean, Func1<UserDrag, UserDrag>>> drags;
private final Action2<View, UserDrags> measurement;
private final Action2<Integer, Integer> cb;
private UserDrags lastDrag;
private BetterItemTouchHelper(
View view,
Func0<Boolean> dragsAvailable,
Func1<UserDrag, Func1<UserDrag, UserDrag>> drags,
Action2<View, UserDrags> measurement,
Action2<Integer, Integer> cb){
this.view = view;
this.dragsAvailable = dragsAvailable;
this.drags = drags;
this.measurement = measurement;
this.callback = cb;
}
@Override public boolean interceptDrags() {
return dragsAvailable.call();
}
@Override public UserDrag interceptUserDrags(UserDrag drag) {
lastDrag = drag;
return drags.call(drag);
}
@Override public void onMeasurement() {
measurement.call(view, lastDrag);
}
@Override public void onDrag(int x, int y) {
cb.call(x, y);
}
}
Recap
What have we covered today? Composition makes for faster development than inheritance because it reduces the amount of code required to implement a new API. Secondly, it enhances code reuse by allowing the creation of new elements using your existing methods, and static helper components like Actions.empty(). Lastly, it makes it possible to understand at a single glance what the intention of the implementation of the class does.
If you want to apply these concepts on real projects, try wrapping an existing library or creating a wrapper for some features on your least favourite inheritance-based API.
In the next blog entry we'll go back and improve OnDragCallback
and similar callbacks. This is useful in Android because we live on lifecycles, and those callbacks can often be leaky and need to be removed to be garbage collected. Enhancing a callback system usually takes a large amount of boilerplate code, unless...
The "Intro to FRP" Series
- Functional libraries for Java
- Advantages of using generic method references, Functions and Actions as parameters
- Transforming APIs based on inheritance to simpler, composable, ones by means of Functions
- Review of Android lifecycle issues with traditional callbacks
- The reactive approach to composable and cancellable callbacks with observable sequences
- The Observable in RxJava as a composable sequence for your existing code