This blog discuss several principles and tips about refactoring the existing code to eliminate some problems.

Layer and Abstraction

Software systems are composed in layers, where higher layers use the facilities provided by lower layers. In a well-designed system, each layer provides a different abstraction from the layers above and below it. If a system contains adjacent layers with similar abstractions, this is a red flag that suggests a problem with the class decomposition.

Pass-through methods

When adjacent layers have similar abstractions, the problem often manifests itself in the form of pass-through methods. A pass-through method is one that does little except invoke another method, whose signature is similar or identical to that of the calling method, as shown as follows.

public class TextDocument ... {
    private TextArea textArea;
    private TextDocumentListener listener;
    ...
    public Character getLastTypedCharacter() {
        return textArea.getLastTypedCharacter();
    }
    public int getCursorOffset() {
        return textArea.getCursorOffset();
    }
    public void insertString(String textToInsert, int offset) {
        textArea.insertString(textToInsert, offset);
    }
    public void willInsertString(String stringToInsert, int offset) {
        if (listener != null) {
            listener.willInsertString(this, stringToInsert, offset);
        }
    }
    ...
}

Pass-through methods indicate that there is confusion over the division of responsibility between classes. The insertString function is clearly a pass-through method. This is usually a confusion or an overlap in responsibility between the TextDocument and TextArea class. Specifically, which class should be responsible for inserting string, TextDocument.insertString() or TextArea.insertString()? In case, it is better to redo the class decomposition.

When is interface duplication OK?

Having methods with the same or similar signature is not always bad. The key is whether the new method contributes significant functionality. Therefore, pass-through methods are bad because they contribute no new functionality. However, some patterns are acceptable and useful.

  • Dispatcher: according the input arguments, dispatcher function chooses which of several other methods should carry out each task. Although dispatcher just pass the arguments to the proper methods, it contributes important functionality.

  • Decorator (Wrapper): decorator function takes an existing object and extends its functionality. Essentailly, the motivation for decorators is to separate special-purpose extensions of a class from a more generic core, namely adding special-purpose functionalities to the exising general-purpose objectives.

Be careful with these duplicated interface and argue ourselves should we use them. For example, considering the following questions:

  1. Could you add the new functionality directly to the underlying class, rather than creating a new class?

  2. If the new functionality is specialized for a particular use case, would it make sense to merge it with the use case, rather than creating a separate class?

  3. Whether the new functionality needs to wrap the existing functionality or based on the existing functionality?

Pass-through variables

Another form of API duplication across layers is a pass-through variable, which is a variable that is passed down through a long chain of methods. This add complexity because they force all of the intermediate methods to be aware of their existence, even though the methods have no use for the variables. The solution the author use most often is to introduce a context object that stores all of the application’s global state (anything that would otherwise be a pass-through variable or global variable).

Better Together Or Better Apart?

Given two pieces of functionality, should they be implemented together in the same place, or should their implementations be separated?

The key to this big question is to reduce the complexity of the system as a whole and improve its modularity. Bringing pieces of code together is most beneficial if they are closely related; If the pieces are unrelated, they are probably better off apart.

  • They share information; for example, both pieces of code might depend on the syntax of a particular type of document.

  • They are used together: anyone using one of the pieces of code is likely to use the other as well. This form of relationship is only compelling if it is bidirectional. As a counter-example, a disk block cache will almost always involve a hash table, but hash tables can be used in many situations that don’t involve block caches; thus, these modules should be separate.

  • They overlap conceptually, in that there is a simple higher-level category that includes both of the pieces of code. For example, searching for a substring and case conversion both fall under the category of string manipulation; flow control and reliable delivery both fall under the category of network communication.

  • It is hard to understand one of the pieces of code without looking at the other.