RefactorMCP catalog

Remove Middle Man

Removes the members of a class that only pass requests on to another object it holds, so that callers talk to that object directly. The reverse of Hide Delegate.

Recipe

  1. Inline Method on each method or property that only delegates, which replaces every call or read with the delegating expression reached through the caller's receiver, then deletes the member: { "refactoring": "inline-method", "target": { "symbol": "M:Company.Person.GetManager" } }, { "refactoring": "inline-method", "target": { "symbol": "P:Company.Person.Manager" } }.

The plan's recipe ends with Safe Delete Member; Inline Method already deletes the member once its uses are inlined, so that step has nothing left to do.

Target and arguments

The class that delegates, by symbol: "target": { "symbol": "T:Company.Person" }.

ArgumentRequiredMeaning
viayesthe field or property holding the delegate, such as Department

Precondition

  • The class has a field or property named via.
  • At least one member of the class only delegates through it:
  • a method whose body is one call of a member of the delegate, passing the method's own parameters in order (Department.Budget(year)), or one read of a member of it (Department.Manager);
  • a get-only property that reads a member of the delegate or calls one of its methods without arguments.
  • Wherever a delegating member is used, the delegate and the member it forwards to are accessible. A private delegate has to be exposed first, for example with Encapsulate Field.
  • Every use of a delegating method is a call; a method group has no call to redirect. The other preconditions of Inline Method hold for each method.

Transformation

  • Each use of a delegating member is replaced by what it forwards to, reached through the same receiver: person.GetManager() becomes person.Department.Manager, person.Budget(2024) becomes person.Department.Budget(2024), person?.Manager becomes person?.Department.Manager, and Manager inside the class becomes Department.Manager.
  • The delegating members are deleted.

Preserved

  • The behaviour of every use: the delegate is read and its member used exactly as the delegating member did.
  • Members of the class that do more than delegate, and members that callers reach through dispatch (virtual, abstract, overriding or implementing an interface), which are kept.

Limitations

  • Only members that forward their parameters unchanged and in order are recognised as delegating; one that adapts an argument is kept.
  • Static members, indexers and events are not considered.
  • A refusal from Inline Method on any delegating method refuses the whole change, so nothing is removed.

Error codes

CodeMeaning
via-not-foundthe class has no field or property named via
no-delegating-membersno member of the class only delegates through via
via-not-accessiblethe delegate or its member is not accessible where a delegating member is used
method-group-referencea delegating method is used without being called

Cases

·

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

#delegating-methods

Every method that only delegates through the named property is removed, and its callers use the delegate directly

success
Targetsymbol T:Company.Person
Arguments
via"Department"
Person.csmodified
11 namespace Company
22 {
33 public class Person
44 {
55 public Person(Department department)
66 {
77 Department = department;
88 }
99
1010 public Department Department { get; }
11−
12− public Employee GetManager()
13− {
14− return Department.Manager;
15− }
16−
17− public decimal Budget(int year) => Department.Budget(year);
1811 }
1912 }
Report.csmodified
11 namespace Company
22 {
33 public class Report
44 {
55 public string Describe(Person person)
66 {
7− var manager = person.GetManager();
8− return manager.Name + ": " + person.Budget(2024);
7+ var manager = person.Department.Manager;
8+ return manager.Name + ": " + person.Department.Budget(2024);
99 }
1010 }
1111 }
Department.csunchanged
11 namespace Company
22 {
33 public class Department
44 {
55 public Employee Manager { get; set; }
66
77 public decimal Budget(int year) => year * 1000m;
88 }
99
1010 public class Employee
1111 {
1212 public string Name { get; set; }
1313 }
1414 }

#delegating-properties

A property that only delegates is removed too, including uses inside the class and through null-conditional access; members that do more, and an override, are kept

success
Targetsymbol T:Company.Person
Arguments
via"Department"
Person.csmodified
11 namespace Company
22 {
33 public class Person
44 {
55 public Person(Department department)
66 {
77 Department = department;
88 }
99
1010 public Department Department { get; }
1111
12− public Employee Manager => Department.Manager;
13−
14− public string Code
15− {
16− get { return Department.Code; }
17− }
18−
19− public string ManagerName() => Manager.Name;
12+ public string ManagerName() => Department.Manager.Name;
2013
2114 public override string ToString() => Department.ToString();
2215 }
2316 }
Report.csmodified
11 namespace Company
22 {
33 public class Report
44 {
55 public string Describe(Person person)
66 {
7− return person.Code + " " + person?.Manager.Name;
7+ return person.Department.Code + " " + person?.Department.Manager.Name;
88 }
99 }
1010 }
Department.csunchanged
11 namespace Company
22 {
33 public class Department
44 {
55 public Employee Manager { get; set; }
66
77 public string Code { get; set; }
88 }
99
1010 public class Employee
1111 {
1212 public string Name { get; set; }
1313 }
1414 }

#method-group-rejected

A delegating method used as a method group has no call to redirect, and nothing is changed, including the members that could be removed

refusal
Targetsymbol T:Company.Person
Arguments
via"Department"
Refusesmethod-group-reference; every file is left unchanged
Department.csinput
1 namespace Company
2 {
3 public class Department
4 {
5 public Employee Manager { get; set; }
6
7 public decimal Budget(int year) => year * 1000m;
8 }
9
10 public class Employee
11 {
12 public string Name { get; set; }
13 }
14 }
Person.csinput
1 namespace Company
2 {
3 public class Person
4 {
5 public Person(Department department)
6 {
7 Department = department;
8 }
9
10 public Department Department { get; }
11
12 public Employee GetManager()
13 {
14 return Department.Manager;
15 }
16
17 public decimal Budget(int year) => Department.Budget(year);
18 }
19 }
Report.csinput
1 using System;
2
3 namespace Company
4 {
5 public class Report
6 {
7 public string Describe(Person person)
8 {
9 Func<Employee> manager = person.GetManager;
10 return manager().Name + ": " + person.Budget(2024);
11 }
12 }
13 }

#no-delegating-members-rejected

A class with no member that only delegates through the via has no middle man to remove

refusal
Targetsymbol T:Company.Person
Arguments
via"Department"
Refusesno-delegating-members; every file is left unchanged
Department.csinput
1 namespace Company
2 {
3 public class Department
4 {
5 public Employee Manager { get; set; }
6
7 public decimal Budget(int year) => year * 1000m;
8 }
9
10 public class Employee
11 {
12 public string Name { get; set; }
13 }
14 }
Person.csinput
1 namespace Company
2 {
3 public class Person
4 {
5 public Person(Department department)
6 {
7 Department = department;
8 }
9
10 public Department Department { get; }
11
12 public string Describe() => "Managed by " + Department.Manager.Name;
13 }
14 }

#recipe

The recipe: Inline Method on each method that only delegates, which rewrites its callers to use the delegate and deletes it

successrecipe
Recipe
  1. inline-method on symbol M:Company.Person.GetManager
  2. inline-method on symbol M:Company.Person.Budget(System.Int32)
Person.csmodified
11 namespace Company
22 {
33 public class Person
44 {
55 public Person(Department department)
66 {
77 Department = department;
88 }
99
1010 public Department Department { get; }
11−
12− public Employee GetManager()
13− {
14− return Department.Manager;
15− }
16−
17− public decimal Budget(int year) => Department.Budget(year);
1811 }
1912 }
Report.csmodified
11 namespace Company
22 {
33 public class Report
44 {
55 public string Describe(Person person)
66 {
7− var manager = person.GetManager();
8− return manager.Name + ": " + person.Budget(2024);
7+ var manager = person.Department.Manager;
8+ return manager.Name + ": " + person.Department.Budget(2024);
99 }
1010 }
1111 }
Department.csunchanged
11 namespace Company
22 {
33 public class Department
44 {
55 public Employee Manager { get; set; }
66
77 public decimal Budget(int year) => year * 1000m;
88 }
99
1010 public class Employee
1111 {
1212 public string Name { get; set; }
1313 }
1414 }

#recipe-delegating-properties

The recipe for properties: Inline Method on each property that only delegates, which rewrites its reads, including through ?., and deletes it

successrecipe
Recipe
  1. inline-method on symbol P:Company.Person.Manager
  2. inline-method on symbol P:Company.Person.Code
Person.csmodified
11 namespace Company
22 {
33 public class Person
44 {
55 public Person(Department department)
66 {
77 Department = department;
88 }
99
1010 public Department Department { get; }
1111
12− public Employee Manager => Department.Manager;
13−
14− public string Code
15− {
16− get { return Department.Code; }
17− }
18−
19− public string ManagerName() => Manager.Name;
12+ public string ManagerName() => Department.Manager.Name;
2013
2114 public override string ToString() => Department.ToString();
2215 }
2316 }
Report.csmodified
11 namespace Company
22 {
33 public class Report
44 {
55 public string Describe(Person person)
66 {
7− return person.Code + " " + person?.Manager.Name;
7+ return person.Department.Code + " " + person?.Department.Manager.Name;
88 }
99 }
1010 }
Department.csunchanged
11 namespace Company
22 {
33 public class Department
44 {
55 public Employee Manager { get; set; }
66
77 public string Code { get; set; }
88 }
99
1010 public class Employee
1111 {
1212 public string Name { get; set; }
1313 }
1414 }

#via-not-accessible-rejected

Callers outside the class cannot reach a private delegate; expose it first

refusal
Targetsymbol T:Company.Person
Arguments
via"_department"
Refusesvia-not-accessible; every file is left unchanged
Department.csinput
1 namespace Company
2 {
3 public class Department
4 {
5 public Employee Manager { get; set; }
6
7 public decimal Budget(int year) => year * 1000m;
8 }
9
10 public class Employee
11 {
12 public string Name { get; set; }
13 }
14 }
Person.csinput
1 namespace Company
2 {
3 public class Person
4 {
5 private readonly Department _department;
6
7 public Person(Department department)
8 {
9 _department = department;
10 }
11
12 public Employee GetManager() => _department.Manager;
13 }
14 }
Report.csinput
1 namespace Company
2 {
3 public class Report
4 {
5 public string Describe(Person person)
6 {
7 return person.GetManager().Name;
8 }
9 }
10 }

#via-not-found-rejected

The via must name a field or property of the class

refusal
Targetsymbol T:Company.Person
Arguments
via"Team"
Refusesvia-not-found; every file is left unchanged
Department.csinput
1 namespace Company
2 {
3 public class Department
4 {
5 public Employee Manager { get; set; }
6
7 public decimal Budget(int year) => year * 1000m;
8 }
9
10 public class Employee
11 {
12 public string Name { get; set; }
13 }
14 }
Person.csinput
1 namespace Company
2 {
3 public class Person
4 {
5 public Person(Department department)
6 {
7 Department = department;
8 }
9
10 public Department Department { get; }
11
12 public Employee GetManager()
13 {
14 return Department.Manager;
15 }
16
17 public decimal Budget(int year) => Department.Budget(year);
18 }
19 }
Report.csinput
1 namespace Company
2 {
3 public class Report
4 {
5 public string Describe(Person person)
6 {
7 var manager = person.GetManager();
8 return manager.Name + ": " + person.Budget(2024);
9 }
10 }
11 }