RefactorMCP catalog

Replace Type Code with Subclasses

Replaces an enum field that says what kind of object an instance is with a subclass per kind. Each object's class then carries its kind, so Replace Conditional with Polymorphism can move the behaviour that switches on it into the subclasses.

This is a generator: it adds types and changes how objects are created, and its after/ pins one chosen design, described below, rather than the only correct answer. Behaviour changes in two ways: GetType() returns the subclass rather than the original class, and asking the factory for a value the enum does not define throws ArgumentOutOfRangeException where the constructor used to accept it.

Arguments

None. The target is the field, by symbol: "target": { "symbol": "F:Staff.Employee._type" }.

Precondition

  • The field's type is an enum. Run Replace Type Code with Enum first for an int or string code.
  • The field's class is a class that can be derived from: not sealed, static or a record.
  • No type in the class's namespace is named after an enum member.
  • The field is assigned only in the constructor, not in methods, property setters or its declaration.
  • The class has exactly one explicit constructor, which assigns the field from one of its parameters in a statement of its own and does not chain to another constructor with this(...).
  • The result compiles, which rules out, for example, an existing member with the property's name.

Transformation

  • The class becomes abstract.
  • The field becomes an abstract get-only property, in the field's place and with its comments, named from the field without its leading underscore and with a capital first letter (_type becomes Type). It is protected when the field was private and otherwise keeps the field's accessibility. If the class already has a get-only property that just returns the field, that property becomes abstract instead and the field is removed.
  • Every read of the field reads the property.
  • The constructor loses the code parameter and the assignment, and becomes protected. A constructor left with no parameters and nothing to do is removed.
  • A factory, public static Base Create(...), takes the constructor's original parameters and returns code switch with an arm per enum member constructing its subclass, and a discard arm that throws ArgumentOutOfRangeException. It follows the constructor, or takes its place when the constructor was removed. using System; is added if needed.
  • After the class, in the same file and namespace, comes one sealed class <Member> : Base per enum member, in the enum's order, with the class's accessibility, type parameters and constraints. It has a public constructor passing the remaining parameters to base, when there are any, and overrides the property with an expression body returning its member.
  • Across the solution, new Base(Code.Member, x) becomes new Member(x), and a construction whose code is not a constant becomes Base.Create(code, x).

Preserved

  • Every read of the code gives the same value as before.
  • The class's other members, their order and comments.
  • The constructor's other parameters and statements, and the nullable annotations of its parameters, which the subclasses and factory repeat.

Limitations

  • A class that already has subclasses, or several constructors, is not supported.
  • The subclasses are not moved to files of their own; Move Type to File does that.
  • A target-typed new(...) of the class is not rewritten, so the result does not compile and the refactoring refuses.
  • The conditionals on the code stay as they are; Replace Conditional with Polymorphism replaces them.

Error codes

CodeMeaning
not-an-enumthe field's type is not an enum
class-sealedthe class is sealed, static or a record, so it cannot have subclasses
type-already-existsthe namespace already has a type named after an enum member
code-changesthe field is assigned outside the constructor, so an object's kind can change
unsupported-constructorthe class does not have exactly one constructor assigning the field from a parameter
breaks-compilationthe result would not compile

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#breaks-compilation

Refuses when the property named after the field would clash with an existing member

refusal
Targetsymbol F:Staff.Employee._type
Refusesbreaks-compilation; every file is left unchanged
Employee.csinput
1 namespace Staff;
2
3 public enum EmployeeType
4 {
5 Engineer,
6 Salesman
7 }
8
9 public class Employee
10 {
11 private readonly EmployeeType _type;
12
13 public Employee(EmployeeType type)
14 {
15 _type = type;
16 }
17
18 public string Type() => _type.ToString();
19 }

#callers-in-another-file

Constructs the subclass where a caller passes a known code, and calls the factory where it passes a variable

success
Targetsymbol F:Staff.Employee._type
Employee.csmodified
1+using System;
2+
13 namespace Staff;
24
3−public class Employee
5+public abstract class Employee
46 {
5− private readonly EmployeeType _type;
7+ protected abstract EmployeeType Type { get; }
68
7− public Employee(EmployeeType type, string name)
9+ protected Employee(string name)
810 {
9− _type = type;
1011 Name = name;
1112 }
13+
14+ public static Employee Create(EmployeeType type, string name) => type switch
15+ {
16+ EmployeeType.Engineer => new Engineer(name),
17+ EmployeeType.Salesman => new Salesman(name),
18+ _ => throw new ArgumentOutOfRangeException(nameof(type)),
19+ };
1220
1321 public string Name { get; }
1422
1523 public int Bonus()
1624 {
17− switch (_type)
25+ switch (Type)
1826 {
1927 case EmployeeType.Engineer:
2028 return 100;
2129 default:
2230 return 200;
2331 }
2432 }
2533 }
34+
35+public sealed class Engineer : Employee
36+{
37+ public Engineer(string name) : base(name)
38+ {
39+ }
40+
41+ protected override EmployeeType Type => EmployeeType.Engineer;
42+}
43+
44+public sealed class Salesman : Employee
45+{
46+ public Salesman(string name) : base(name)
47+ {
48+ }
49+
50+ protected override EmployeeType Type => EmployeeType.Salesman;
51+}
Hiring.csmodified
11 using Staff;
22
33 namespace Recruiting;
44
55 public class Hiring
66 {
7− public Employee HireSalesman(string name) => new Employee(EmployeeType.Salesman, name);
7+ public Employee HireSalesman(string name) => new Salesman(name);
88
99 public Employee Hire(EmployeeType type, string name)
1010 {
11− var hired = new Employee(type, name);
11+ var hired = Employee.Create(type, name);
1212 return hired;
1313 }
1414 }
EmployeeType.csunchanged
11 namespace Staff;
22
33 public enum EmployeeType
44 {
55 Engineer,
66 Salesman
77 }

#class-sealed

Refuses a sealed class, which cannot have subclasses

refusal
Targetsymbol F:Staff.Employee._type
Refusesclass-sealed; every file is left unchanged
Employee.csinput
1 namespace Staff;
2
3 public enum EmployeeType
4 {
5 Engineer,
6 Salesman
7 }
8
9 public sealed class Employee
10 {
11 private readonly EmployeeType _type;
12
13 public Employee(EmployeeType type)
14 {
15 _type = type;
16 }
17
18 public bool Sells() => _type == EmployeeType.Salesman;
19 }

#code-changes

Refuses a code that changes after construction, which an object's class cannot

refusal
Targetsymbol F:Staff.Employee._type
Refusescode-changes mentioning “Promote”; every file is left unchanged
Employee.csinput
1 namespace Staff;
2
3 public enum EmployeeType
4 {
5 Engineer,
6 Manager
7 }
8
9 public class Employee
10 {
11 private EmployeeType _type;
12
13 public Employee(EmployeeType type)
14 {
15 _type = type;
16 }
17
18 public void Promote()
19 {
20 _type = EmployeeType.Manager;
21 }
22
23 public bool Manages() => _type == EmployeeType.Manager;
24 }

#comments-and-blank-lines

Keeps the field's and constructor's comments and removes the assignment with its trailing comment

success
Targetsymbol F:Staff.Employee._type
Employee.csmodified
1+using System;
2+
13 namespace Staff;
24
35 public enum EmployeeType
46 {
57 Engineer,
68 Salesman
79 }
810
911 /// <summary>Someone on the payroll.</summary>
10−public class Employee
12+public abstract class Employee
1113 {
1214 /// <summary>What the employee does.</summary>
13− private readonly EmployeeType _type;
15+ protected abstract EmployeeType Type { get; }
1416
1517 /// <summary>Hires someone.</summary>
16− public Employee(EmployeeType type, string name)
18+ protected Employee(string name)
1719 {
1820 // The name is required.
1921 Name = name;
20− _type = type; // fixed for life
2122 }
23+
24+ public static Employee Create(EmployeeType type, string name) => type switch
25+ {
26+ EmployeeType.Engineer => new Engineer(name),
27+ EmployeeType.Salesman => new Salesman(name),
28+ _ => throw new ArgumentOutOfRangeException(nameof(type)),
29+ };
2230
2331 public string Name { get; }
2432
25− public bool Sells() => _type == EmployeeType.Salesman;
33+ public bool Sells() => Type == EmployeeType.Salesman;
2634 }
35+
36+public sealed class Engineer : Employee
37+{
38+ public Engineer(string name) : base(name)
39+ {
40+ }
41+
42+ protected override EmployeeType Type => EmployeeType.Engineer;
43+}
44+
45+public sealed class Salesman : Employee
46+{
47+ public Salesman(string name) : base(name)
48+ {
49+ }
50+
51+ protected override EmployeeType Type => EmployeeType.Salesman;
52+}

#enum-field

Makes the class abstract with a sealed subclass per enum member, an abstract property in place of the field and a factory taking the code

success
Targetsymbol F:Staff.Employee._type
Employee.csmodified
1+using System;
2+
13 namespace Staff;
24
3−public class Employee
5+public abstract class Employee
46 {
5− private readonly EmployeeType _type;
7+ protected abstract EmployeeType Type { get; }
68
7− public Employee(EmployeeType type, string name)
9+ protected Employee(string name)
810 {
9− _type = type;
1011 Name = name;
1112 }
13+
14+ public static Employee Create(EmployeeType type, string name) => type switch
15+ {
16+ EmployeeType.Engineer => new Engineer(name),
17+ EmployeeType.Salesman => new Salesman(name),
18+ _ => throw new ArgumentOutOfRangeException(nameof(type)),
19+ };
1220
1321 public string Name { get; }
1422
1523 public int Bonus()
1624 {
17− switch (_type)
25+ switch (Type)
1826 {
1927 case EmployeeType.Engineer:
2028 return 100;
2129 default:
2230 return 200;
2331 }
2432 }
2533 }
34+
35+public sealed class Engineer : Employee
36+{
37+ public Engineer(string name) : base(name)
38+ {
39+ }
40+
41+ protected override EmployeeType Type => EmployeeType.Engineer;
42+}
43+
44+public sealed class Salesman : Employee
45+{
46+ public Salesman(string name) : base(name)
47+ {
48+ }
49+
50+ protected override EmployeeType Type => EmployeeType.Salesman;
51+}
EmployeeType.csunchanged
11 namespace Staff;
22
33 public enum EmployeeType
44 {
55 Engineer,
66 Salesman
77 }

#existing-property

Makes the property that returned the field abstract, and drops a constructor left with nothing to do

success
Targetsymbol F:Shapes.Shape._kind
Shape.csmodified
1+using System;
2+
13 namespace Shapes;
24
35 public enum ShapeKind
46 {
57 Circle,
68 Square
79 }
810
9−public class Shape
11+public abstract class Shape
1012 {
11− private readonly ShapeKind _kind;
13+ public static Shape Create(ShapeKind kind) => kind switch
14+ {
15+ ShapeKind.Circle => new Circle(),
16+ ShapeKind.Square => new Square(),
17+ _ => throw new ArgumentOutOfRangeException(nameof(kind)),
18+ };
1219
13− public Shape(ShapeKind kind)
14− {
15− _kind = kind;
16− }
20+ public abstract ShapeKind Kind { get; }
1721
18− public ShapeKind Kind => _kind;
22+ public string Describe() => Kind == ShapeKind.Circle ? "round" : "angular";
23+}
1924
20− public string Describe() => _kind == ShapeKind.Circle ? "round" : "angular";
25+public sealed class Circle : Shape
26+{
27+ public override ShapeKind Kind => ShapeKind.Circle;
2128 }
29+
30+public sealed class Square : Shape
31+{
32+ public override ShapeKind Kind => ShapeKind.Square;
33+}

#generic-class

Gives each subclass the generic class's type parameters and constraints, in a block namespace

success
Targetsymbol F:Storage.Store`1._kind
Store.csmodified
1+using System;
12 using System.Collections.Generic;
23
34 namespace Storage
45 {
56 public enum StoreKind
67 {
78 Local,
89 Shared
910 }
1011
11− public class Store<T> where T : class
12+ public abstract class Store<T> where T : class
1213 {
13− private readonly StoreKind _kind;
14+ protected abstract StoreKind Kind { get; }
1415
15− public Store(StoreKind kind)
16+ public static Store<T> Create(StoreKind kind) => kind switch
1617 {
17− _kind = kind;
18− }
18+ StoreKind.Local => new Local<T>(),
19+ StoreKind.Shared => new Shared<T>(),
20+ _ => throw new ArgumentOutOfRangeException(nameof(kind)),
21+ };
1922
2023 public List<T> Items { get; } = new List<T>();
2124
22− public bool IsShared => _kind == StoreKind.Shared;
25+ public bool IsShared => Kind == StoreKind.Shared;
26+ }
27+
28+ public sealed class Local<T> : Store<T> where T : class
29+ {
30+ protected override StoreKind Kind => StoreKind.Local;
31+ }
32+
33+ public sealed class Shared<T> : Store<T> where T : class
34+ {
35+ protected override StoreKind Kind => StoreKind.Shared;
2336 }
2437 }

#not-an-enum

Refuses an int code, which Replace Type Code with Enum turns into an enum first

refusal
Targetsymbol F:Staff.Employee._type
Refusesnot-an-enum; every file is left unchanged
Employee.csinput
1 namespace Staff;
2
3 public class Employee
4 {
5 public const int Engineer = 0;
6 public const int Salesman = 1;
7
8 private readonly int _type;
9
10 public Employee(int type)
11 {
12 _type = type;
13 }
14
15 public bool Sells() => _type == Salesman;
16 }

#nullable-annotations

Keeps the nullable annotations of the parameters the subclasses and factory pass on

success
Targetsymbol F:Bank.Account._kind
Projectnullable enable
Account.csmodified
1+using System;
2+
13 namespace Bank;
24
35 public enum AccountKind
46 {
57 Current,
68 Savings
79 }
810
9−public class Account
11+public abstract class Account
1012 {
11− private readonly AccountKind _kind;
13+ protected abstract AccountKind Kind { get; }
1214
13− public Account(string owner, AccountKind kind, string? nickname)
15+ protected Account(string owner, string? nickname)
1416 {
1517 Owner = owner;
16− _kind = kind;
1718 Nickname = nickname;
1819 }
20+
21+ public static Account Create(string owner, AccountKind kind, string? nickname) => kind switch
22+ {
23+ AccountKind.Current => new Current(owner, nickname),
24+ AccountKind.Savings => new Savings(owner, nickname),
25+ _ => throw new ArgumentOutOfRangeException(nameof(kind)),
26+ };
1927
2028 public string Owner { get; }
2129
2230 public string? Nickname { get; }
2331
24− public decimal Rate() => _kind == AccountKind.Savings ? 0.02m : 0m;
32+ public decimal Rate() => Kind == AccountKind.Savings ? 0.02m : 0m;
2533 }
34+
35+public sealed class Current : Account
36+{
37+ public Current(string owner, string? nickname) : base(owner, nickname)
38+ {
39+ }
40+
41+ protected override AccountKind Kind => AccountKind.Current;
42+}
43+
44+public sealed class Savings : Account
45+{
46+ public Savings(string owner, string? nickname) : base(owner, nickname)
47+ {
48+ }
49+
50+ protected override AccountKind Kind => AccountKind.Savings;
51+}

#type-already-exists

Refuses when the namespace already has a type named after an enum member

refusal
Targetsymbol F:Staff.Employee._type
Refusestype-already-exists mentioning “Salesman”; every file is left unchanged
Employee.csinput
1 namespace Staff;
2
3 public enum EmployeeType
4 {
5 Engineer,
6 Salesman
7 }
8
9 public class Employee
10 {
11 private readonly EmployeeType _type;
12
13 public Employee(EmployeeType type)
14 {
15 _type = type;
16 }
17
18 public bool Sells() => _type == EmployeeType.Salesman;
19 }
20
21 public class Salesman
22 {
23 }

#unsupported-constructor

Refuses a class with several constructors, where the code does not come from one parameter

refusal
Targetsymbol F:Staff.Employee._type
Refusesunsupported-constructor; every file is left unchanged
Employee.csinput
1 namespace Staff;
2
3 public enum EmployeeType
4 {
5 Engineer,
6 Salesman
7 }
8
9 public class Employee
10 {
11 private readonly EmployeeType _type;
12
13 public Employee(EmployeeType type)
14 {
15 _type = type;
16 }
17
18 public Employee()
19 {
20 _type = EmployeeType.Engineer;
21 }
22
23 public bool Sells() => _type == EmployeeType.Salesman;
24 }