Sunday, July 5, 2015

Economy of Means: On the Elimination of Inheritance (5/6)


Object happy families



Happy families card game

In the previous example, the various characters (Witch or Cauldron) were accessed only through the signature of their common parent class (Character). This is exactly what made our refactoring possible: all characters could be encoded using the parent class only.
This is not always the case. Consider various objects that share some common behaviour. Sometimes, they are grouped together; then only the common methods can be called. But sometimes, they are individually set up and then the specific interfaces must be accessed. That is an ideal case for inheritance, with both the union and reuse aspects intertwined.
In the next example, I draw inspiration from the java Abstract Window Toolkit (AWT), a library of graphical components. We consider a baby version of such a graphical library. It only offers two components: a Label and an HorizontalLayoutContainer. The Label displays one line of text, while the HorizontalLayoutContainer arranges components horizontally, inserting some padding between each. Some methods are shared by all components. For instance, method setBackgroundColor allows to choose the background color of any component. These methods are defined on a base element called Component. Whether Component is an abstract class or an interface does not matter from the external perspective of the library user. We postpone this decision until implementation time. The library user wants to be able to declare and assemble components in order to paint them on some canvas (class Graphics). In addition, she might need to dynamically change the aspect of her graphical interface, without having to build everything from scratch. To fulfill these needs, the public signature of the library classes might look like this:
Component
  void paint(Graphics)
  void setBackgroundColor(Color)

HorizontalLayoutContainer(): Component
  void setPadding(int)
  void add(Component)
  void remove(Component)

Label(): Component
  void setText(String)
  void setTextColor(Color)
The traditional implementation of such an API would use inheritance and could look like this:
package not.at.school.gui;

Box {
    private int x;
    private int y;
    private int width;
    private int height;
    private Component component;
    private Collection<Box> content;

    Box(int x, int y, int width, int height, Component component) {
        this(x, y, width, height, component, new ArrayList<Box>());
    }

    Box(int x, int y, int width, int height, Component component, 
        Collection<Box> content) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.component = component;
        this.content = content;
    }

    void paint(Graphics canvas) {
        this.component.draw(this.bounds, canvas);
        for (Box box: content) {
            box.paint(canvas);
        }
    }

    int getWidth() {
        return this.width;
    }

    int getHeight() {
        return this.height;
    }
}

public abstract Component {
    private Color backgroundColor;

    Component() {
        this.backgroundColor = Color.WHITE;
    }

    void draw(int x, int y, int width, int height, Graphics canvas) {
        canvas.setColor(this.backgroundColor);
        canvas.fillRect(x, y, width, height);
    }

    abstract Box computeBox(int x, int y);

    public void paint(Graphics canvas) {
        Box box = this.computeBox(0, 0);
        box.paint(canvas);
    }

    public void setBackgroundColor(Color color) {
        this.backgroundColor = color;
    }
}

public HorizontalLayoutContainer extends Component {
    private int padding;
    private List<Component> components;

    public HorizontalLayoutContainer() {
        super();
        this.padding = 0;
        this.components = new ArrayList<Component>();
    }

    override Box computeBox(int x, int y) {
        int x = location.getX();
        int y = location.getY();
        int width = 0;
        if (!this.components.isEmpty()) {
            width = -this.padding;
        }
        int height = 0;
        List<Box> content = new ArrayList<Box>();
        for (Component component: this.components) {
            Box box = component.computeBox(x + width, y);
            content.add(box);
            width = width + box.getWidth() + this.padding;
            height = Math.max(height, box.getHeight());
        }
        return new Box(x, y, width, height, this, content);
    }

    public void setPadding(int padding) {
        this.padding = padding;
    }

    public void add(Component component) {
        this.components.add(component);
    }

    public void remove(Component component) {
        this.components.remove(component);
    }
}

public Label extends Component {
    private String text;
    private Font font;
    private Color textColor;

    public Label(Font font) {
        super();
        this.text = "";
        this.font = font;
        this.textColor = Color.BLACK;
    }

    override void draw(int x, int y, int width, int height, 
                       Graphics canvas) {
        super.draw(x, y, width, height, canvas);
        canvas.setColor(this.textColor);
        canvas.setFont(this.font);
        canvas.drawString(this.text, x, y + height);
    }

    override Box computeBox(int x, int y, Graphics canvas) {
        FontMetrics metrics = graphics.getFontMetrics(this.font);
        int height = metrics.getHeight();
        int width = metrics.stringWidth(text);
        return new Box(x, y, width, height, this);
    }

    public void setText(String text) {
        this.text = text;
    }

    public void setTextColor(Color color) {
        this.textColor = color;
    }
}
Painting is a two-step process:
  • first the bounds (the x and y position as well as the width and height) of all components are computed from bottom to top,
  • then components are individually drawn going from top to bottom.
This ensures that components are stacked in the right order: backgrounds are filled before texts are drawn. All dynamic properties (bounds and sub-boxes) of a drawn component are carried by a distinct class Box. I prefer this to the traditional AWT implementation where components carry both their declaration and dynamic properties. Because of this design choice, some properties (such as the dimensions) of the AWT components are invalid until drawn.
There is an anecdoctical implementation difficulty with method draw of class Label. On the screen, y-coordinates increase downward, but strings are rendered above the baseline. Hence, when drawing text, the text height must be added to the y-coordinate.
Please take note of the access modifiers: they have been carefully picked so as to make this implementation work, while at the same time restricting external user to access only the minimal API. In order to do so:
  • all classes belong to package not.at.school.gui,
  • all fields are private,
  • methods and classes which need to be accessed from outside the package are public,
  • all remaining methods and classes are package internal.
Component is an abstract class. It includes both the signature of methods common to all components (such as abstract method computeBox), as well as the shared implementation of some methods (such as draw, paint and setBackgroundColor). So, let us play the refactoring without inheritance game once again and see where this leads us to.




As previously explained in Revisiting keyword abstract, abstract classes tend to rely on their descendant classes. So is also the case for Component whose method paint calls abstract method computeBox. We are first going to eliminate this reverse dependence relationship by applying the strategy pattern. We split Component in two by introducing a new class Renderer whose only responsibility is to paint components. This changes the external API of our package, but in my opinion to the better. Why should the Component be in charge of painting itself? APIs with more structure, and smaller classes are usually easier to understand. Here is the code which is modified to perform this first rewrite step:
public Renderer {
    private Graphics canvas;

    public Renderer(Graphics canvas) {
        this.canvas = canvas;
    }

    public void paint(Component component) {
        Box box = component.computeBox(0, 0);
        box.paint(canvas);
    }
}

public abstract Component {
    private Color backgroundColor;

    Component() {
        this.backgroundColor = Color.WHITE;
    }

    void draw(int x, int y, int width, int height, Graphics canvas) {
        canvas.setColor(this.backgroundColor);
        canvas.fillRect(x, y, width, height);
    }

    abstract Box computeBox(int x, int y);

    public void setBackgroundColor(Color color) {
        this.backgroundColor = color;
    }
}
Next we further split base class Component to distinguish the common signature definition from the code reuse aspects. Ideally, this should lead to, on one hand, an interface, and on the other hand a concrete class.
However, Component interface consists in two kinds of methods:
  • Some methods, such as setBackgroundColor, are part of the public API. An external user should be able to call them.
  • Other methods, such as computeBox and draw, are implementation details, only necessary for the code to be functional.
Unfortunately, for whatever reason, Java interfaces do not let you declare methods as package internal. Making all methods public, would unnecessarily burden the external user with useless knowledge about the internal implementation details. So we prefer a fully abstract class, with some methods public and other package internal.
We decide to name the new concrete class Background. It contains the implementation of two methods:
  • method draw fills an opaque rectangle with the component's background color,
  • while method setBackgroundColor allows to choose this color.
Here is the result of this refactoring step:
public abstract Component {
    public abstract void setBackgroundColor(Color color);
    abstract Box computeBox(int x, int y);
    abstract void draw(int x, int y, int width, int height, 
                       Graphics canvas);
}

Background {
    private Color backgroundColor;

    Background() {
        this.backgroundColor = Color.WHITE;
    }

    void draw(int x, int y, int width, int height, Graphics canvas) {
        canvas.setColor(this.backgroundColor);
        canvas.fillRect(x, y, width, height);
    }

    void setBackgroundColor(Color color) {
        this.backgroundColor = color;
    }
}
Next, we are able to replace the inheritance, by a delegation to Background, in both leaf components:
public HorizontalLayoutContainer extends Component {
    private Background background;
    private int padding;
    private List<Component> components;

    public HorizontalLayoutContainer() {
        this.background = new Background();
        this.padding = 0;
        this.components = new ArrayList<Component>();
    }

    override public void setBackgroundColor(Color color) {
        this.background.setBackgroundColor(color);
    }

    override void draw(int x, int y, int width, int height, 
                       Graphics canvas) {
        this.background.draw(x, y, width, height, canvas);
    }

    // the rest of the code remains unchanged...
}

public Label extends Component {
    private Background background;
    private String text;
    private Font font;
    private Color textColor;    

    public Label(Font font) {
        this.background = new Background();
        this.text = "";
        this.font = font;
        this.textColor = Color.BLACK;
    }

    override public void setBackgroundColor(Color color) {
        this.background.setBackgroundColor(color);
    }

    override void draw(int x, int y, int width, int height,
                       Graphics canvas) {
        this.background.draw(x, y, width, height, canvas);
        canvas.setColor(this.textColor);
        canvas.setFont(this.font);
        canvas.drawString(this.text, x, y + height);
    }

    // the rest of the code remains unchanged...
}
To recapitulate, here is the final code that results from the application of all the rewrite steps that were just described:
package not.at.school.gui;

Box {
    private int x;
    private int y;
    private int width;
    private int height;
    private Component component;
    private Collection<Box> content;

    Box(int x, int y, int width, int height, Component component) {
        this(x, y, width, height, component, new ArrayList<Box>());
    }

    Box(int x, int y, int width, int height, Component component,
        Collection<Box> content) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.component = component;
        this.content = content;
    }

    void paint(Graphics canvas) {
        this.component.draw(this.bounds, canvas);
        for (Box box: content) {
            box.paint(canvas);
        }
    }

    int getWidth() {
        return this.width;
    }

    int getHeight() {
        return this.height;
    }
}

public Renderer {
    private Graphics canvas;

    public Renderer(Graphics canvas) {
        this.canvas = canvas;
    }

    public void paint(Component component) {
        Box box = component.computeBox(0, 0);
        box.paint(canvas);
    }
}

public abstract Component {
    public abstract void setBackgroundColor(Color color);
    abstract Box computeBox(int x, int y);
    abstract void draw(int x, int y, int width, int height, 
                       Graphics canvas);
}

Background {
    private Color backgroundColor;

    Background() {
        this.backgroundColor = Color.WHITE;
    }

    void draw(int x, int y, int width, int height, Graphics canvas) {
        canvas.setColor(this.backgroundColor);
        canvas.fillRect(x, y, width, height);
    }

    void setBackgroundColor(Color color) {
        this.backgroundColor = color;
    }
}

public HorizontalLayoutContainer extends Component {
    private Background background;
    private int padding;
    private List<Component> components;

    public HorizontalLayoutContainer() {
        this.background = new Background();
        this.padding = 0;
        this.components = new ArrayList<Component>();
    }

    override Box computeBox(int x, int y) {
        int x = location.getX();
        int y = location.getY();
        int width = 0;
        if (!this.components.isEmpty()) {
            width = -this.padding;
        }
        int height = 0;
        List<Box> content = new ArrayList<Box>();
        for (Component component: this.components) {
            Box box = component.computeBox(x + width, y);
            content.add(box);
            width = width + box.getWidth() + this.padding;
            height = Math.max(height, box.getHeight());
        }
        return new Box(x, y, width, height, this, content);
    }

    override void draw(int x, int y, int width, int height,
                       Graphics canvas) {
        this.background.draw(x, y, width, height, canvas);
    }

    override public void setBackgroundColor(Color color) {
        this.background.setBackgroundColor(color);
    }

    public void setPadding(int padding) {
        this.padding = padding;
    }

    public void add(Component component) {
        this.components.add(component);
    }

    public void remove(Component component) {
        this.components.remove(component);
    }
}

public Label extends Component {
    private Background background;
    private String text;
    private Font font;
    private Color textColor;

    public Label(Font font) {
        this.background = new Background();
        this.text = "";
        this.font = font;
        this.textColor = Color.BLACK;
    }

    override Box computeBox(int x, int y, Graphics canvas) {
        FontMetrics metrics = graphics.getFontMetrics(this.font);
        int height = metrics.getHeight();
        int width = metrics.stringWidth(text);
        return new Box(x, y, width, height, this);
    }

    override void draw(int x, int y, int width, int height,
                       Graphics canvas) {
        this.background.draw(x, y, width, height, canvas);
        canvas.setColor(this.textColor);
        canvas.setFont(this.font);
        canvas.drawString(this.text, x, y + height);
    }

    override public void setBackgroundColor(Color color) {
        this.background.setBackgroundColor(color);
    }

    public void setText(String text) {
        this.text = text;
    }

    public void setTextColor(Color color) {
        this.textColor = color;
    }
}
Does this alternative implementation really look that much insane ? By avoiding inheritance and several related complex concepts, this code strives to follow a philosophy of economy of means. I personaly like it.
Admittedly, it is a real pity that Java interfaces do not accept package internal methods. This would eliminate some spurious syntax (the presence of override and abstract keywords).
Other than that, the main disadvantage of this kind of code consists in the cost of having to write all method forwarding by hand (see the definition of method setBackgroundColor in both leaf classes). With just a small extension to the Java programming language we could improve on that. If you are interested, please read the discussion in the next section.


The Parakeet and the Mermaid
Henri Matisse


1 comment:

  1. Hey Nice Blog!!! Thank you for sharing information. Wonderful blog & good post.Its really helpful for me, waiting for a more new post. Keep Blogging!

    Photography Studio Management
    ERP Software Solution Company
    Best Network Security Provider Company in Lucknow

    ReplyDelete