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
andy
position as well as thewidth
andheight
) 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
anddraw
, 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 |
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!
ReplyDeletePhotography Studio Management
ERP Software Solution Company
Best Network Security Provider Company in Lucknow