'. '

EliminateFuzzyModifiers

From APIDesign

Revision as of 07:16, 2 August 2013 by JaroslavTulach (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

Fuzzy access modifiers are source of all evil as described at ClarityOfAccessModifiers page. Good message is that we do not need them. Our APIs can be clear enough with use of following easy steps.

The examples on this page are slightly enhanced to those discussed Chapter 10, Cooperating with Other APIs, where this topic is deeply discussed too.

Contents

Theory

Public Abstract

Imagine that there is a class with a single public abstract method like in this example:

Code from PublicAbstract.java:
See the whole file.

public abstract void increment();
 

This indeed violates the clarity rules and in many cases API designers may wish to eliminate it. The change is simple. Just split the method into two:

Code from PublicAbstract.java:
See the whole file.

public final void increment() {
    overridableIncrement();
}
protected abstract void overridableIncrement();
 

Now both methods use single meaning access modifiers, so the so wanted clarity is achieved. Moreover the usage of such API is made complex. Compare the original snippet with classical modifier:

Code from PublicAbstractTest.java:
See the whole file.

class DoubleIncrement extends PublicAbstract.Dirty {
    int counter;
 
    @Override
    public void increment() {
        counter += 2;
    }
}
DoubleIncrement doubleIncr = new DoubleIncrement();
doubleIncr.incrementTenTimes();
Assert.assertEquals(20, doubleIncr.counter);
 

With the same code rewritten to clean version with two methods. Everything remains the same, just instead of overriding the final method, one needs to override the protected abstract one:

Code from PublicAbstractTest.java:
See the whole file.

class DoubleIncrement extends PublicAbstract.Clean {
    int counter;
 
    @Override
    protected void overridableIncrement() {
        counter += 2;
    }
}
DoubleIncrement doubleIncr = new DoubleIncrement();
doubleIncr.incrementTenTimes();
Assert.assertEquals(20, doubleIncr.counter);
 

Eliminating public abstract modifiers is easy, if found desirable.

Protected

protected methods are also said to have double meaning. They can be overriden, plus they can carry some implementation for subclasses to call just like in this example:

Code from Protected.java:
See the whole file.

protected void increment() {
    // implementation:
    counter++;
}
 

Similarly like in previous case, this fuzziness can be eliminated by splitting the method into two:

Code from Protected.java:
See the whole file.

protected abstract void increment();
protected final void defaultIncrement() {
    counter++;
}
 

The expressive power of such API remains unchanged. One can still rewrite code that is using the old version:

Code from ProtectedTest.java:
See the whole file.

class DoubleIncrement extends Protected.Dirty {
    @Override
    protected void increment() {
        super.increment();
        super.increment();
    }
}
DoubleIncrement doubleIncr = new DoubleIncrement();
doubleIncr.incrementTenTimes();
doubleIncr.assertCounter(20);
 

into code written against new version quite easily:

Code from ProtectedTest.java:
See the whole file.

class DoubleIncrement extends Protected.Clean {
    @Override
    protected void increment() {
        // cannot be access directly, it is abstract:
        // super.increment();
        // we need to call default implementation instead
        defaultIncrement();
        defaultIncrement();
    }
}
DoubleIncrement doubleIncr = new DoubleIncrement();
doubleIncr.incrementTenTimes();
doubleIncr.assertCounter(20);
 

Just instead of calling super.increment() you need to delegate to the protected final defaultImplementation. The rest of the code stays the same. It is so easy to clarify purpose of protected methods!

Public

The plain public modifier has in fact even more meanings. Just writing:

Code from Public.java:
See the whole file.

public void increment() {
    // internal implementation
    counter++;
}
 

may actually mean three different things. In the process of cleaning the meaning we thus need to split the method in following way:

Code from Public.java:
See the whole file.

public final void increment() {
    overridableIncrement();
}
protected abstract void overridableIncrement();
protected final void defaultIncrement() {
    counter++;
}
 

Again this does not make any use more complex. The old test:

Code from PublicTest.java:
See the whole file.

class DoubleIncrement extends Public.Dirty {
    @Override
    public void increment() {
        super.increment();
        super.increment();
    }
}
DoubleIncrement doubleIncr = new DoubleIncrement();
doubleIncr.incrementTenTimes();
doubleIncr.assertCounter(20);
 

needs just two modifications - name of overriden method is different and instead of calling super implementation, we call the default one:

Code from PublicTest.java:
See the whole file.

class DoubleIncrement extends Public.Clean {
    @Override
    protected void overridableIncrement() {
        defaultIncrement();
        defaultIncrement();
    }
}
DoubleIncrement doubleIncr = new DoubleIncrement();
doubleIncr.incrementTenTimes();
doubleIncr.assertCounter(20);
 

Getting rid of multi-meaning public modifiers is easy too.

"Real World" Example

Let's now look how we can use the above suggestions on well known Arithmetica and Factorial example that started the whole Odyssey with access modifiers. However we are not going to reach end of our journey yet. We will have correct, but slightly ugly API. We'll fix that later.

All methods in Arithmetica class are public and as such we are advised to split each of them into three:

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

Looks complex, doesn't it? Instead of one method three. However now we, as authors of the API, can at least see what the original meaning of our single public sumTwo method was. Is it important to understand this? Yes, it is. As soon as you are about to rewrite the sumRange method, you are immediately faced with an important dilemma:

Code from Arithmetica.java:
See the whole file.

// Again, an API author has to ask what sumAll one wants to call?
// Am I about to open up to subclasses or do I want default impl?
return openUpToSubclasses() ?
    sumAll(array) :
    defaultSumAll(array);
 

Do you want to open up to subclasses or do you want to call the default implementation? Both is possible, but each leads to different API! Only because we correctly split the public methods into fuzzyless replacement we could enlarge our horizons and notice this important question. If we decide to open up and call the sumAll method we allow the Factorial implementation:

Code from Factorial.java:
See the whole file.

public final class Factorial extends Arithmetica {
    public int factorial(int n) {
        return sumRange(1, n);
    }
 
    @Override
    protected int overridableSumTwo(int one, int second) {
        return one * second;
    }
 
    @Override
    protected int overridableSumAll(int... numbers) {
        return defaultSumAll(numbers);
    }
 
    @Override
    protected int overridableSumRange(int from, int to) {
        return defaultSumRange(from, to);
    }
}
 

which correctly computes the factorial. However if we decide to call just the defaultSumAll and defaultSumTwo implementations, the same attempt to implement factorial will fail. Why? Because providing new implementation of overridableSumTwo will have effect only on those who directly call sumTwo, not on behaviour of default sumRange.

Different API

The important thing is that the choice of modifiers can turn the API into something completely else. Boths APIs can be useful in certain situations, but one shall be aware that they are completely different. It is not necessary to EliminateFuzzyModifiers all the time, but remember to perform such mental exercise in your mind at least. Only then you'll see what kind of API you are in fact giving to your users.

In the particular Arithmetica example the intention very likely is (especially in the light of future optimization suggested in AlternativeBehaviour) to support just summing numbers. Computing factorial is a misuse, not a use case. In such case, one wants to call the defaultSumAll and defaultSumTwo.

Moreover the subclassing is not a use case either. As such it makes sense to eliminate all the protected methods and restrict the API to just:

public final int sumTwo(int one, int second);
public final int sumAll(int... numbers);
public final int sumRange(int from, int to);

Simple, easy to call, good looking API. We achieved this by realizing that the original access modifier was fuzzy, we did the mechanical split into three single meaning methods replacement and finally we have choosen to eliminate the unnecessary meanings. This is the base method for making sure our APIs really do what we think they do.

Support for Factorial like Use Case

If, on the other hand, we decide to support subclassing, we'll be left with up-to triple methods for the same thing in one class. As you can imagine this may not look good in Javadoc. Why do users who just call into the class need to be bothered with information about methods only for subclasses? This is not real cluelessness.

On the other hand, the situation is not necessarily that bad. For example the java.nio.charset.CharsetEncoder is using style with public XYZ methods and pair protected implXYZ ones. But when we seek clarity, we shall not stop. We shall seek for better alternative.

Let's think about ClarityOfTypes - but let's leave this for next time. If there is an interest, I'll reveal more. Meanwhile you may want to find the answer in Chapter 10, Cooperating with Other APIs of TheAPIBook.

Pre/Post Conditions

Careful readers might have realized that the split to public final method and protected abstract in fact allows the API to enforce pre/post conditions inside of the final method. This is very good for clarity and consistency too. But again more about that in Chapter 10 or later, if there is some interest...

TBD: Allows pre/post conditions. E.g. enforcing consistency of an API

Personal tools
buy