A logbook for curious waters
← Software Engineering Field Guide

Extract Class

Before you begin: Classes, constructors, and object composition.

Extract Class moves a coherent group of fields and methods into a new class. The original class keeps a reference to the new object and delegates the relevant work to it. 1

Original version: a position is hidden among actor fields

An actor in a tile-based game has a name, a row, and a column. Position formatting belongs to the row and column together.

public sealed class Actor
{
    public string Name { get; }
    private readonly int row;
    private readonly int column;
    public Actor(string name, int row, int column)
    {
        Name = name;
        this.row = row;
        this.column = column;
    }
    public string PositionLabel() { return row + "," + column; }
}
class Actor:
    def __init__(self, name, row, column):
        self.name = name
        self._row = row
        self._column = column

    def position_label(self):
        return str(self._row) + "," + str(self._column)

If positions also appear on treasure chests, doors, and map markers, each type would otherwise repeat the same grouping and formatting.

Refactored version: position becomes an explicit object

public sealed class TilePosition
{
    private readonly int row;
    private readonly int column;
    public TilePosition(int row, int column)
    {
        this.row = row;
        this.column = column;
    }
    public string Label() { return row + "," + column; }
}

public sealed class Actor
{
    public string Name { get; }
    private readonly TilePosition position;
    public Actor(string name, int row, int column)
    {
        Name = name;
        position = new TilePosition(row, column);
    }
    public string PositionLabel() { return position.Label(); }
}

TilePosition owns the paired coordinates and their formatting. Actor.PositionLabel preserves the old call while delegating to the new object.

class TilePosition:
    def __init__(self, row, column):
        self._row = row
        self._column = column

    def label(self):
        return str(self._row) + "," + str(self._column)


class Actor:
    def __init__(self, name, row, column):
        self.name = name
        self._position = TilePosition(row, column)

    def position_label(self):
        return self._position.label()

The actor still produces 3,4 for row 3 and column 4. Other game objects can now use the same position concept without pretending to be actors.

Extract a concept, not an arbitrary slice

Move the fields and the behavior that belongs with them. If the new class constantly reaches back into the old one, reconsider whether the split follows a real responsibility.

No map-boundary validation is added here, so negative coordinates retain their prior behavior. Adding validation could be sensible, but it would be a separate behavior change.

For a single tiny class, extraction may cost more navigation than it saves. The stronger reason is that a group has its own meaning, rules, reuse, or independent changes. A name such as TilePosition explains the group; ActorHelper2 does not.

Sources

  1. Martin Fowler — Extract Class.