RefactorMCP catalog

Replace Parameter with Explicit Methods

Gives each value a method dispatches on a method of its own, and makes calls that pass the value as a constant call that method directly. SetValue("height", 10) becomes SetHeight(10).

Recipe

  1. extract-method on the statements of each value's branch, with name the explicit method's name. Extracting the last branch first leaves the methods in the order of their values. A return; or break; ending the branch stays in the branch.
  2. change-accessibility on each new method, giving it the dispatching method's accessibility.
  3. redirect-calls-with-constant-argument on the dispatching method for each value, with parameter, the value and the new method: each call that passes the value as a constant calls the value's method.
  4. safe-delete-member on the dispatching method once nothing calls it.
"steps": [
  { "refactoring": "extract-method", "target": { "file": "Box.cs", "selection": "marker" }, "arguments": { "name": "SetWidth" } },
  { "refactoring": "extract-method", "target": { "file": "Box.cs", "range": "14:17-14:33" }, "arguments": { "name": "SetHeight" } },
  { "refactoring": "change-accessibility", "target": { "symbol": "M:Shapes.Box.SetHeight(System.Int32)" }, "arguments": { "accessibility": "public" } },
  { "refactoring": "change-accessibility", "target": { "symbol": "M:Shapes.Box.SetWidth(System.Int32)" }, "arguments": { "accessibility": "public" } },
  { "refactoring": "redirect-calls-with-constant-argument", "target": { "symbol": "M:Shapes.Box.SetValue(System.String,System.Int32)" }, "arguments": { "parameter": "name", "value": "\"height\"", "method": "SetHeight" } },
  { "refactoring": "redirect-calls-with-constant-argument", "target": { "symbol": "M:Shapes.Box.SetValue(System.String,System.Int32)" }, "arguments": { "parameter": "name", "value": "\"width\"", "method": "SetWidth" } }
]

In the recipe case a call passing a variable remains, so step 4 does not apply. The dedicated implementation does all four steps as one change.

Arguments

ArgumentMeaning
parameterthe parameter the method dispatches on
methodsthe values to give methods of their own, each { "value": "\"height\"", "name": "SetHeight" }, the value written as C#

The target is the method, by symbol.

Precondition

  • The method is a block-bodied, non-virtual method of a class that neither overrides nor implements another.
  • Its body starts by choosing what to do from the parameter: a switch on it with one constant case label per section, or a run of if statements and else if chains comparing it with == to constants.
  • Each named value has a branch, which is not empty.
  • Each branch ends the method: it returns or throws, or nothing follows the dispatch.
  • Each branch meets Extract Method's precondition, and the class has no member with the new names.

Transformation

  • Each named value's branch becomes a method with the given name and the dispatching method's accessibility, taking the parameters the branch reads and returning what it returns. The branch now calls it.
  • Each call that passes one of the named values as a constant, and whose arguments have no side effects, calls that value's method on the same receiver, passing the arguments for its parameters.
  • The dispatching method is deleted once no reference to it remains.

Preserved

  • What each call does: a call passing a named value runs that value's branch, as the dispatch would have chosen it.
  • Calls that pass a variable, or another value, still reach the dispatching method, which runs the same code through the new methods.

Limitations

  • The dedicated implementation keeps any call whose arguments have side effects calling the dispatching method, since the explicit method may not take all of them. The recipe's redirect step is finer: it keeps only calls where an argument it would drop or reorder has side effects.
  • Values are matched by their constant value; a call passing a variable that happens to hold a value is not redirected.
  • A dispatch that does other work first, or a branch that runs on into later statements, is refused.

Error codes

CodeMeaning
not-a-dispatchthe body does not start by choosing from the parameter compared with constants
unknown-valuethe method has no branch for a named value
falls-througha named value's branch runs on into the rest of the method
empty-brancha named value's branch has no statements
no-valuesno value is named
polymorphic-methodthe method is virtual, an override or an interface implementation
not-in-classthe method is not a block-bodied method of a class
unknown-parameterthe method has no parameter of that name
assigned-local-used-aftera branch assigns a local read after it
name-conflictthe class already has a member with one of the names

Cases

·

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

#falls-through-rejected

Refuses a branch that carries on into statements after the dispatch, which its own method would not run

refusal
Targetsymbol M:Shapes.Box.SetValue(System.String,System.Int32)
Arguments
parameter"name"
methods[{"value": "\"height\"", "name": "SetHeight"}]
Refusesfalls-through; every file is left unchanged
Box.csinput
1 namespace Shapes
2 {
3 public class Box
4 {
5 private int _height;
6 private int _width;
7 private int _changes;
8
9 public void SetValue(string name, int value)
10 {
11 if (name == "height")
12 _height = value;
13 else if (name == "width")
14 _width = value;
15 _changes++;
16 }
17
18 public int Changes()
19 {
20 return _changes + _height + _width;
21 }
22 }
23 }

#if-chain

Each branch of an if chain on a string parameter becomes a public method; calls passing a constant call it, and a call passing a variable keeps the original method

success
Targetsymbol M:Shapes.Box.SetValue(System.String,System.Int32)
Arguments
parameter"name"
methods[{"value": "\"height\"", "name": "SetHeight"}, {"value": "\"width\"", "name": "SetWidth"}]
Box.csmodified
11 using System;
22
33 namespace Shapes
44 {
55 public class Box
66 {
77 private int _height;
88 private int _width;
99
1010 public void SetValue(string name, int value)
1111 {
1212 if (name == "height")
1313 {
14− _height = value;
14+ SetHeight(value);
1515 return;
1616 }
1717 if (name == "width")
1818 {
19− _width = value;
19+ SetWidth(value);
2020 return;
2121 }
2222 throw new ArgumentException("Unknown dimension " + name);
23+ }
24+
25+ public void SetHeight(int value)
26+ {
27+ _height = value;
28+ }
29+
30+ public void SetWidth(int value)
31+ {
32+ _width = value;
2333 }
2434
2535 public int Area()
2636 {
2737 return _height * _width;
2838 }
2939 }
3040 }
Builder.csmodified
11 namespace Shapes
22 {
33 public class Builder
44 {
55 public Box Build(string dimension, int size)
66 {
77 var box = new Box();
8− box.SetValue("height", 10);
9− box.SetValue("width", size * 2);
8+ box.SetHeight(10);
9+ box.SetWidth(size * 2);
1010 box.SetValue(dimension, size);
1111 return box;
1212 }
1313 }
1414 }

#not-a-dispatch-rejected

Refuses a method that does other work before choosing what to do from the parameter

refusal
Targetsymbol M:Shapes.Box.SetValue(System.String,System.Int32)
Arguments
parameter"name"
methods[{"value": "\"height\"", "name": "SetHeight"}]
Refusesnot-a-dispatch; every file is left unchanged
Box.csinput
1 using System;
2
3 namespace Shapes
4 {
5 public class Box
6 {
7 private int _height;
8
9 public void SetValue(string name, int value)
10 {
11 Console.WriteLine(name);
12 if (name == "height")
13 _height = value;
14 }
15
16 public int Height()
17 {
18 return _height;
19 }
20 }
21 }

#recipe

Runs the recipe: Extract Method on each branch, Change Accessibility on each new method, then Redirect Calls with Constant Argument for each value; the call passing a variable keeps the original method

successrecipe
Recipe
  1. extract-method on Box.cs, the /*[*/ … /*]*/ selection
    name"SetWidth"
  2. extract-method on Box.cs, range 14:17-14:33
    name"SetHeight"
  3. change-accessibility on symbol M:Shapes.Box.SetHeight(System.Int32)
    accessibility"public"
  4. change-accessibility on symbol M:Shapes.Box.SetWidth(System.Int32)
    accessibility"public"
  5. redirect-calls-with-constant-argument on symbol M:Shapes.Box.SetValue(System.String,System.Int32)
    parameter"name"
    value"\"height\""
    method"SetHeight"
  6. redirect-calls-with-constant-argument on symbol M:Shapes.Box.SetValue(System.String,System.Int32)
    parameter"name"
    value"\"width\""
    method"SetWidth"
Box.csmodified
11 using System;
22
33 namespace Shapes
44 {
55 public class Box
66 {
77 private int _height;
88 private int _width;
99
1010 public void SetValue(string name, int value)
1111 {
1212 if (name == "height")
1313 {
14− _height = value;
14+ SetHeight(value);
1515 return;
1616 }
1717 if (name == "width")
1818 {
19− /*[*/_width = value;/*]*/
19+ SetWidth(value);
2020 return;
2121 }
2222 throw new ArgumentException("Unknown dimension " + name);
23+ }
24+
25+ public void SetHeight(int value)
26+ {
27+ _height = value;
28+ }
29+
30+ public void SetWidth(int value)
31+ {
32+ _width = value;
2333 }
2434
2535 public int Area()
2636 {
2737 return _height * _width;
2838 }
2939 }
3040 }
Builder.csmodified
11 namespace Shapes
22 {
33 public class Builder
44 {
55 public Box Build(string dimension, int size)
66 {
77 var box = new Box();
8− box.SetValue("height", 10);
9− box.SetValue("width", size * 2);
8+ box.SetHeight(10);
9+ box.SetWidth(size * 2);
1010 box.SetValue(dimension, size);
1111 return box;
1212 }
1313 }
1414 }

#switch-on-enum

Sections of a switch on an enum parameter become methods returning the value; once every call passes a constant the original method is deleted

success
Targetsymbol M:Shipping.Rates.Cost(Shipping.Zone,System.Decimal)
Arguments
parameter"zone"
methods[{"value": "Zone.Domestic", "name": "DomesticCost"}, {"value": "Zone.Europe", "name": "EuropeCost"}]
Rates.csmodified
11 using System;
22
33 namespace Shipping
44 {
55 public enum Zone
66 {
77 Domestic,
88 Europe,
99 World,
1010 }
1111
1212 public class Rates
1313 {
14− public decimal Cost(Zone zone, decimal weight)
14+ public decimal DomesticCost(decimal weight)
1515 {
16− switch (zone)
17− {
18− case Zone.Domestic:
19− return weight * 1.5m;
20− case Zone.Europe:
21− return weight * 4m + 2m;
22− default:
23− throw new ArgumentOutOfRangeException(nameof(zone));
24− }
16+ return weight * 1.5m;
17+ }
18+
19+ public decimal EuropeCost(decimal weight)
20+ {
21+ return weight * 4m + 2m;
2522 }
2623
2724 public decimal Quote(decimal weight)
2825 {
29− return Cost(Zone.Domestic, weight) + Cost(Zone.Europe, 1m);
26+ return DomesticCost(weight) + EuropeCost(1m);
3027 }
3128 }
3229 }

#unknown-value-rejected

Refuses a value the method has no branch for

refusal
Targetsymbol M:Shapes.Box.SetValue(System.String,System.Int32)
Arguments
parameter"name"
methods[{"value": "\"depth\"", "name": "SetDepth"}]
Refusesunknown-value; every file is left unchanged
Box.csinput
1 using System;
2
3 namespace Shapes
4 {
5 public class Box
6 {
7 private int _height;
8 private int _width;
9
10 public void SetValue(string name, int value)
11 {
12 if (name == "height")
13 {
14 _height = value;
15 return;
16 }
17 if (name == "width")
18 {
19 _width = value;
20 return;
21 }
22 throw new ArgumentException("Unknown dimension " + name);
23 }
24
25 public int Area()
26 {
27 return _height * _width;
28 }
29 }
30 }
Builder.csinput
1 namespace Shapes
2 {
3 public class Builder
4 {
5 public Box Build(string dimension, int size)
6 {
7 var box = new Box();
8 box.SetValue("height", 10);
9 box.SetValue("width", size * 2);
10 box.SetValue(dimension, size);
11 return box;
12 }
13 }
14 }