Skip to content
C.W.K.
Lesson 03 of 04 · published

Hide Is a Child, Not the Root

~12 min · oop, inheritance, capability-root, abstraction

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Hide/show is the first capability, not the root abstraction." — Tinker's house sign

The Easy Wrong Shape

When a tool has exactly one feature, the cheapest design is to let that feature be the tool. An app whose only job is hiding apps would naturally grow a HiddenApps store, a hideAll() engine and a settings screen full of app lists. It would work on day one. On the day a second action arrives — dimming the display, muting a meeting app, pausing a sync — every one of those names would be wrong, and the second feature would either be bolted on beside the first or force a rewrite of the engine underneath both.

Tinker was built the other way round from its first commit. The engine never mentions apps. It knows an open class called Capability: something with a stable type ID, a display name, a set of permissions it needs, and three verbs — prepare, apply, undo. The journal stores receipts for capability changes, not for hidden apps. Presets are lists of capability steps. Commands point at presets or at capability steps. HideApplications is a subclass that happens to be the only one so far.

Inheritance That Earns Its Keep

Dad reads the world through object orientation — not as a coding style but as the way things are organized. Tinker is a clean, small specimen of why that view pays. The root defines the contract every reversible action shares: you must describe a change before making it, make it one planned piece at a time, and be able to take each piece back. The child supplies the specifics: what its input looks like, which permission it needs, what "applied" means for an app and how to reverse it. The engine is polymorphic over children it has never heard of. That is inheritance used for what it is good at — sharing an obligation — rather than for sharing code.

What the Root Deliberately Does Not Know

Look at what is absent from the root: no notion of processes, windows, visibility, or even of the Mac's UI. The root does not know whether a change is cheap or risky, fast or slow. It only knows that a change has a key (so two changes to the same thing can be recognised as one), a serialisable payload (so it can be written to disk and read back after a crash), and a flag saying whether it actually needs applying. Everything else is the child's business. Abstraction here means refusing to know.

The Test of the Design

A good root is one where adding a child touches nothing else. In Tinker, registering a new capability automatically gives it an editable command in Settings and a slot on the app door — before anyone writes a line of UI for it — because commands and presets are data that refer to capability IDs. The final lesson of this quest designs that second child on paper and checks the claim.

Code

The root: an open class the engine depends on (simplified from TinkerCore)·swift
import Foundation

enum MacPermission: String, Codable { case accessibility }

struct ChangePlan: Codable {
    var key: String          // identifies the thing being changed
    var payload: Data        // everything undo will need, serialisable
    var needsApply: Bool     // false when the world is already in that state
}

enum UndoResult { case restored, noLongerApplicable }

@MainActor
class Capability {
    let id: String                       // stable type ID, e.g. "applications.hide"
    let name: String
    let permissions: Set<MacPermission>
    init(id: String, name: String, permissions: Set<MacPermission>) {
        self.id = id; self.name = name; self.permissions = permissions
    }
    func prepare(_ input: Data) throws -> [ChangePlan] { fatalError("child must implement") }
    func apply(_ change: Data) async throws { fatalError("child must implement") }
    func undo(_ change: Data) async throws -> UndoResult { fatalError("child must implement") }
}
The engine holds capabilities by ID — it never names a child·swift
@MainActor
final class Engine {
    let capabilities: [String: Capability]
    init(_ list: [Capability]) {
        precondition(Set(list.map(\.id)).count == list.count, "duplicate capability ID")
        capabilities = Dictionary(uniqueKeysWithValues: list.map { ($0.id, $0) })
    }
}

// The app wires its children in one place:
// let engine = Engine([HideApplications(applications: SystemApplicationVisibility())])

External links

Exercise

Take a single-feature tool you know and rename its core types as if the feature were only the first child of a root. What would the root's three verbs be? Which of today's type names would turn out to be wrong the day a second feature arrives?
Hint
Names that mention the feature's nouns (apps, files, tabs) in engine-level types are the ones that break. A root's name usually describes the obligation (reversible change, capability, job), and its verbs describe the lifecycle every child shares.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.