RefactorMCP catalog

Preserve Whole Object

Replaces parameters that every call fills from members of one object with a single parameter taking the object, and reads the members in the body.

Recipe

  1. change-signature on the method: the new list replaces the parameters with one of the object's type, value is the object the call reads from, and replacements turns each use of a removed parameter into a read of the member it came from.
"steps": [
  {
    "refactoring": "change-signature",
    "target": { "symbol": "M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)" },
    "arguments": {
      "parameters": [ { "name": "range", "type": "TempRange", "value": "DaysTempRange" } ],
      "replacements": { "low": "range.Low", "high": "range.High" }
    }
  }
]

The plan's second step, rewriting parameter uses as member access, is Change Signature's replacements, so the recipe is one step. Change Signature passes one value to every call, so the recipe only describes calls that name the object the same way; the dedicated implementation passes each call the object that call read from.

Arguments

ArgumentMeaning
parametersthe parameters every call fills from members of one object
parameterNamethe name of the parameter that takes the object

The target is the method or constructor, by symbol.

Precondition

  • The method is called at least once.
  • At every call, each argument for a listed parameter reads a field or property of the same object, written as a name or a chain of member accesses, so evaluating it again has no side effects.
  • Every call fills each parameter from the same member, and passes objects of the same type.
  • No other parameter has the new parameter's name, and the listed parameters are never assigned in the body.
  • Change Signature's precondition holds, and the result compiles.

Transformation

  • The object's parameter takes the place of the first listed parameter, with the type the calls pass; the others are removed.
  • Each use of a removed parameter, in every body of the method's family, reads the member it was filled from.
  • Each call passes the object instead of the values it read from it.

Preserved

  • The values the body sees, as long as the members return the same value when read again in the body.
  • Other parameters and their arguments, with their comments and layout.

Limitations

  • The members are read when the body uses them rather than before the call, so a member that changes in between gives the new value.
  • Values read from this without a receiver, or computed from members, are refused rather than recognised as coming from the object.
  • The method gains a dependency on the object's type, which may not suit a method in another layer.

Error codes

CodeMeaning
not-from-one-objecta call passes a value that is not a member of an object
several-objectsa call reads its values from more than one object
members-differcalls fill a parameter from different members
types-differcalls pass objects of different types
no-callsnothing calls the method
unknown-parametera listed name is not a parameter of the method
duplicate-parameteranother parameter has the new parameter's name
replaced-parameter-assigneda listed parameter is assigned in the body
method-group-referencethe method is used as a method group
external-memberthe method overrides or implements a member outside the solution

Cases

·

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

#across-files

Takes the object a caller in another file reads both values from, as the recipe does

success
Targetsymbol M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)
Arguments
parameters["low", "high"]
parameterName"range"
HeatingPlan.csmodified
11 namespace Heating
22 {
33 public class TempRange
44 {
55 public TempRange(int low, int high)
66 {
77 Low = low;
88 High = high;
99 }
1010
1111 public int Low { get; }
1212
1313 public int High { get; }
1414 }
1515
1616 public class HeatingPlan
1717 {
1818 private readonly TempRange _allowed;
1919
2020 public HeatingPlan(TempRange allowed)
2121 {
2222 _allowed = allowed;
2323 }
2424
25− public bool WithinRange(int low, int high)
25+ public bool WithinRange(TempRange range)
2626 {
27− return low >= _allowed.Low && high <= _allowed.High;
27+ return range.Low >= _allowed.Low && range.High <= _allowed.High;
2828 }
2929 }
3030 }
Room.csmodified
11 namespace Heating
22 {
33 public class Room
44 {
55 public Room(TempRange daysTempRange)
66 {
77 DaysTempRange = daysTempRange;
88 }
99
1010 public TempRange DaysTempRange { get; }
1111
1212 public bool IsComfortable(HeatingPlan plan)
1313 {
14− return plan.WithinRange(DaysTempRange.Low, DaysTempRange.High);
14+ return plan.WithinRange(DaysTempRange);
1515 }
1616 }
1717 }

#members-differ-rejected

Refuses when calls fill a parameter from different members, since the body could read only one of them

refusal
Targetsymbol M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)
Arguments
parameters["low", "high"]
parameterName"range"
Refusesmembers-differ; every file is left unchanged
HeatingPlan.csinput
1 namespace Heating
2 {
3 public record TempRange(int Low, int High);
4
5 public class HeatingPlan
6 {
7 public bool WithinRange(int low, int high)
8 {
9 return low >= 16 && high <= 24;
10 }
11
12 public bool Check(TempRange range)
13 {
14 return WithinRange(range.Low, range.High) && WithinRange(range.High, range.High);
15 }
16 }
17 }

#no-calls-rejected

Refuses a method nothing calls, since no call shows which object the values come from

refusal
Targetsymbol M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)
Arguments
parameters["low", "high"]
parameterName"range"
Refusesno-calls; every file is left unchanged
HeatingPlan.csinput
1 namespace Heating
2 {
3 public class HeatingPlan
4 {
5 public bool WithinRange(int low, int high)
6 {
7 return low >= 16 && high <= 24;
8 }
9 }
10 }

#not-from-one-object-rejected

Refuses when a call passes a value that is not read from a member of an object

refusal
Targetsymbol M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)
Arguments
parameters["low", "high"]
parameterName"range"
Refusesnot-from-one-object mentioning “'high'”; every file is left unchanged
HeatingPlan.csinput
1 namespace Heating
2 {
3 public record TempRange(int Low, int High);
4
5 public class HeatingPlan
6 {
7 public bool WithinRange(int low, int high)
8 {
9 return low >= 16 && high <= 24;
10 }
11
12 public bool Check(TempRange range)
13 {
14 return WithinRange(range.Low, 22);
15 }
16 }
17 }

#recipe

Runs the recipe: Change Signature to take the object the call reads the values from, replacing each use of a removed parameter with a read of its member

successrecipe
Recipe
  1. change-signature on symbol M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)
    parameters[{"name": "range", "type": "TempRange", "value": "DaysTempRange"}]
    replacements{"low": "range.Low", "high": "range.High"}
HeatingPlan.csmodified
11 namespace Heating
22 {
33 public class TempRange
44 {
55 public TempRange(int low, int high)
66 {
77 Low = low;
88 High = high;
99 }
1010
1111 public int Low { get; }
1212
1313 public int High { get; }
1414 }
1515
1616 public class HeatingPlan
1717 {
1818 private readonly TempRange _allowed;
1919
2020 public HeatingPlan(TempRange allowed)
2121 {
2222 _allowed = allowed;
2323 }
2424
25− public bool WithinRange(int low, int high)
25+ public bool WithinRange(TempRange range)
2626 {
27− return low >= _allowed.Low && high <= _allowed.High;
27+ return range.Low >= _allowed.Low && range.High <= _allowed.High;
2828 }
2929 }
3030 }
Room.csmodified
11 namespace Heating
22 {
33 public class Room
44 {
55 public Room(TempRange daysTempRange)
66 {
77 DaysTempRange = daysTempRange;
88 }
99
1010 public TempRange DaysTempRange { get; }
1111
1212 public bool IsComfortable(HeatingPlan plan)
1313 {
14− return plan.WithinRange(DaysTempRange.Low, DaysTempRange.High);
14+ return plan.WithinRange(DaysTempRange);
1515 }
1616 }
1717 }

#recipe-without-replacements-rejected

The recipe's Change Signature refuses to remove parameters the body reads when no replacements are given, and nothing changes

refusalrecipe
Recipe
  1. change-signature on symbol M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)
    parameters[{"name": "range", "type": "TempRange", "value": "DaysTempRange"}]
Refusesremoved-parameter-in-use mentioning “'low'”; every file is left unchanged
HeatingPlan.csinput
1 namespace Heating
2 {
3 public class TempRange
4 {
5 public TempRange(int low, int high)
6 {
7 Low = low;
8 High = high;
9 }
10
11 public int Low { get; }
12
13 public int High { get; }
14 }
15
16 public class HeatingPlan
17 {
18 private readonly TempRange _allowed;
19
20 public HeatingPlan(TempRange allowed)
21 {
22 _allowed = allowed;
23 }
24
25 public bool WithinRange(int low, int high)
26 {
27 return low >= _allowed.Low && high <= _allowed.High;
28 }
29 }
30 }
Room.csinput
1 namespace Heating
2 {
3 public class Room
4 {
5 public Room(TempRange daysTempRange)
6 {
7 DaysTempRange = daysTempRange;
8 }
9
10 public TempRange DaysTempRange { get; }
11
12 public bool IsComfortable(HeatingPlan plan)
13 {
14 return plan.WithinRange(DaysTempRange.Low, DaysTempRange.High);
15 }
16 }
17 }

#several-callers

Each call passes its own object, and a parameter that is not replaced keeps its place

success
Targetsymbol M:Heating.HeatingPlan.Describe(System.String,System.Int32,System.Int32)
Arguments
parameters["low", "high"]
parameterName"range"
HeatingPlan.csmodified
11 namespace Heating
22 {
33 public record TempRange(int Low, int High);
44
55 public class Room
66 {
77 public TempRange DaysTempRange { get; set; } = new TempRange(18, 21);
88 }
99
1010 public class HeatingPlan
1111 {
12− public string Describe(string label, int low, int high)
12+ public string Describe(string label, TempRange range)
1313 {
14− return label + ": " + low + " to " + high + " (" + (high - low) + " degrees)";
14+ return label + ": " + range.Low + " to " + range.High + " (" + (range.High - range.Low) + " degrees)";
1515 }
1616
1717 public string Report(Room room, TempRange forecast)
1818 {
19− var today = Describe("today", room.DaysTempRange.Low, room.DaysTempRange.High);
20− return today + "; " + Describe("forecast", forecast.Low, forecast.High);
19+ var today = Describe("today", room.DaysTempRange);
20+ return today + "; " + Describe("forecast", forecast);
2121 }
2222 }
2323 }

#several-objects-rejected

Refuses when a call reads its values from two different objects

refusal
Targetsymbol M:Heating.HeatingPlan.WithinRange(System.Int32,System.Int32)
Arguments
parameters["low", "high"]
parameterName"range"
Refusesseveral-objects; every file is left unchanged
HeatingPlan.csinput
1 namespace Heating
2 {
3 public record TempRange(int Low, int High);
4
5 public class HeatingPlan
6 {
7 public bool WithinRange(int low, int high)
8 {
9 return low >= 16 && high <= 24;
10 }
11
12 public bool Check(TempRange morning, TempRange evening)
13 {
14 return WithinRange(morning.Low, evening.High);
15 }
16 }
17 }