RefactorMCP catalog

Hide Delegate

Stops clients reaching through one object to another. When clients write person.Department.Manager, the person gains a Manager member that forwards to its department, and the clients write person.Manager, so they no longer depend on the department.

Recipe

For one client's chained call:

  1. extract-method on the client's statement that calls through the delegate: "target": { "file": "Payroll.cs", "selection": "marker" }, "arguments": { "name": "ApproveByManager" }. The new method takes the server object (person) as a parameter.
  2. move-instance-method the extracted method onto the server through that parameter, without a stub: "target": { "symbol": "M:Staff.Payroll.ApproveByManager(Staff.Person,Staff.Invoice)" }, "arguments": { "via": "person", "stub": false }. The call becomes person.ApproveByManager(invoice).

The plan's third step, repointing the other callers, has no primitive: each other client's chain would need its own Extract Method and would produce another method rather than reuse the first. The recipe therefore hides one chained statement, as an internal method since Extract Method makes it private. The dedicated implementation hides the delegate's member itself and repoints every client, so its cases describe a different result from the recipe's.

Arguments

ArgumentRequiredMeaning
memberyesThe delegate's field, property or method that clients reach, such as Manager
namenoThe forwarding member's name; defaults to member

The target is the server's field or property holding the delegate, by symbol: "target": { "symbol": "P:Staff.Person.Department" }.

Precondition

  • The target is an instance field or property.
  • The delegate's type, or a base type, has an instance field, property or method with the member's name.
  • A method to forward is not generic and has no ref, out, params or optional parameters.
  • The server and its base classes have no member with the forwarding name.
  • The result compiles, which fails when, for example, the forwarding member would be more accessible than the member's type.

Transformation

  • The server gains, right after the field or property holding the delegate, a member as accessible as that field or property: a read-only property public Employee Manager => Department.Manager; for a field or property, or public string Describe(string prefix) => Department.Describe(prefix); for each overload of a method.
  • Every read of the member through the delegate on an explicit receiver, x.Department.Manager, becomes x.Manager, and every call x.Department.Describe(a) becomes x.Describe(a), in every file.

Preserved

  • What every client reads and calls: the forwarding member reaches the same member of the same delegate.

Limitations

  • Assignments through the delegate, null-conditional access (person.Department?.Manager), method groups, and uses inside the server without a receiver are left as they were.
  • The forwarding member is get-only; it does not forward a setter.

Error codes

CodeMeaning
static-delegatethe field or property holding the delegate is static
member-not-foundthe delegate's type has no instance member of that name
name-conflictthe server already has a member with the forwarding name
unsupported-memberthe method to forward is generic or has ref, out, params or optional parameters
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.

#member-not-found-rejected

The department has no member called Budget

refusal
Targetsymbol P:Staff.Person.Department
Arguments
member"Budget"
Refusesmember-not-found; every file is left unchanged
Department.csinput
1 namespace Staff
2 {
3 public class Department
4 {
5 public Employee Manager { get; set; } = new Employee();
6 }
7
8 public class Employee
9 {
10 public string Name { get; set; } = "";
11
12 public void Approve(Invoice invoice) => invoice.Approved = true;
13 }
14
15 public class Invoice
16 {
17 public bool Approved { get; set; }
18 }
19 }
Payroll.csinput
1 namespace Staff
2 {
3 public class Payroll
4 {
5 public void Submit(Person person, Invoice invoice)
6 {
7 person.Department.Manager.Approve(invoice);
8 }
9 }
10 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public Department Department { get; set; } = new Department();
8 }
9 }

#method-delegate

Person gains a method under a new name that forwards to its department's Describe, and the client calling Describe through the department calls it instead; the other chain is left alone

success
Targetsymbol P:Staff.Person.Department
Arguments
member"Describe"
name"DescribeDepartment"
Payroll.csmodified
11 namespace Staff
22 {
33 public class Payroll
44 {
55 public void Submit(Person person, Invoice invoice)
66 {
77 person.Department.Manager.Approve(invoice);
88 }
99
10− public string Heading(Person person) => person.Department.Describe("Department of ") + ": " + person.Name;
10+ public string Heading(Person person) => person.DescribeDepartment("Department of ") + ": " + person.Name;
1111 }
1212 }
Person.csmodified
11 namespace Staff
22 {
33 public class Person
44 {
55 public string Name { get; set; } = "";
66
77 public Department Department { get; set; } = new Department();
8+
9+ public string DescribeDepartment(string prefix) => Department.Describe(prefix);
810 }
911 }
Department.csunchanged
11 namespace Staff
22 {
33 public class Department
44 {
55 public Employee Manager { get; set; } = new Employee();
66
77 public string Describe(string prefix) => prefix + Manager.Name;
88 }
99
1010 public class Employee
1111 {
1212 public string Name { get; set; } = "";
1313
1414 public void Approve(Invoice invoice) => invoice.Approved = true;
1515 }
1616
1717 public class Invoice
1818 {
1919 public bool Approved { get; set; }
2020 }
2121 }

#name-conflict-rejected

Person already has a member called Name, the name asked for the forwarding property

refusal
Targetsymbol P:Staff.Person.Department
Arguments
member"Manager"
name"Name"
Refusesname-conflict; every file is left unchanged
Department.csinput
1 namespace Staff
2 {
3 public class Department
4 {
5 public Employee Manager { get; set; } = new Employee();
6 }
7
8 public class Employee
9 {
10 public string Name { get; set; } = "";
11
12 public void Approve(Invoice invoice) => invoice.Approved = true;
13 }
14
15 public class Invoice
16 {
17 public bool Approved { get; set; }
18 }
19 }
Payroll.csinput
1 namespace Staff
2 {
3 public class Payroll
4 {
5 public void Submit(Person person, Invoice invoice)
6 {
7 person.Department.Manager.Approve(invoice);
8 }
9 }
10 }
Person.csinput
1 namespace Staff
2 {
3 public class Person
4 {
5 public string Name { get; set; } = "";
6
7 public Department Department { get; set; } = new Department();
8 }
9 }

#property-delegate

Person gains a Manager property forwarding to its department's manager, and every client that read the manager through the department uses it; an assignment through the department is left alone

success
Targetsymbol P:Staff.Person.Department
Arguments
member"Manager"
Payroll.csmodified
11 namespace Staff
22 {
33 public class Payroll
44 {
55 public void Submit(Person person, Invoice invoice)
66 {
7− person.Department.Manager.Approve(invoice);
7+ person.Manager.Approve(invoice);
88 }
99 }
1010 }
Person.csmodified
11 namespace Staff
22 {
33 public class Person
44 {
55 public string Name { get; set; } = "";
66
77 public Department Department { get; set; } = new Department();
8+
9+ public Employee Manager => Department.Manager;
810 }
911 }
Reception.csmodified
11 namespace Staff
22 {
33 public class Reception
44 {
5− public string Greet(Person person) => "Please ask " + person.Department.Manager.Name;
5+ public string Greet(Person person) => "Please ask " + person.Manager.Name;
66
77 public void Reorganise(Person person, Employee manager)
88 {
99 // Assigning through the department stays as it is.
1010 person.Department.Manager = manager;
1111 }
1212 }
1313 }
Department.csunchanged
11 namespace Staff
22 {
33 public class Department
44 {
55 public Employee Manager { get; set; } = new Employee();
66 }
77
88 public class Employee
99 {
1010 public string Name { get; set; } = "";
1111
1212 public void Approve(Invoice invoice) => invoice.Approved = true;
1313 }
1414
1515 public class Invoice
1616 {
1717 public bool Approved { get; set; }
1818 }
1919 }

#recipe

Extracts the client's call through the person's department into a method, then moves that method onto Person through the person parameter, as the recipe of primitive steps

successrecipe
Recipe
  1. extract-method on Payroll.cs, the /*[*/ … /*]*/ selection
    name"ApproveByManager"
  2. move-instance-method on symbol M:Staff.Payroll.ApproveByManager(Staff.Person,Staff.Invoice)
    via"person"
    stubfalse
Payroll.csmodified
11 namespace Staff
22 {
33 public class Payroll
44 {
55 public void Submit(Person person, Invoice invoice)
66 {
7− /*[*/person.Department.Manager.Approve(invoice);/*]*/
7+ person.ApproveByManager(invoice);
88 }
99 }
1010 }
Person.csmodified
11 namespace Staff
22 {
33 public class Person
44 {
55 public string Name { get; set; } = "";
66
77 public Department Department { get; set; } = new Department();
8+
9+ internal void ApproveByManager(Invoice invoice)
10+ {
11+ Department.Manager.Approve(invoice);
12+ }
813 }
914 }
Department.csunchanged
11 namespace Staff
22 {
33 public class Department
44 {
55 public Employee Manager { get; set; } = new Employee();
66 }
77
88 public class Employee
99 {
1010 public string Name { get; set; } = "";
1111
1212 public void Approve(Invoice invoice) => invoice.Approved = true;
1313 }
1414
1515 public class Invoice
1616 {
1717 public bool Approved { get; set; }
1818 }
1919 }

#static-delegate-rejected

A static property holds no per-instance delegate for a person to hide

refusal
Targetsymbol P:Staff.Person.Employer
Arguments
member"Name"
Refusesstatic-delegate; every file is left unchanged
Person.csinput
1 namespace Staff
2 {
3 public class Company
4 {
5 public string Name { get; set; } = "";
6 }
7
8 public class Person
9 {
10 public static Company Employer { get; set; } = new Company();
11 }
12
13 public class Badge
14 {
15 public string Print() => Person.Employer.Name;
16 }
17 }

#unsupported-member-rejected

The delegate's method has an out parameter, which a forwarding method is not generated for

refusal
Targetsymbol P:Staff.Person.Ledger
Arguments
member"TryBalance"
Refusesunsupported-member; every file is left unchanged
Person.csinput
1 namespace Staff
2 {
3 public class Ledger
4 {
5 public bool TryBalance(out decimal balance)
6 {
7 balance = 0;
8 return true;
9 }
10 }
11
12 public class Person
13 {
14 public Ledger Ledger { get; } = new Ledger();
15 }
16
17 public class Audit
18 {
19 public bool Check(Person person) => person.Ledger.TryBalance(out _);
20 }
21 }