RefactorMCP catalog

Replace Delegation with Inheritance

For a class that holds an object of another class and forwards most of its members to it: the class derives from the other class instead, the members that only forwarded are removed so the inherited ones take their place, and the field goes.

Recipe

  1. change-base-type the class to the field's class: "target": { "symbol": "T:Staff.Employee" }, "arguments": { "to": "Person" }.
  2. replace-field-uses-with-base, so uses of the field's members reach the inherited members and the field goes: "target": { "symbol": "T:Staff.Employee" }, "arguments": { "field": "_person" }. A forwarding member now reads get => base.Name; or return base.LastName();.
  3. remove-delegating-member for each member that now only forwards to the inherited one: "target": { "symbol": "P:Staff.Employee.Name" }, then "target": { "symbol": "M:Staff.Employee.LastName" }. Uses written with base only because the removed member hid the inherited one become plain uses.

The plan's recipe, Change Base Type, Inline Method for each delegating member, Inline Field, cannot preserve behaviour: Inline Method would put the private field into callers outside the class, and a field holding an object is not inlined. Turning the field into the instance itself first, then removing the members that forward to the inherited ones, does the same job one safe step at a time.

Arguments

ArgumentRequiredMeaning
fieldyesThe private field holding the object the class delegates to

The target is the class, by symbol: "target": { "symbol": "T:Staff.Employee" }.

Precondition

  • The target is a single, non-static class declaration, not a record, that derives only from object.
  • The field's class is a class that is neither sealed nor static.
  • The field is a private instance field, initialised with a new instance of its own class created without arguments or an object initializer, and declared on its own.
  • The field is never assigned after its initialiser, and every use of it is followed by a member access (_person.Name), so no code holds the object itself.
  • No member the class keeps shares a name with an accessible member of the field's class, which it would hide.
  • The result compiles.

Transformation

  • The field's class becomes the class's base class, first in its base list.
  • A member of the class is removed when all it does is forward to the member of the field with the same name, signature and accessibility: a method whose body is _person.M(a, b) passing its own parameters in order, or a property whose getter reads _person.P and whose setter, if any, assigns _person.P = value.
  • Every other use of the field becomes a use of the instance: _person.Greeting("x") becomes Greeting("x"), keeping this. only where a local or parameter would otherwise hide the member.
  • The field is removed with its comments.

Preserved

  • Every call of a removed forwarding member, which now reaches the inherited member it forwarded to.
  • The behaviour of the class's other members, given that the field's object was only ever used through its members.

Limitations

  • Names in the class that bound to something else, such as an extension method or a type, can bind to a newly inherited member of the same name; this is not checked.
  • Other code can now convert the class to its new base class; nothing relied on that before, so it changes no existing behaviour.
  • A field whose object needs constructor arguments is refused rather than turned into a base(...) call.

Error codes

CodeMeaning
field-not-foundthe class has no field of that name
not-a-classthe target is not a single, non-static class declaration
has-base-classthe class already derives from another class
invalid-base-typethe field's class is sealed, static or not a class
field-not-privatethe field is not a private instance field
not-created-by-fieldthe field is not initialised with a new instance of its class
field-assignedthe field is assigned after its initialiser
field-escapesthe field is used as a value rather than through its members
hides-base-membera member the class keeps would hide a member of the new base class
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.

#field-assigned-rejected

The field can be pointed at another person, which an employee cannot do to itself

refusal
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Refusesfield-assigned; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 private Person _person = new Person();
6
7 public string LastName() => _person.LastName();
8
9 public void Become(Person person) => _person = person;
10 }
11 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }

#field-escapes-rejected

The person is handed to other code, which would receive the employee itself instead

refusal
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Refusesfield-escapes; every file is left unchanged
Employee.csinput
1 using System.Collections.Generic;
2
3 namespace Staff
4 {
5 public class Employee
6 {
7 private readonly Person _person = new Person();
8
9 public string LastName() => _person.LastName();
10
11 public void Register(List<Person> people) => people.Add(_person);
12 }
13 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }

#field-not-private-rejected

The field is public, so code outside the class may reach the person through it

refusal
Targetsymbol T:Staff.Employee
Arguments
field"Person"
Refusesfield-not-private; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 public readonly Person Person = new Person();
6
7 public string LastName() => Person.LastName();
8 }
9 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }

#forwarding-members-removed

Employee derives from Person instead of holding one; the name property and LastName method that only forwarded are removed, and the badge calls the inherited members

success
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Employee.csmodified
11 namespace Staff
22 {
3− public class Employee
3+ public class Employee : Person
44 {
5− // The person this employee is.
6− private readonly Person _person = new Person();
7−
85 public decimal Salary { get; set; }
96
10− public string Name
11− {
12− get => _person.Name;
13− set => _person.Name = value;
14− }
15−
16− public string LastName()
17− {
18− return _person.LastName();
19− }
20−
21− public string Badge() => _person.Greeting("Employee") + " (" + this._person.LastName() + ")";
7+ public string Badge() => Greeting("Employee") + " (" + LastName() + ")";
228 }
239 }
Payroll.csunchanged
11 namespace Staff
22 {
33 public class Payroll
44 {
55 public string Slip(Employee employee)
66 {
77 employee.Name = "Ann Lee";
88 return employee.LastName() + ": " + employee.Salary + " " + employee.Badge();
99 }
1010 }
1111 }
Person.csunchanged
11 namespace Staff
22 {
33 public class Person
44 {
55 public string Name { get; set; } = "";
66
77 public string LastName() => Name.Split(' ')[^1];
88
99 public string Greeting(string salutation) => salutation + " " + Name;
1010 }
1111 }

#has-base-class-rejected

Employee already derives from Worker, and cannot derive from Person as well

refusal
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Refuseshas-base-class; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Worker
4 {
5 public decimal Salary { get; set; }
6 }
7
8 public class Employee : Worker
9 {
10 private readonly Person _person = new Person();
11
12 public string LastName() => _person.LastName();
13 }
14 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }

#hides-base-member-rejected

Employee's Greeting does something of its own and would hide the Greeting it inherits

refusal
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Refuseshides-base-member mentioning “Greeting”; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 private readonly Person _person = new Person();
6
7 public string LastName() => _person.LastName();
8
9 public string Greeting(string salutation) => salutation + " " + _person.LastName();
10 }
11 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }

#not-a-class-rejected

A struct cannot derive from a class

refusal
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Refusesnot-a-class; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public struct Employee
4 {
5 private readonly Person _person = new Person();
6
7 public Employee()
8 {
9 }
10
11 public string LastName() => _person.LastName();
12 }
13 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }

#not-created-by-field-rejected

The person is passed in rather than created by the field, so it may be shared with other code

refusal
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Refusesnot-created-by-field; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 private readonly Person _person;
6
7 public Employee(Person person)
8 {
9 _person = person;
10 }
11
12 public string LastName() => _person.LastName();
13 }
14 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }

#recipe

The recipe: derive from Person, reach the field's members through the instance and remove the field, then remove each member that now only forwards to the inherited one

successrecipe
Recipe
  1. change-base-type on symbol T:Staff.Employee
    to"Person"
  2. replace-field-uses-with-base on symbol T:Staff.Employee
    field"_person"
  3. remove-delegating-member on symbol P:Staff.Employee.Name
  4. remove-delegating-member on symbol M:Staff.Employee.LastName
Employee.csmodified
11 namespace Staff
22 {
3− public class Employee
3+ public class Employee : Person
44 {
5− // The person this employee is.
6− private readonly Person _person = new Person();
7−
85 public decimal Salary { get; set; }
96
10− public string Name
11− {
12− get => _person.Name;
13− set => _person.Name = value;
14− }
15−
16− public string LastName()
17− {
18− return _person.LastName();
19− }
20−
21− public string Badge() => _person.Greeting("Employee") + " (" + this._person.LastName() + ")";
7+ public string Badge() => Greeting("Employee") + " (" + LastName() + ")";
228 }
239 }
Payroll.csunchanged
11 namespace Staff
22 {
33 public class Payroll
44 {
55 public string Slip(Employee employee)
66 {
77 employee.Name = "Ann Lee";
88 return employee.LastName() + ": " + employee.Salary + " " + employee.Badge();
99 }
1010 }
1111 }
Person.csunchanged
11 namespace Staff
22 {
33 public class Person
44 {
55 public string Name { get; set; } = "";
66
77 public string LastName() => Name.Split(' ')[^1];
88
99 public string Greeting(string salutation) => salutation + " " + Name;
1010 }
1111 }

#sealed-delegate-rejected

Person is sealed, so Employee cannot derive from it

refusal
Targetsymbol T:Staff.Employee
Arguments
field"_person"
Refusesinvalid-base-type; every file is left unchanged
Employee.csinput
1 namespace Staff
2 {
3 public class Employee
4 {
5 private readonly Person _person = new Person();
6
7 public string LastName() => _person.LastName();
8 }
9 }
Person.csinput
1 namespace Staff
2 {
3 public sealed class Person
4 {
5 public string Name { get; set; } = "";
6
7 public string LastName() => Name.Split(' ')[^1];
8
9 public string Greeting(string salutation) => salutation + " " + Name;
10 }
11 }