RefactorMCP catalog

Replace Conditional with Polymorphism

Replaces a method whose body chooses what to do by the kind of object it is dealing with, with a method each subclass overrides. The conditional can test either:

  • a type code: a property of the method's own class that every subclass overrides to return a constant, as Replace Type Code with Subclasses leaves it (switch (Type) { case EmployeeType.Engineer: ... }); or
  • a parameter's type: type patterns on one of the method's parameters, whose class is the hierarchy's base (case Circle c:, Circle c =>, shape is Circle c). The method may live in another class.

This is a generator: it restructures the hierarchy, and its after/ pins one chosen design, described below, rather than the only correct answer. Each case runs exactly as before for its subclass. Behaviour changes where the conditional had a default that only threw: the throw is dropped, because the abstract method leaves no subclass without an implementation. For a parameter's type, a null argument now throws NullReferenceException at the delegating call instead of reaching the default.

Arguments

None. The target is the method, by symbol: "target": { "symbol": "M:Shapes.Geometry.Area(Shapes.Shape)" }.

Precondition

  • The method's body is one of:
  • a single switch statement;
  • a switch expression, as its expression body or its only return;
  • an if / else if chain, optionally ending in else; or a run of if statements without else whose branches all end in return or throw, followed by the default's statements.
  • Every test is on the same property of the method's class, against constants, or on the same parameter of the method, against types.
  • Each label maps to subclasses declared in the solution: a constant to the subclasses whose override returns it, a type to that subclass. No label has a when clause, and no subclass is chosen by two cases. A case does not leave the switch part way through with break or goto.
  • When there is no default, or the default only throws, the base class is abstract and every concrete subclass has a case, directly or through a class between it and the base.
  • For a parameter's type, the cases use nothing of the class declaring the method except static members reached through the class's name.
  • The result compiles.

Transformation

  • Each case becomes override of the method at the end of its subclass, after a blank line, with the method's accessibility. A section with several labels becomes an override in each of their subclasses. A case that is a single return e; or a switch expression arm becomes an expression body, => e;; otherwise the statements form a block body, without a trailing break, keeping their comments.
  • The subclass's usings gain the namespaces the case needs.
  • In a generic hierarchy, each override is written with its subclass's own type arguments: in Shared<TValue> : Store<TValue>, T becomes TValue.
  • For a type code, the method is replaced in place by an abstract declaration when there is no default (or the default only throws), or by a virtual method with the default's code. Private members of the class that the moved cases use become protected.
  • For a parameter's type, the parameter's class gains, at its end, an abstract or virtual method with the same name and the method's other parameters, with the method's accessibility (private becomes internal). In the moved code the parameter and the pattern variable become the instance: c.Radius becomes Radius, and c alone becomes this. The original method keeps its signature and delegates: return shape.Area();.
  • A void method without a default gets an empty virtual method, since doing nothing was its default.
  • Usings the change leaves unnecessary are removed.

Preserved

  • What each subclass computes, and the signature callers use: the original method still exists, as the base method or as the delegating method.
  • Nullable annotations on the return type and parameters.
  • Comments inside the moved cases and on the method.

Limitations

  • Only one conditional, making up the whole body, is replaced. A method that does other work around the switch needs Extract Method first.
  • Conditions combining tests (Type == A || Type == B), relational or property patterns, and case null are not supported.
  • For a type code the code must be a property; subclasses that return their code from a field or a computed expression are not recognised.
  • Generic type arguments are translated only for direct subclasses of the base.
  • The overrides are not placed near other members of the subclass; they go at its end.

Error codes

CodeMeaning
no-conditionalthe body is not one conditional on a type code property or a parameter's type
unsupported-casea label has a when clause, is not a constant or type, matches no subclass, repeats a subclass, or its case leaves the switch early
subclass-without-casethere is no default and a concrete subclass has no case
base-not-abstractthere is no default and the base class is not abstract
uses-caller-membersa case on a parameter's type uses a member of the class declaring the method
breaks-compilationthe result would not compile, for example because a subclass already declares the method

Cases

·

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

#base-not-abstract

Refuses when there is no default to keep and the base class is not abstract, so the method could not be abstract

refusal
Targetsymbol M:Shapes.Geometry.Area(Shapes.Shape)
Refusesbase-not-abstract; every file is left unchanged
Shapes.csinput
1 using System;
2
3 namespace Shapes;
4
5 public class Shape
6 {
7 }
8
9 public sealed class Circle : Shape
10 {
11 public double Radius { get; set; }
12 }
13
14 public sealed class Square : Shape
15 {
16 public double Side { get; set; }
17 }
18
19 public static class Geometry
20 {
21 public static double Area(Shape shape) => shape switch
22 {
23 Circle c => Math.PI * c.Radius * c.Radius,
24 Square s => s.Side * s.Side,
25 _ => throw new ArgumentException("Unknown shape"),
26 };
27 }

#breaks-compilation

Refuses when a subclass already declares a method with the override's signature

refusal
Targetsymbol M:Staff.Employee.Bonus
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 abstract class Employee
10 {
11 public abstract EmployeeType Type { get; }
12
13 public int Bonus() => Type switch
14 {
15 EmployeeType.Engineer => 100,
16 _ => 0,
17 };
18 }
19
20 public sealed class Engineer : Employee
21 {
22 public override EmployeeType Type => EmployeeType.Engineer;
23
24 public new int Bonus() => 1;
25 }
26
27 public sealed class Salesman : Employee
28 {
29 public override EmployeeType Type => EmployeeType.Salesman;
30 }

#generic-hierarchy

Writes each override in terms of its subclass's own type parameters, and moves a section with two labels to both subclasses

success
Targetsymbol M:Storage.Store`1.Describe(`0)
Store.csmodified
11 using System.Collections.Generic;
22
33 namespace Storage
44 {
55 public enum StoreKind
66 {
77 Local,
88 Shared,
99 Mirrored
1010 }
1111
1212 public abstract class Store<T>
1313 {
1414 protected abstract StoreKind Kind { get; }
1515
16− public string Describe(T item)
17− {
18− switch (Kind)
19− {
20− case StoreKind.Local:
21− return "local " + item;
22− case StoreKind.Shared:
23− case StoreKind.Mirrored:
24− var copies = new List<T> { item, item };
25− return "shared " + copies.Count;
26− default:
27− return "unknown";
28− }
29− }
16+ public virtual string Describe(T item) => "unknown";
3017 }
3118
3219 public sealed class Local<T> : Store<T>
3320 {
3421 protected override StoreKind Kind => StoreKind.Local;
22+
23+ public override string Describe(T item) => "local " + item;
3524 }
3625
3726 public sealed class Shared<TValue> : Store<TValue>
3827 {
3928 protected override StoreKind Kind => StoreKind.Shared;
29+
30+ public override string Describe(TValue item)
31+ {
32+ var copies = new List<TValue> { item, item };
33+ return "shared " + copies.Count;
34+ }
4035 }
4136
4237 public sealed class Mirrored<TValue> : Store<TValue>
4338 {
4439 protected override StoreKind Kind => StoreKind.Mirrored;
40+
41+ public override string Describe(TValue item)
42+ {
43+ var copies = new List<TValue> { item, item };
44+ return "shared " + copies.Count;
45+ }
4546 }
4647 }

#if-chain

Moves the branches of an if chain comparing the type code into overrides, and the statements after it into the virtual default

success
Targetsymbol M:Bank.Account.Rate
Account.csmodified
11 namespace Bank;
22
33 public enum AccountKind
44 {
55 Current,
66 Savings,
77 Fixed
88 }
99
1010 public abstract class Account
1111 {
1212 protected abstract AccountKind Kind { get; }
1313
14− public decimal Rate()
15− {
16− if (Kind == AccountKind.Savings)
17− return 0.02m;
18− if (AccountKind.Fixed == Kind)
19− {
20− var bonus = 0.01m;
21− return 0.03m + bonus;
22− }
23−
24− return 0m;
25− }
14+ public virtual decimal Rate() => 0m;
2615 }
2716
2817 public sealed class Current : Account
2918 {
3019 protected override AccountKind Kind => AccountKind.Current;
3120 }
3221
3322 public sealed class Savings : Account
3423 {
3524 protected override AccountKind Kind => AccountKind.Savings;
25+
26+ public override decimal Rate() => 0.02m;
3627 }
3728
3829 public sealed class Fixed : Account
3930 {
4031 protected override AccountKind Kind => AccountKind.Fixed;
32+
33+ public override decimal Rate()
34+ {
35+ var bonus = 0.01m;
36+ return 0.03m + bonus;
37+ }
4138 }

#no-conditional

Refuses a method whose body is not a conditional on a type code or a type

refusal
Targetsymbol M:Shapes.Geometry.Label(Shapes.Shape)
Refusesno-conditional; every file is left unchanged
Geometry.csinput
1 namespace Shapes;
2
3 public static class Geometry
4 {
5 public static string Label(Shape shape)
6 {
7 var name = shape.Name;
8 return name.ToUpperInvariant();
9 }
10 }
Shapes.csinput
1 namespace Shapes;
2
3 public abstract class Shape
4 {
5 public string Name { get; set; }
6 }
7
8 public sealed class Circle : Shape
9 {
10 public double Radius { get; set; }
11 }
12
13 public sealed class Square : Shape
14 {
15 public double Side { get; set; }
16 }

#nullable-is-patterns

Moves the branches of an if chain of is patterns into overrides, keeping the nullable return type

success
Targetsymbol M:Pets.Vet.Label(Pets.Pet)
Projectnullable enable
Pets.csmodified
1−using System;
2−
31 namespace Pets;
42
53 public abstract class Pet
64 {
75 public string? Nickname { get; set; }
6+
7+ public abstract string? Label();
88 }
99
1010 public sealed class Dog : Pet
1111 {
1212 public string Breed { get; set; } = "";
13+
14+ public override string? Label() => Nickname ?? Breed;
1315 }
1416
1517 public sealed class Cat : Pet
1618 {
1719 public bool Indoor { get; set; }
20+
21+ public override string? Label() => Indoor ? Nickname : null;
1822 }
1923
2024 public class Vet
2125 {
2226 public string? Label(Pet pet)
2327 {
24− if (pet is Dog dog)
25− return dog.Nickname ?? dog.Breed;
26− else if (pet is Cat cat)
27− return cat.Indoor ? cat.Nickname : null;
28− else
29− throw new NotSupportedException();
28+ return pet.Label();
3029 }
3130 }

#overload-with-parameters

Chooses one overload, passes its other parameters on, and keeps a default arm as a virtual method of a concrete base

success
Targetsymbol M:Shapes.Geometry.Describe(Shapes.Shape,System.String)
Geometry.csmodified
11 namespace Shapes;
22
33 public class Shape
44 {
5+ public virtual string Describe(string prefix) => prefix + " shape";
56 }
67
78 public class Circle : Shape
89 {
910 public double Radius { get; set; }
11+
12+ public override string Describe(string prefix) => prefix + " circle of radius " + Radius;
1013 }
1114
1215 public class Square : Shape
1316 {
1417 public double Side { get; set; }
18+
19+ public override string Describe(string prefix) => prefix + " square of side " + Side;
1520 }
1621
1722 public static class Geometry
1823 {
1924 public static string Describe(Shape shape) => Describe(shape, "a");
2025
21− public static string Describe(Shape shape, string prefix) => shape switch
22− {
23− Circle c => prefix + " circle of radius " + c.Radius,
24− Square s => prefix + " square of side " + s.Side,
25− _ => prefix + " shape",
26− };
26+ public static string Describe(Shape shape, string prefix) => shape.Describe(prefix);
2727 }

#subclass-without-case

Refuses when a subclass has no case and the only default throws, so it would not implement the abstract method

refusal
Targetsymbol M:Shapes.Geometry.Area(Shapes.Shape)
Refusessubclass-without-case mentioning “Triangle”; every file is left unchanged
Shapes.csinput
1 using System;
2
3 namespace Shapes;
4
5 public abstract class Shape
6 {
7 }
8
9 public sealed class Circle : Shape
10 {
11 public double Radius { get; set; }
12 }
13
14 public sealed class Square : Shape
15 {
16 public double Side { get; set; }
17 }
18
19 public sealed class Triangle : Shape
20 {
21 }
22
23 public static class Geometry
24 {
25 public static double Area(Shape shape) => shape switch
26 {
27 Circle c => Math.PI * c.Radius * c.Radius,
28 Square s => s.Side * s.Side,
29 _ => throw new ArgumentException("Unknown shape"),
30 };
31 }

#subclasses-in-other-files

Moves the cases of a switch statement into subclasses in their own files, keeps the default as a virtual method and makes the private field the cases use protected

success
Targetsymbol M:Staff.Employee.Bonus
Employee.csmodified
11 namespace Staff;
22
33 public enum EmployeeType
44 {
55 Engineer,
66 Salesman,
77 Manager
88 }
99
1010 public abstract class Employee
1111 {
12− private readonly int _salary;
12+ protected readonly int _salary;
1313
1414 protected Employee(int salary)
1515 {
1616 _salary = salary;
1717 }
1818
1919 public abstract EmployeeType Type { get; }
2020
2121 /// <summary>The bonus for the year.</summary>
22− public int Bonus()
23− {
24− switch (Type)
25− {
26− case EmployeeType.Engineer:
27− return 100;
28− case EmployeeType.Manager:
29− // Managers share in the salary pool.
30− var share = _salary / 10;
31− return share + 50;
32− default:
33− return 0;
34− }
35− }
22+ public virtual int Bonus() => 0;
3623 }
Engineer.csmodified
11 namespace Staff;
22
33 public sealed class Engineer : Employee
44 {
55 public Engineer(int salary) : base(salary)
66 {
77 }
88
99 public override EmployeeType Type => EmployeeType.Engineer;
10+
11+ public override int Bonus() => 100;
1012 }
Manager.csmodified
11 namespace Staff;
22
33 public sealed class Manager : Employee
44 {
55 public Manager(int salary) : base(salary)
66 {
77 }
88
99 public override EmployeeType Type
1010 {
1111 get { return EmployeeType.Manager; }
1212 }
13+
14+ public override int Bonus()
15+ {
16+ // Managers share in the salary pool.
17+ var share = _salary / 10;
18+ return share + 50;
19+ }
1320 }
Salesman.csunchanged
11 namespace Staff;
22
33 public sealed class Salesman : Employee
44 {
55 public Salesman(int salary) : base(salary)
66 {
77 }
88
99 public override EmployeeType Type => EmployeeType.Salesman;
1010 }

#switch-expression-on-type-code

Moves each arm of a switch on the type code into an override, leaving the method abstract when the only default throws

success
Targetsymbol M:Staff.Employee.Bonus
Employee.csmodified
1−using System;
2−
31 namespace Staff;
42
53 public enum EmployeeType
64 {
75 Engineer,
86 Salesman
97 }
108
119 public abstract class Employee
1210 {
1311 public abstract EmployeeType Type { get; }
1412
15− public int Bonus() => Type switch
16− {
17− EmployeeType.Engineer => 100,
18− EmployeeType.Salesman => 200,
19− _ => throw new ArgumentOutOfRangeException(),
20− };
13+ public abstract int Bonus();
2114 }
2215
2316 public sealed class Engineer : Employee
2417 {
2518 public override EmployeeType Type => EmployeeType.Engineer;
19+
20+ public override int Bonus() => 100;
2621 }
2722
2823 public sealed class Salesman : Employee
2924 {
3025 public override EmployeeType Type => EmployeeType.Salesman;
26+
27+ public override int Bonus() => 200;
3128 }

#type-patterns

Moves the cases of a switch on a parameter's type into an abstract method of the parameter's class, and delegates to it

success
Targetsymbol M:Shapes.Geometry.Area(Shapes.Shape)
Geometry.csmodified
1−using System;
2−
31 namespace Shapes;
42
53 public static class Geometry
64 {
75 public static double Area(Shape shape)
86 {
9− switch (shape)
10− {
11− case Circle c:
12− return Math.PI * c.Radius * c.Radius;
13− case Square s:
14− return s.Side * s.Side;
15− default:
16− throw new ArgumentException("Unknown shape", nameof(shape));
17− }
7+ return shape.Area();
188 }
199 }
Shapes.csmodified
1+using System;
2+
13 namespace Shapes;
24
35 public abstract class Shape
46 {
57 public string Name { get; set; }
8+
9+ public abstract double Area();
610 }
711
812 public sealed class Circle : Shape
913 {
1014 public double Radius { get; set; }
15+
16+ public override double Area() => Math.PI * Radius * Radius;
1117 }
1218
1319 public sealed class Square : Shape
1420 {
1521 public double Side { get; set; }
22+
23+ public override double Area() => Side * Side;
1624 }

#unsupported-case

Refuses a case guarded by a when clause, which does not belong to the whole subclass

refusal
Targetsymbol M:Shapes.Geometry.IsLarge(Shapes.Shape)
Refusesunsupported-case mentioning “when”; every file is left unchanged
Geometry.csinput
1 namespace Shapes;
2
3 public static class Geometry
4 {
5 public static bool IsLarge(Shape shape)
6 {
7 switch (shape)
8 {
9 case Circle c when c.Radius > 10:
10 return true;
11 case Square s:
12 return s.Side > 20;
13 default:
14 return false;
15 }
16 }
17 }
Shapes.csinput
1 namespace Shapes;
2
3 public abstract class Shape
4 {
5 public string Name { get; set; }
6 }
7
8 public sealed class Circle : Shape
9 {
10 public double Radius { get; set; }
11 }
12
13 public sealed class Square : Shape
14 {
15 public double Side { get; set; }
16 }

#uses-caller-members

Refuses a case that uses a member of the class declaring the method, which the subclass cannot reach

refusal
Targetsymbol M:Shapes.Geometry.Area(Shapes.Shape)
Refusesuses-caller-members mentioning “_scale”; every file is left unchanged
Geometry.csinput
1 namespace Shapes;
2
3 public class Geometry
4 {
5 private readonly double _scale = 2;
6
7 public double Area(Shape shape)
8 {
9 switch (shape)
10 {
11 case Circle c:
12 return 3 * c.Radius * c.Radius;
13 case Square s:
14 return s.Side * s.Side * _scale;
15 default:
16 return 0;
17 }
18 }
19 }
Shapes.csinput
1 namespace Shapes;
2
3 public abstract class Shape
4 {
5 public string Name { get; set; }
6 }
7
8 public sealed class Circle : Shape
9 {
10 public double Radius { get; set; }
11 }
12
13 public sealed class Square : Shape
14 {
15 public double Side { get; set; }
16 }