ClarityOfTypes

From APIDesign

Revision as of 07:22, 2 August 2013 by JaroslavTulach (Talk | contribs)
Jump to: navigation, search

The fuzziness is all around us, including our APIs. The essay ClarityOfAccessModifiers explains the importance of correct choice of access modifiers. The EliminateFuzzyModifiers gives a recipe to turn those fuzzy meanings into clear ones. Yet the result is still not ideal. Something is missing.

This essay en-lights what it is. More on the topic can be found in Chapter 10 of TheAPIBook. Here is alternative online version.

The result of elimination of public, protected and public abstract can be triplication of each fuzzy method. This may not be bad all times, for example CharsetDecoder successfully provides duplicated versions of its methods (one for ClientAPI users and one for providers subclassing the class), but on the other hand this is definitely distracting. Here is an example of more correct, but slightly more complex elimination:

Code from Arithmetica.java:
See the whole file.

public final int sumTwo(int one, int second) {
    return overridableSumTwo(one, second);
}
protected abstract int overridableSumTwo(int one, int second);
protected final int defaultSumTwo(int one, int second) {
    return one + second;
}
 

Callers to the API reading Javadoc see all the methods and in fact only one third of them is of some interest. They need to ignore the rest. It would be much better to separate the concerns and have one API for callers and one for implementors.

This can be achieved by using DelegationAndComposition. Create one type for client API users, another type for implementors and yet another to hold methods used for callback from providers to the superclass implementation (callback means protected final as mentioned here).

Contents

Theory

Imagine we have a class full of clear, single meaning access modifiers. Just like the following Mixed one:

Code from MixedClass.java:
See the whole file.

public abstract class MixedClass {
    private int counter;
    private int sum;
 
    protected MixedClass() {
        super();
    }
 
    public final int apiForClients() {
        int subclass = toBeImplementedBySubclass();
        sum += subclass;
        return sum / counter;
    }
 
    protected abstract int toBeImplementedBySubclass();
 
    protected final void toBeCalledBySubclass() {
        counter++;
    }
}
 

However, we are not happy with mixing API method for different users in a single class. Instead we want to separate them into individual types each targeted to appropriate audience. One possible rewrite using DelegationAndComposition is to introduce one class for ClientAPI users:

Code from NonMixed.java:
See the whole file.

public final class NonMixed {
    int counter;
    private int sum;
    private final Provider impl;
 
    NonMixed(Provider impl) {
        this.impl = impl;
    }
 
    public final int apiForClients() {
        int subclass = impl.toBeImplementedBySubclass();
        sum += subclass;
        return sum / counter;
    }
}
 

The ClientAPI class NonMixed is final and as such all its methods are public final which is single meaning access modifier. Moreover there needs to be an API for subclassers:

Code from NonMixedFactory.java:
See the whole file.

public final class NonMixedFactory {
    private NonMixedFactory() {
    }
 
    public static NonMixed create(Provider impl) {
        NonMixed api = new NonMixed(impl);
        Callback callback = new Callback(api);
        impl.initialize(callback);
        return api;
    }
 
    public interface Provider {
        public void initialize(Callback c);
        public int toBeImplementedBySubclass();
    }
 
    public static final class Callback {
        final NonMixed api;
 
        Callback(NonMixed api) {
            this.api = api;
        }
        public final void toBeCalledBySubclass() {
            api.counter++;
        }
    }
}
 

The originally protected abstract methods are moved to Provider interface. Interface methods are public and this might seem like a contradiction to ClarityOfAccessModifiers. However, it is not. The instances of Provider interface are not accessible to clients of the API. They are hidden and only useful for the create factory method that turns them into NonMixed instances. As such they are single meaning methods in an ImplementOnlyInterface with only one message: implement me, don't call me!. Their clarity is kept.

There is also the Callback class which holds originally protected final methods. It is a final class, with non-public constructor, so its creation is fully under the control of the NonMixed API infrastructure. Clients of the NonMixed class cannot get access to it - the reference is only available to the Provider interface via the initialize method. As such its use is protected, only Provider interface implementor can call its methods. Moreover the class is final and as such its methods have only single meaning: call me, if you are implementing the provider. Fully clear again.

Clarity for Clients

Clients that hold reference to NonMixed class see only its public methods in Javadoc, IDE code completion, etc. They are not distracted by details useful only for providers. The code that callers need to provide is absolutelly same as the one when using the original Mixed class - e.g. we achieved clarity for callers without complicating their code even a bit (see the calls in the example in following section)!

Clarity for Implementors

The use case for implementors is slightly changed, but not dramatically. Instead of subclassing Mixed class they need to implement the Provider interface and hold a reference to the Callback class (in case they wish to call its methods, otherwise this can be left out). Instead of creating the instance of NonMixed subclass directly, they need to use the create factory method:

Code from MixedTest.java:
See the whole file.

@Test public void useWithoutMixedMeanings() {
    class AddFiveMixedCounter implements NonMixedFactory.Provider {
        private Callback callback;
 
        public int toBeImplementedBySubclass() {
            callback.toBeCalledBySubclass();
            return 5;
        }
 
        public void initialize(Callback c) {
            callback = c;
        }
    }
    NonMixed add5 = NonMixedFactory.create(new AddFiveMixedCounter());
    assertEquals("5/1 = 5", 5, add5.apiForClients());
    assertEquals("10/2 = 5", 5, add5.apiForClients());
    assertEquals("15/3 = 5", 5, add5.apiForClients());
}
 

By using this DelegationAndComposition they get few benefits: it is clear which methods they have to implement (all in the Provider) interface. They can use multiple inheritance, which works with interfaces but not class in Java and moreover they are well set for future, as evolution story of this pattern is quite good.

Evolution Aspects

Using this kind of DelegationAndComposition is useful from evolution point of view. It is easy to evolve the client part of the API, as adding new methods into final class is absolutely backward compatible in Java.

It is easy to evolve the Callback contract as well. It is also final and as such adding new methods there is OK. And new methods in Callback class is exactly what the providers of the Provider interface may need in future.

Also the evolution of Provider interface is possible and easy. Adding new methods into subclassable types is of course not backward compatible at all, but one can always create new interfaces like ExtraProvider. Because of DelegationAndComposition this addition has no effect on ClientAPI - the NonMixed class API does not need to change at all. The only thing that may need to be added is new factory method for each new interface. But that is quite well thought evolution plan.

<comments/>

Personal tools
buy