InterviewPrepKit

Home / Cheat Sheet / Object-Oriented Design

Cheat sheet

How to design a file-search utility

Read the full lesson →

Make every query an object so criteria compose with and, or, not; make the tree uniform so traversal never asks file-vs-directory.

Why a parameter list fails

  • find(root, name=, size=, extension=): each optional guard is an implicit AND, nothing else.
  • Can express “over 10 MB AND ends .log”, never “over 10 MB OR ends .log, and NOT under /tmp”.
  • With 5 yes/no facts, a flag list reaches 2^5 = 32 conjunctions; the full boolean space is 2^32 (~4.3 billion). OR, NOT, and mixtures are all outside it.

The two patterns

  • Specification (query side): each criterion is a Filter with one matches(node) -> bool; combinators return Filter, so a combination is usable anywhere a single one is. A query is a tree, not a list.
  • Composite (tree side): File (leaf) and Directory (branch) share one Node interface (path, size()), so traversal never asks which it holds.

Filter algebra

  • Filter.__and__ → And, __or__ → Or, __invert__ → Not: these are the hooks for &, |, ~, so queries read (a | b) & ~c.
  • And = all(...), Or = any(...), Not = negation; each is itself a Filter holding Filters.
  • Each concrete criterion (NameMatches, Extension, LargerThan, UnderPath, IsFile) = one constructor + one matches; none mentions traversal or another criterion.
  • Adding a criterion = new class, no caller edits (open-closed).
        And
       /   \
     Or     Not
    /  \       \
 >10MB  .log   under /tmp

Gotchas:

  • & binds tighter than |, so a | b & c = a | (b & c); parens in (a | b) & ~c are required.
  • &/| do NOT short-circuit like the and/or keywords; a | b always builds both operands.

The tree (Composite)

  • Node: @abstractmethod size(), @property path (walks parent chain, read without ()).
  • File.size() returns bytes; Directory.size() sums children.
  • Directory.add is the only place to keep a tree a tree: rejects non-Node, refuses re-parenting (c.parent is not None), refuses cycles (walk up from new parent looking for the child).
  • walk = pre-order generator; yield node, recurse into directories, optionally prune with descend.

The costs it charges

DecisionCost
Composite uniform interfaceLeaf questions on branches have no honest answer: Directory.size() sums subtree (real find tests the dir’s own block); mtime = entry-list change, not newest child
matches depends on outside stateModifiedWithin(clock) uses dependency injection; no longer a pure function, so caching is unsound and same query varies by time
Streaming (generator)First hit in µs at constant memory, but loses len()/sorted() without buffering, single-pass, deferred exceptions and mutation, yield from depth (one frame per level)

Match vs prune

  • Same predicate, two positions: where (matcher) visits then discards; descend (pruner) never enters the subtree. Same answer, different work.
  • Negation is a property of a predicate in a position: IsFile() as descend yields only the root (no directory is a file, so none is entered).
  • Inferring pruning from the match expression = a query optimiser; correct only for monotone criteria (UnderPath yes, LargerThan no). Keep the two arguments separate.

What the design assumes

  • Signature matches(node) -> bool commits to three things: match decidable from one node alone (no “ten largest”, duplicates, “newer than sibling”); boolean not a score (no ranking); opaque code not inspectable data (can’t push into an index).
  • Relax any one → you’re building an index (locate), a stream pipeline, or a ranker instead.
  • One line: a query earns its object the moment it needs OR or NOT; after that, the next criterion is a new class with no callers to break.
Want the full picture? The lesson has the derivations, worked examples, and diagrams this card compresses into bullets. Read the full lesson →
Report a bug