Install
openclaw skills install bookforge-type-code-refactoring-selectorSelect and execute the correct refactoring path for type codes — enumerations, integer constants, or string tags that flag object variants (e.g., ENGINEER/SALESMAN/MANAGER as ints, blood group as 0/1/2/3, ORDER_STATUS as strings). Applies Fowler's three-way decision tree to pick between Replace Type Code with Class, Replace Type Code with Subclasses, and Replace Type Code with State/Strategy, then drives the full mechanics for the chosen path through to Replace Conditional with Polymorphism. Use when: a class stores an integer or enum constant that controls conditional behavior in switch statements or if-else chains scattered across multiple methods or callers; Primitive Obsession or Switch Statements smells have been diagnosed and the root cause is a type code; a new variant keeps requiring edits in multiple places (classic signal that polymorphism is needed); a type code is passed between classes as a raw integer, weakening type safety and allowing invalid values; subclasses exist that vary only in constant return values (reverse path: Replace Subclass with Fields). Also covers the exceptions: use Replace Parameter with Explicit Methods instead of polymorphism when the switch affects only a single method and variants are stable; use Introduce Null Object when one of the cases is null.
openclaw skills install bookforge-type-code-refactoring-selectorYou have a class that stores a type code: an integer constant, an enum, or a string tag whose value identifies which variant of the object this is. The code switches on this value in multiple places — different methods, different callers — and adding a new variant means hunting down every switch and adding a case.
This skill applies when:
switch (_type) or if (type == ENGINEER) pattern recurs across the codebase, driven by the same type code fieldcode-smell-diagnosis skill has named Switch Statements or Primitive Obsession and the underlying cause is a type codeint — the compiler cannot enforce valid values, and callers can pass arbitrary numbersThe core insight from Fowler: Polymorphism eliminates the need to know which variant you have. Instead of asking "what type are you?" and branching, you call a method and each variant answers differently. But to reach polymorphism, you first need the right structural foundation — and which foundation you build depends on two criteria: does the type code affect behavior, and can the class be subclassed?
Upstream dependency: code-smell-diagnosis identifies the smell. This skill executes the remedy once Switch Statements or Primitive Obsession (type code variant) has been diagnosed.
Downstream: After this skill, use conditional-simplification-strategy if any complex conditionals remain that are not type-code-driven.
ENGINEER = 0, SALESMAN = 1, MANAGER = 2). Why: each value becomes either a subclass or a state object; knowing the full value set before starting prevents incomplete refactoring.Grep for the type code to map the full scope before choosing a path:
Search targets:
- The type code field name across all files → how many switch sites are there?
- switch (_type) or if (type == X) patterns → do they affect behavior or just read the value?
- The class's extends clause → does it already have a superclass?
- Setter methods on the type code → does the type change after object creation?
- Subclasses of the class → are there existing subclasses for another reason?
Does the type code drive different behavior?
(switch statements / if-else chains execute different code per value)
│
├── NO ──→ PATH A: Replace Type Code with Class
│ (type code is pure data — used for identity/classification only,
│ no conditional branching on its value)
│
└── YES
│
Can the class be subclassed?
(No existing inheritance blocking it AND type value does not change
after the object is created — no setter on the type field)
│
├── YES ──→ PATH B: Replace Type Code with Subclasses
│ (simplest path — the class itself grows subclasses,
│ one per type code value)
│
└── NO ──→ PATH C: Replace Type Code with State/Strategy
(class already subclassed for another reason, OR
type value changes at runtime — use a state object
that holds the variant behavior instead)
State vs. Strategy sub-decision (within Path C):
ACTION: Read the class holding the type code. Grep for the type code constants and field name across the entire codebase.
WHY: A type code refactoring that only touches the host class and misses switch statements in callers produces an inconsistent codebase — old integer-based calls mixed with new class-based ones. The full scope must be known before any transformation begins.
Identify:
private int _type, private String status, etc.)Output of this step: A table like:
Type code field: Employee._type
Values: ENGINEER(0), SALESMAN(1), MANAGER(2)
Switch sites:
- Employee.payAmount() — behavioral (different pay calculation per type)
- ReportGenerator.formatTitle() — behavioral (different title per type)
- EmployeeDAO.save() — non-behavioral (writes type code as integer to DB)
Type mutable: YES (setType() method exists) → cannot use subclasses
Host class inherits: NO
Decision: PATH C — Replace Type Code with State/Strategy
ACTION: Apply the decision tree using the observations from Step 1. Select Path A, B, or C.
WHY: Each path produces a fundamentally different structure. Applying Path B (subclasses) when the type changes at runtime will break the object model — an object cannot change its class. Applying Path A when the type code drives behavior leaves the switch statements in place and produces no improvement. The routing decision is the highest-leverage moment in this skill.
Decision criteria (in order):
Are there switch statements or if-else chains that execute different code depending on the type code's value?
Does the type code field have a setter, or does the host class already extend another class?
Exception checks (apply before executing the chosen path):
null — apply Introduce Null Object for that case before or alongside the main path.Precondition: Type code is pure data — no switch statements execute different behavior per value.
WHY this path: The compiler sees integers, not type names. Any integer can be passed, including invalid ones. Replacing the integer with a class gives the compiler the ability to enforce valid values at call sites. It also creates a home for behavior that belongs to the type (Move Method opportunities).
Mechanics:
Create the type code class.
BloodGroup, EmployeeType)_code that stores the underlying valuepublic static final BloodGroup O = new BloodGroup(0);_values to map integers to instancescode(int arg) that returns the correct instance from _valuesgetCode() method that returns the integer (needed during the transition)Modify the host class to use the new type.
int to the new classNewClass.INSTANCE.getCode() so existing callers still compileNewClass.code(intArg) to convert incoming integersMigrate callers one by one.
BloodGroup.O instead of Person.O)getBloodGroupCode() (old) → getBloodGroup() (new, returns BloodGroup)Remove the old integer interface.
getCode() on the type class — it is now an implementation detailAlert: Even if the type code does not cause different behavior in switch statements, check whether any behavior would be better placed on the new type code class. Apply Move Method for any method that primarily operates on the type value.
Precondition: Type code drives behavior (switch statements exist) AND the type value is immutable after object creation AND the host class has no existing superclass blocking subclassing.
WHY this path: The simplest path to polymorphism. Each type code value becomes a subclass of the host class. The subclasses override the type code getter to return their specific value (a temporary measure), then the switch statements are replaced with polymorphic method dispatch. Knowledge of which variant you have moves from callers into the class itself.
Mechanics:
Self-encapsulate the type code.
int getType() { return _type; }Replace the constructor with a factory method (if the type code is passed to the constructor).
static Employee create(int type) { return new Employee(type); }Create one subclass per type code value.
class Engineer extends Employee {
int getType() { return Employee.ENGINEER; }
}
static Employee create(int type) {
if (type == ENGINEER) return new Engineer();
else if (type == SALESMAN) return new Salesman();
// etc.
}
Remove the type code field from the superclass.
getType() abstract in the superclassApply Replace Conditional with Polymorphism (Step 4 below) on all switch statements that remain.
Push down type-specific features.
Precondition: Type code drives behavior (switch statements exist) AND either (a) the type value changes at runtime (mutable type), OR (b) the host class already has a superclass.
WHY this path: Path B requires the host class to be extended. When that is not possible — the host already has a superclass (single inheritance), or the type changes during the object's lifetime — the polymorphic behavior must live in a separate object. The host class delegates to a state/strategy object, which can be swapped at runtime without changing the host object's class.
Mechanics:
Self-encapsulate the type code.
Create the state/strategy abstract class.
EmployeeType for an employee type code)abstract int getTypeCode();Create one concrete subclass per type code value.
getTypeCode()class Engineer extends EmployeeType {
int getTypeCode() { return Employee.ENGINEER; }
}
Connect the host class to the state/strategy object.
private EmployeeType _type;int getType() { return _type.getTypeCode(); }void setType(int arg) {
switch (arg) {
case ENGINEER: _type = new Engineer(); break;
case SALESMAN: _type = new Salesman(); break;
// etc.
}
}
Move type code constants to the state class.
EmployeeType; add a factory method newType(int code) with the switchEmployeeType.newType(arg) instead of the switchEmployeeType.ENGINEER etc.Apply Replace Conditional with Polymorphism (Step 4 below) on all behavioral switch statements.
ACTION: For each switch statement (or if-else chain) that branches on the type code, replace it with a polymorphic method call.
WHY: This is the payoff step. After Paths B and C, the type code has an inheritance structure. Replace Conditional with Polymorphism moves each branch of each switch into the appropriate subclass. The switch disappears; callers simply invoke the method and get the right behavior for their variant automatically. Adding a new variant now means adding one subclass — no switch hunting.
Mechanics (apply per switch statement):
After all switches are replaced:
Replace Parameter with Explicit Methods (polymorphism overkill):
Apply instead of the full type code replacement when ALL of these are true:
Mechanics: Create a separate named method for each case. Instead of one setHeight(int metricType, double amount) with a switch on metricType, create setHeightInMeters(double amount) and setHeightInFeet(double amount). Callers use the explicit method name instead of passing a type code constant.
Introduce Null Object:
Apply when one of the switch cases handles null:
Replace Subclass with Fields (reverse path):
Apply when you have subclasses that vary only in constant return values — no real behavior difference, just returning different hard-coded constants.
Signal: subclasses that look like:
class Male extends Person { boolean isMale() { return true; } char getCode() { return 'M'; } }
class Female extends Person { boolean isMale() { return false; } char getCode() { return 'F'; } }
Mechanics:
createMale(), createFemale())super(true, 'M') etc.| Situation | Path |
|---|---|
| Type code is pure data — no switches on value | A: Replace Type Code with Class |
| Type code drives behavior + class can be subclassed + type is immutable | B: Replace Type Code with Subclasses |
| Type code drives behavior + (class already subclassed OR type changes at runtime) | C: Replace Type Code with State/Strategy |
| Focusing on a single algorithm | Name the object Strategy |
| Object transitions between states at runtime | Name the object State |
| Switch affects only one method, variants stable | Replace Parameter with Explicit Methods |
| One switch case is null | Introduce Null Object |
| Subclasses vary only in constant return values | Replace Subclass with Fields (reverse) |
Situation: Person class stores blood group as int _bloodGroup with constants O=0, A=1, B=2, AB=3. No method switches on blood group value — it is stored and retrieved for display. The type code is pure data.
Routing: No behavior-affecting switch → Path A.
Outcome: Create BloodGroup class with static instances O, A, B, AB. Person stores private BloodGroup _bloodGroup. All callers that used Person.O now use BloodGroup.O. The compiler now rejects person.setBloodGroup(99) — invalid blood groups become compile errors, not runtime surprises.
Situation: Employee stores int _type with ENGINEER=0, SALESMAN=1, MANAGER=2. Method payAmount() switches on _type to calculate different pay. The type is set once at construction. Employee has no existing superclass.
Routing: Behavior-affecting switch + no setter + no existing superclass → Path B.
Outcome: Engineer, Salesman, Manager subclasses of Employee. payAmount() becomes abstract on Employee; each subclass implements its own pay calculation. Adding a new employee type (Contractor) means adding one class — no switch hunting.
Situation: Same Employee as above, but employees can be promoted (a manager can become an engineer). setType(int arg) exists. Subclassing is impossible because the type changes at runtime.
Routing: Behavior-affecting switch + setter exists (type mutable) → Path C.
Outcome: EmployeeType abstract class with Engineer, Salesman, Manager subclasses. Employee holds private EmployeeType _type. payAmount() moves to EmployeeType and is overridden in each subclass. Calling employee.setType(Employee.ENGINEER) swaps in a new Engineer state object. The object model is valid — Employee does not change class, its delegate does.
Situation: A single method setValue(int metricType, double amount) switches on metricType (FEET=0, METERS=1). This switch appears only in this one method.
Routing: Single method affected, stable variants → Replace Parameter with Explicit Methods.
Outcome: setValueInFeet(double amount) and setValueInMeters(double amount). Callers are more readable; the compiler enforces the call rather than trusting the caller to pass a valid constant.
1. Route before executing. The most expensive mistake is applying the wrong path. Applying Path B when the type is mutable requires undoing the subclass structure and building a state object instead. Take 10 minutes to answer the two decision tree questions before writing any code.
2. Self-encapsulate first, always. Both Paths B and C begin with self-encapsulation of the type code field. Skipping this step and leaving direct field access in the host class means subclasses' getter overrides are bypassed. The refactoring silently fails.
3. One switch is acceptable after Path C. The setter in Path C has a switch that instantiates the correct state subclass. This is the only remaining switch and it is isolated to one place. Do not try to eliminate it with more indirection — it is the factory, and one factory switch is exactly right.
4. Migrate callers one at a time. In Path A especially, changing all callers at once is error-prone. Change one caller, compile, test, then the next. The old and new interfaces can coexist during the transition — that is the point of keeping getCode() available until all callers have migrated.
5. Replace Conditional with Polymorphism is the payoff. Paths B and C are scaffolding. The actual gain — no more switch hunting when adding variants — comes from completing Replace Conditional with Polymorphism. Do not stop at the structural step.
| File | Contents | When to read |
|---|---|---|
references/refactoring-prescriptions.md | Full prescription tree per smell with all conditional branches | Verifying routing decisions |
references/smell-catalog.md | Switch Statements and Primitive Obsession detection criteria | Confirming the type code is the root cause |
Skill relationships:
code-smell-diagnosis — upstream: identifies Switch Statements or Primitive Obsession that routes hereconditional-simplification-strategy — downstream: handles complex conditionals not driven by type codesdata-organization-refactoring — parallel: handles Primitive Obsession when the issue is data clusters, not type codesThis skill is licensed under CC-BY-SA-4.0. Source: BookForge — Refactoring: Improving the Design of Existing Code by Martin Fowler and Kent Beck.
Install related skills from ClawhHub:
clawhub install bookforge-code-smell-diagnosisclawhub install bookforge-conditional-simplification-strategyclawhub install bookforge-data-organization-refactoringOr install the full book set from GitHub: bookforge-skills