RefactorMCP catalog

Collapse Hierarchy

Merges a class and its base class into one when they are no longer different enough to be kept apart: a subclass into its base class, or a base class into its only subclass.

Recipe

Merging a subclass into its base class:

  1. Pull Up Field and Pull Up Method for each member of the subclass: { "refactoring": "pull-up-field", "target": { "symbol": "F:Staff.Salesman._commission" } }, { "refactoring": "pull-up-method", "target": { "symbol": "M:Staff.Salesman.Bonus(System.Decimal)" } }.
  2. Change Type on each declaration that names the subclass, to the base class: { "refactoring": "change-type", "target": { "symbol": "M:Staff.Payroll.Pay(Staff.Salesman,System.Decimal)" }, "arguments": { "parameter": "salesman", "to": "Employee" } }.
  3. Safe Delete Type on the subclass: { "refactoring": "safe-delete-type", "target": { "symbol": "T:Staff.Salesman" } }.

Merging a base class into its subclass uses Push Down Field and Push Down Method instead, then Change Base Type on the subclass to the base class's own base.

The primitives have limits the dedicated implementation does not: Pull Up makes a private member protected, which the merged class does not need; properties and events have no pull-up primitive; and new Salesman() has no declaration that Change Type could retarget. The recipe case uses public and protected fields and methods, and a class only named by a parameter.

Target and arguments

The class to remove, by symbol: "target": { "symbol": "T:Staff.Salesman" }.

ArgumentRequiredMeaning
intonothe class to merge into: the base class (the default), or the only direct subclass

Precondition

For both directions:

  • Both classes are classes declared in the solution, and neither is static, generic or partial.
  • The class removed declares no constructor.
  • Nothing names the class removed in typeof or nameof, whose results would change.
  • The result compiles.

Merging a subclass into its base class, additionally:

  • The subclass is not abstract and has no subclasses of its own.
  • None of its members overrides another: moved up, it would replace what the base class does for its own instances.
  • No member of the base class, of a class above it, or of another subclass shares a name with a member of the subclass.
  • Nothing tests for the subclass or casts to it (is, as, a cast or a pattern), since every instance of the base class would then pass.

Merging a base class into its subclass, additionally:

  • into is the base class's only direct subclass.
  • The base class is never created itself, so every instance of it is already an instance of the subclass.
  • The subclass does not call the base class's members through base.
  • Every member of the subclass that shares a name with a member of the base class overrides it.

Transformation

  • The members of the class removed move into the class kept, with their documentation, comments and modifiers: fields after the last field, other members at the end. Private members stay private.
  • Merging into the subclass, abstract members of the base class are dropped, and so are virtual ones the subclass overrides. The subclass's overrides of them lose override (and sealed), becoming virtual when a class further down overrides them in turn; an override of a member the base class itself overrode stays an override.
  • Merging into the subclass, the subclass's base class becomes the removed class's own base, or none.
  • The interfaces the class removed implements are added to the class kept.
  • Every reference to the class removed, in any file, names the class kept: declarations, new, type arguments and base lists. Usings are added where the name needs them.
  • The class removed is deleted, with its file when nothing else is left in it.

Preserved

  • The behaviour of every member and every call: merging up, the base class's instances and other subclasses gain members they do not use; merging down, every instance was already of the subclass.

Limitations

  • Constructors are not merged; a class that declares one is refused.
  • Field initializers of members merged up run for every instance of the base class and its other subclasses, not only for instances of the subclass.
  • A generic class, or a subclass of a constructed generic class, is refused rather than having its type parameters substituted.

Error codes

CodeMeaning
unsupported-classa class is not a class, or is static, generic or partial
no-base-classinto is omitted and the class has no base class in the solution
not-relatedinto is neither the base class nor a direct subclass
several-subclassesthe base class has other subclasses besides into
has-subclassesthe subclass merged up has subclasses of its own
declares-constructorthe class removed declares a constructor
overrides-membera member of the subclass merged up overrides another
member-existsa member's name is already used in the class kept, above it or in another subclass
type-testedcode tests for, casts to, or takes typeof or nameof of the class removed
base-instantiatedthe base class merged down is created itself
uses-basethe subclass calls the base class's version of a member through base

Cases

·

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

#base-instantiated-rejected

A base class merged into a subclass must never be created itself, or its instances would gain the subclass's behaviour

refusal
Targetsymbol T:Staff.Employee
Arguments
into"Salesman"
Refusesbase-instantiated; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public string Name;
6 }
7 }
Office.csinput
1 namespace Staff
2 {
3 public class Office
4 {
5 public Employee Hire() => new Employee { Name = "new starter" };
6 }
7 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 protected decimal _commission = 0.1m;
6
7 public decimal Bonus(decimal sales) => sales * _commission;
8 }
9 }

#declares-constructor-rejected

A constructor of the removed class would have to be merged with the other class's construction

refusal
Targetsymbol T:Staff.Salesman
Refusesdeclares-constructor; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public string Name;
6 }
7 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 public Salesman()
6 {
7 Name = "sales";
8 }
9 }
10 }

#has-subclasses-rejected

A class merged into its base class must have no subclasses of its own

refusal
Targetsymbol T:Staff.Salesman
Refuseshas-subclasses; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public string Name;
6 }
7 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 protected decimal _commission = 0.1m;
6
7 public decimal Bonus(decimal sales) => sales * _commission;
8 }
9 }
SeniorSalesman.csinput
1 namespace Staff
2 {
3 public class SeniorSalesman : Salesman
4 {
5 }
6 }

#into-base

A subclass is merged into its base class: its members move up, every reference names the base class, and it is deleted

success
Targetsymbol T:Staff.Salesman
Employee.csmodified
11 namespace Staff
22 {
33 public class Employee
44 {
55 public string Name;
6+ protected decimal _commission = 0.1m;
7+
8+ public decimal Bonus(decimal sales) => sales * _commission;
69 }
710 }
Payroll.csmodified
11 namespace Staff
22 {
33 public class Payroll
44 {
5− public decimal Pay(Salesman salesman, decimal sales) => 1000m + salesman.Bonus(sales);
5+ public decimal Pay(Employee salesman, decimal sales) => 1000m + salesman.Bonus(sales);
66 }
77 }
Salesman.csdeleted
1−namespace Staff
2−{
3− public class Salesman : Employee
4− {
5− protected decimal _commission = 0.1m;
6−
7− public decimal Bonus(decimal sales) => sales * _commission;
8− }
9−}

#into-base-keeps-private-and-interfaces

Private members stay private, since the subclass is gone; construction and generic type arguments name the base class; the subclass's interfaces and usings move up with it

success
Targetsymbol T:Staff.Salesman
Arguments
into"Employee"
Employee.csmodified
1+using System;
2+using System.Collections.Generic;
3+
14 namespace Staff
25 {
3− public class Employee
6+ public class Employee : IComparable<Employee>
47 {
8+ private readonly List<decimal> _sales = new List<decimal>();
9+
510 public string Name { get; set; }
11+
12+ public void Record(decimal amount) => _sales.Add(amount);
13+
14+ public int CompareTo(Employee other) => _sales.Count.CompareTo(other._sales.Count);
615 }
716 }
Office.csmodified
11 using System.Collections.Generic;
22
33 namespace Staff
44 {
55 public class Office
66 {
7− private readonly List<Salesman> _team = new List<Salesman>();
7+ private readonly List<Employee> _team = new List<Employee>();
88
99 public void Hire(string name)
1010 {
11− var salesman = new Salesman { Name = name };
11+ var salesman = new Employee { Name = name };
1212 salesman.Record(0m);
1313 _team.Add(salesman);
1414 }
1515 }
1616 }
Salesman.csdeleted
1−using System;
2−using System.Collections.Generic;
3−
4−namespace Staff
5−{
6− public class Salesman : Employee, IComparable<Salesman>
7− {
8− private readonly List<decimal> _sales = new List<decimal>();
9−
10− public void Record(decimal amount) => _sales.Add(amount);
11−
12− public int CompareTo(Salesman other) => _sales.Count.CompareTo(other._sales.Count);
13− }
14−}

#into-subclass

An abstract base class is merged into its only subclass: abstract members go, the subclass's overrides become ordinary members, the rest move down, and every reference names the subclass

success
Targetsymbol T:Drawing.Shape
Arguments
into"Circle"
Canvas.csmodified
11 namespace Drawing
22 {
33 public class Canvas
44 {
5− public string Caption(Shape shape) => shape.Label();
5+ public string Caption(Circle shape) => shape.Label();
66
77 public string Draw() => Caption(new Circle { Radius = 2 });
88 }
99 }
Circle.csmodified
11 using System;
22
33 namespace Drawing
44 {
5− public class Circle : Shape
5+ public class Circle
66 {
77 public double Radius;
8+ protected string _name = "shape";
89
9− public override double Area() => Math.PI * Radius * Radius;
10+ public double Area() => Math.PI * Radius * Radius;
1011
11− public override string Describe() => "circle";
12+ public string Describe() => "circle";
13+
14+ public string Label() => Describe() + ": " + Area();
1215 }
1316 }
Shape.csdeleted
1−namespace Drawing
2−{
3− public abstract class Shape
4− {
5− protected string _name = "shape";
6−
7− public abstract double Area();
8−
9− public virtual string Describe() => _name;
10−
11− public string Label() => Describe() + ": " + Area();
12− }
13−}

#keeps-comments

Documentation and comments move with the members, and a subclass declared in the same file as its base is removed from it

success
Targetsymbol T:Staff.Salesman
Employee.csmodified
11 namespace Staff
22 {
33 public class Employee
44 {
55 public string Name { get; set; }
6− }
76
8− public class Salesman : Employee
9− {
107 // Paid on every sale.
118 public decimal Rate { get; set; } = 0.1m;
129
1310 /// <summary>The commission earned on <paramref name="sales"/>.</summary>
1411 public decimal Commission(decimal sales) => sales * Rate;
1512 }
1613 }

#member-exists-rejected

A member moving up must not share its name with a member of the base class or of another subclass

refusal
Targetsymbol T:Staff.Salesman
Refusesmember-exists; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public string Name;
6 }
7 }
Engineer.csinput
1 namespace Staff
2 {
3 public class Engineer : Employee
4 {
5 public decimal Bonus(decimal sales) => 0m;
6 }
7 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 protected decimal _commission = 0.1m;
6
7 public decimal Bonus(decimal sales) => sales * _commission;
8 }
9 }

#no-base-class-rejected

Without into, the class is merged into its base class, which it must have

refusal
Targetsymbol T:Staff.Employee
Refusesno-base-class; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public string Name;
6 }
7 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 protected decimal _commission = 0.1m;
6
7 public decimal Bonus(decimal sales) => sales * _commission;
8 }
9 }

#overrides-member-rejected

Moving an override up would replace what the base class itself does for its own instances

refusal
Targetsymbol T:Staff.Salesman
Refusesoverrides-member; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public virtual decimal Pay() => 1000m;
6 }
7 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 public override decimal Pay() => 1200m;
6 }
7 }

#recipe

The recipe: pull every member of the subclass up, change the parameter that named it to the base class, then delete it

successrecipe
Recipe
  1. pull-up-field on symbol F:Staff.Salesman._commission
  2. pull-up-method on symbol M:Staff.Salesman.Bonus(System.Decimal)
  3. change-type on symbol M:Staff.Payroll.Pay(Staff.Salesman,System.Decimal)
    parameter"salesman"
    to"Employee"
  4. safe-delete-type on symbol T:Staff.Salesman
Employee.csmodified
11 namespace Staff
22 {
33 public class Employee
44 {
55 public string Name;
6+ protected decimal _commission = 0.1m;
7+
8+ public decimal Bonus(decimal sales) => sales * _commission;
69 }
710 }
Payroll.csmodified
11 namespace Staff
22 {
33 public class Payroll
44 {
5− public decimal Pay(Salesman salesman, decimal sales) => 1000m + salesman.Bonus(sales);
5+ public decimal Pay(Employee salesman, decimal sales) => 1000m + salesman.Bonus(sales);
66 }
77 }
Salesman.csdeleted
1−namespace Staff
2−{
3− public class Salesman : Employee
4− {
5− protected decimal _commission = 0.1m;
6−
7− public decimal Bonus(decimal sales) => sales * _commission;
8− }
9−}

#several-subclasses-rejected

A base class merged into a subclass must have no other subclass, which would lose its base

refusal
Targetsymbol T:Staff.Employee
Arguments
into"Salesman"
Refusesseveral-subclasses; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public string Name;
6 }
7 }
Engineer.csinput
1 namespace Staff
2 {
3 public class Engineer : Employee
4 {
5 }
6 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 protected decimal _commission = 0.1m;
6
7 public decimal Bonus(decimal sales) => sales * _commission;
8 }
9 }

#type-tested-rejected

A type test for the subclass would hold for every instance of the base class once they are one class

refusal
Targetsymbol T:Staff.Salesman
Refusestype-tested; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public string Name;
6 }
7 }
Payroll.csinput
1 namespace Staff
2 {
3 public class Payroll
4 {
5 public decimal Pay(Employee employee, decimal sales) =>
6 employee is Salesman salesman ? 1000m + salesman.Bonus(sales) : 1000m;
7 }
8 }
Salesman.csinput
1 namespace Staff
2 {
3 public class Salesman : Employee
4 {
5 protected decimal _commission = 0.1m;
6
7 public decimal Bonus(decimal sales) => sales * _commission;
8 }
9 }

#unsupported-class-rejected

A generic class is not collapsed, since its type parameters would have to be substituted in every member

refusal
Targetsymbol T:Shop.OrderStore
Refusesunsupported-class; every file is left unchanged
Store.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Order
6 {
7 }
8
9 public class Store<TItem>
10 {
11 protected readonly List<TItem> _items = new List<TItem>();
12 }
13
14 public class OrderStore : Store<Order>
15 {
16 public int Count() => _items.Count;
17 }
18 }

#uses-base-rejected

An override that calls the base class's version through base has no base version once the classes are merged

refusal
Targetsymbol T:Drawing.Shape
Arguments
into"Circle"
Refusesuses-base; every file is left unchanged
Circle.csinput
1 namespace Drawing
2 {
3 public class Circle : Shape
4 {
5 public override string Describe() => "round " + base.Describe();
6 }
7 }
Shape.csinput
1 namespace Drawing
2 {
3 public abstract class Shape
4 {
5 public virtual string Describe() => "shape";
6 }
7 }