Decomposition and Composition
A chess game is complicated. There are pieces, positions, moves, captures, checks, castling, en passant, promotion, stalemate... How do you build something so complex?
You don't. You build small things. Then you combine them.
This is the fundamental strategy for managing complexity: decomposition (breaking big things into small things) and composition (combining small things into big things).
The Problem of Complexity
Imagine trying to write a chess game as one giant piece of code. Every rule, every piece, every special case, all tangled together. It would be:
- Unreadable: Too much to hold in your head
- Unmodifiable: Change one thing, break ten others
- Untestable: Can't isolate parts to verify
- Unreusable: Nothing can be extracted for other uses
The antidote is decomposition: identify separate concerns and handle each independently.
Procedural: Procedures and Modules
The procedural approach decomposes by naming sequences of commands:
procedure validate_move(board, move):
if not is_valid_position(move.from):
return false
if not is_valid_position(move.to):
return false
piece = get_piece(board, move.from)
if piece is empty:
return false
if not can_piece_reach(piece, move.from, move.to):
return false
return true Notice how validate_move delegates to smaller procedures:
is_valid_positionget_piececan_piece_reach
Each of those might decompose further. We build a tree of procedures, each handling one concern.
Functional: Functions and Pipelines
The functional approach decomposes by defining small transformations:
valid_moves(board, player) =
board.pieces
|> filter((p) => p.owner == player)
|> flat_map((p) => moves_for_piece(board, p))
|> filter((m) => is_legal(board, m)) The power is in composition---the |> operator:
// These small functions...
get_player_pieces(board, player)
moves_for_piece(board, piece)
is_legal(board, move)
// ...compose into complex behavior
valid_moves = get_player_pieces
>> flat_map(moves_for_piece)
>> filter(is_legal)Object-Oriented: Objects and Collaboration
The object-oriented approach decomposes by distributing knowledge and behavior:
class Knight:
valid_moves(board):
// Knight knows its own movement rules
return l_shaped_moves_from(position, board)
class Board:
pieces_for(player):
// Board knows where pieces are
return pieces.filter((p) => p.owner == player)
is_legal(move):
// Board knows legality rules
return not causes_self_check(move)
class Game:
valid_moves():
// Game orchestrates
player_pieces = board.pieces_for(current_player)
all_moves = player_pieces.flat_map((p) => p.valid_moves(board))
return all_moves.filter((m) => board.is_legal(m))This is distributed responsibility. Instead of one procedure knowing everything, knowledge lives where it belongs.
What Makes Good Decomposition?
Across all paradigms, good decomposition shares qualities:
Single Responsibility: Each unit does one thing.
// Bad: does too much
validate_and_apply_move_and_update_history(...)
// Good: separate concerns
validate_move(...)
apply_move(...)
record_in_history(...)Clear Boundaries: Inputs and outputs are explicit.
// Bad: relies on hidden global state
validate_move() // what board? what move?
// Good: explicit parameters
validate_move(board, move)Appropriate Abstraction Level: Not too detailed, not too vague.
// Too low: implementation details leak
move_piece_from_array_index_to_array_index(12, 28)
// Too high: what does this even mean?
process_game_action(thing)
// Just right: clear intent, hidden details
move_piece(board, e2, e4)Composition Patterns
Composition is how small things become big things:
Sequential Composition: Do this, then that.
// Procedural
result = step1(x)
result = step2(result)
result = step3(result)
// Functional
result = x |> step1 |> step2 |> step3
// OOP
result = processor.step1().step2().step3()Nested Composition: Outer thing contains inner things.
// Procedural: nested calls
result = outer(middle(inner(x)))
// Functional: same
result = outer(middle(inner(x)))
// OOP: object graphs
game.board.square(e4).pieceConditional Composition: Choose which path.
// All paradigms have branching
result = if condition then path_a(x) else path_b(x)The Fractal Nature
Decomposition is fractal---it applies at every scale:
- A system decomposes into services
- A service decomposes into modules
- A module decomposes into classes/functions
- A class decomposes into methods
- A method decomposes into statements
The skill is knowing when to decompose and when to stop. Too little decomposition: tangled complexity. Too much: scattered confusion.
We've seen how to break things apart and put them back together. But there's a related question: what can each part see? This leads us to our next building block: scoping---the art of controlling visibility.