RefactorMCP catalog

Replace Method with Method Object

Turns a long method whose locals get in the way of Extract Method into an object of its own. The instance, each parameter and each local become fields of a new class, so the body can then be split into methods that share them without passing them around. The old method creates one of these objects for each call and runs it.

Recipe

The plan's recipe, "Create Type, Convert Local to Field and move parameters to fields, Move Instance Method", cannot be carried out with primitives: no primitive turns a parameter into a field set by a constructor, or makes the old method create a new object on each call. The recipe that the primitives can carry out holds the object in a field of the old class instead:

  1. create-type the class: { "name": "PriceCalculation", "file": "PriceCalculation.cs" }.
  2. introduce-field of the class on the method's class, holding a new instance: "target": { "symbol": "T:Shop.Order" }, "arguments": { "type": "PriceCalculation", "name": "_priceCalculation" }.
  3. move-instance-method the method through it: "target": { "symbol": "M:Shop.Order.Price(System.Int32,System.Decimal)" }, "arguments": { "via": "_priceCalculation" }.
  4. convert-local-to-field each local of the moved method: "target": { "symbol": "M:Shop.PriceCalculation.Price(Shop.Order,System.Int32,System.Decimal)" }, "arguments": { "local": "basePrice" }.

This preserves the behaviour of calls that do not overlap, but every call on one instance shares the object, so recursive or concurrent calls share its fields, and the parameters stay parameters. The dedicated implementation does what the refactoring intends instead, and its cases describe that result rather than the recipe's.

Arguments

ArgumentRequiredMeaning
nameyesThe new class's name
methodnoThe name of the new class's method that runs the body; defaults to Compute
filenoThe new class's file, relative to the solution; defaults to <name>.cs beside the method's file

The target is the method, by symbol.

Precondition

  • The method has one body: it is not abstract, extern or partial.
  • Neither the method nor any class containing it is generic.
  • An instance method belongs to a class, not a struct, whose copy the object would otherwise work on.
  • No parameter is ref, out or in.
  • The body does not call through base, and uses no protected member.
  • No type has the new class's name in the namespace, and its file does not exist.
  • No two parameters or converted locals share a name, no other name the body declares, such as a lambda parameter, is the name of one of the new fields, and no parameter has the name the constructor gives the instance.

Transformation

  • The new class is in the method's namespace, public when the method's class is, internal otherwise. It has, in order:
  • a private readonly field for the instance, named _ and the class name in camel case, when the body uses the instance;
  • a private field for each parameter, named _ and the parameter's name, readonly unless the body assigns the parameter;
  • a private field for each local declared by a plain declaration statement of the method (not const, using or ref, and not captured by a lambda or local function), nullable in a nullable context when its type is a reference type, since it holds nothing until the body sets it;
  • a public constructor taking the instance and the parameters, assigning each field;
  • a public method with the old return type, async when the old method was, holding the old body, block or expression.
  • In the body, parameters and converted locals become their fields, declarations of converted locals become assignments (or disappear when they have no value, leaving their comments), this and the instance's members go through the instance field (_order._discountRate), and static members of the old class are qualified by it (Tree.Deeper). A recursive call reaches the old method through the instance, creating a new object.
  • The old method keeps its signature and documentation, loses async, and its body becomes return new PriceCalculation(this, quantity, itemPrice).Compute();, without return for a void method, or the same expression for an expression-bodied one.
  • Private members of the old class the body uses become internal, and the old file drops usings only the body needed.

Preserved

  • The result and effects of every call: each call gets its own object, so calls never share the fields.
  • Comments inside the body, which move with it.

Limitations

  • Private members become internal rather than being passed in.
  • Locals declared by for, foreach, using, patterns and out variables, and locals a lambda captures, stay locals.
  • An async or iterator method's body runs in the new method, so the old method returns the new method's task or sequence directly.

Error codes

CodeMeaning
type-already-existsa type has the new class's name
partial-methodthe method is partial
method-type-parameterthe method is generic
generic-typethe method's class, or a class containing it, is generic
not-a-classan instance method belongs to a struct
by-reference-parametera parameter is ref, out or in
uses-basethe body calls through base
uses-protected-memberthe body uses a protected member
repeated-local-nametwo parameters or locals to become fields share a name
name-conflicta name the body declares would hide one of the new fields
instance-parameter-conflicta parameter already has the name the constructor gives the instance

Cases

·

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

#by-reference-parameter-rejected

An out parameter cannot be held in a field and written back to the caller

refusal
Targetsymbol M:Shop.Parser.TryParse(System.String,System.Int32@)
Arguments
name"Parsing"
Refusesby-reference-parameter mentioning “value”; every file is left unchanged
Parser.csinput
1 namespace Shop
2 {
3 public class Parser
4 {
5 public bool TryParse(string text, out int value)
6 {
7 value = text.Length;
8 return value > 0;
9 }
10 }
11 }

#expression-bodied-recursive

An expression-bodied method keeps its form; its recursive call goes through the instance, and the private members it uses become internal

success
Targetsymbol M:Shop.Tree.Depth(Shop.Node)
Arguments
name"DepthSearch"
DepthSearch.csadded
1+using System.Linq;
2+
3+namespace Shop
4+{
5+ public class DepthSearch
6+ {
7+ private readonly Tree _tree;
8+ private readonly Node _node;
9+
10+ public DepthSearch(Tree tree, Node node)
11+ {
12+ _tree = tree;
13+ _node = node;
14+ }
15+
16+ public int Compute() => _node.Children.Count == 0 ? _tree._rootDepth : Tree.Deeper(_node.Children.Max(child => _tree.Depth(child)));
17+ }
18+}
Tree.csmodified
11 using System.Collections.Generic;
2−using System.Linq;
32
43 namespace Shop
54 {
65 public class Node
76 {
87 public List<Node> Children { get; } = new List<Node>();
98 }
109
1110 public class Tree
1211 {
13− private readonly int _rootDepth = 1;
12+ internal readonly int _rootDepth = 1;
1413
15− private static int Deeper(int depth) => depth + 1;
14+ internal static int Deeper(int depth) => depth + 1;
1615
17− public int Depth(Node node) => node.Children.Count == 0 ? _rootDepth : Deeper(node.Children.Max(child => Depth(child)));
16+ public int Depth(Node node) => new DepthSearch(this, node).Compute();
1817 }
1918 }

#generic-type-rejected

The method's class is generic, so the method object would need its type parameters

refusal
Targetsymbol M:Shop.Box`1.Describe(System.String)
Arguments
name"Description"
Refusesgeneric-type; every file is left unchanged
Box.csinput
1 namespace Shop
2 {
3 public class Box<T>
4 {
5 public T Item { get; set; }
6
7 public string Describe(string label)
8 {
9 string text = label + ": " + Item;
10 return text;
11 }
12 }
13 }

#instance-parameter-conflict-rejected

The method already has a parameter named order, the name the constructor would give the instance

refusal
Targetsymbol M:Shop.Order.Difference(Shop.Order)
Arguments
name"Comparison"
Refusesinstance-parameter-conflict; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 public decimal Total { get; set; }
6
7 public decimal Difference(Order order)
8 {
9 decimal difference = Total - order.Total;
10 return difference;
11 }
12 }
13 }

#method-type-parameter-rejected

A generic method's type parameters would have to become the method object's

refusal
Targetsymbol M:Shop.Store.First``1(System.Collections.Generic.List{``0})
Arguments
name"FirstSearch"
Refusesmethod-type-parameter; every file is left unchanged
Store.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Store
6 {
7 public T First<T>(List<T> items)
8 {
9 T first = items[0];
10 return first;
11 }
12 }
13 }

#name-conflict-rejected

A lambda in the method has a parameter named _quantity, which would hide the field the quantity parameter becomes

refusal
Targetsymbol M:Shop.Order.Price(System.Int32,System.Decimal)
Arguments
name"PriceCalculation"
Refusesname-conflict mentioning “_quantity”; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private decimal _discountRate = 0.1m;
6
7 /// <summary>The price of a line of this order.</summary>
8 public decimal Price(int quantity, decimal itemPrice)
9 {
10 // Large orders earn the discount.
11 decimal basePrice = quantity * itemPrice;
12 System.Func<int, decimal> scaled = _quantity => _quantity * itemPrice;
13 decimal discount = basePrice > 1000 ? basePrice * _discountRate : 0;
14 return basePrice - discount;
15 }
16 }
17 }

#not-a-class-rejected

An instance method of a struct would run against a copy of it in the method object

refusal
Targetsymbol M:Shop.Point.Distance(Shop.Point)
Arguments
name"DistanceCalculation"
Refusesnot-a-class; every file is left unchanged
Point.csinput
1 namespace Shop
2 {
3 public struct Point
4 {
5 public int X;
6 public int Y;
7
8 public int Distance(Point other)
9 {
10 int dx = X - other.X;
11 int dy = Y - other.Y;
12 return dx * dx + dy * dy;
13 }
14 }
15 }

#parameters-and-locals-become-fields

Price creates a PriceCalculation for each call, holding the order and the parameters in readonly fields and the locals in fields, and returns what its Compute method works out

success
Targetsymbol M:Shop.Order.Price(System.Int32,System.Decimal)
Arguments
name"PriceCalculation"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
5− private decimal _discountRate = 0.1m;
5+ internal decimal _discountRate = 0.1m;
66
77 /// <summary>The price of a line of this order.</summary>
88 public decimal Price(int quantity, decimal itemPrice)
99 {
10− // Large orders earn the discount.
11− decimal basePrice = quantity * itemPrice;
12− decimal discount = basePrice > 1000 ? basePrice * _discountRate : 0;
13− return basePrice - discount;
10+ return new PriceCalculation(this, quantity, itemPrice).Compute();
1411 }
1512 }
1613 }
PriceCalculation.csadded
1+namespace Shop
2+{
3+ public class PriceCalculation
4+ {
5+ private readonly Order _order;
6+ private readonly int _quantity;
7+ private readonly decimal _itemPrice;
8+ private decimal _basePrice;
9+ private decimal _discount;
10+
11+ public PriceCalculation(Order order, int quantity, decimal itemPrice)
12+ {
13+ _order = order;
14+ _quantity = quantity;
15+ _itemPrice = itemPrice;
16+ }
17+
18+ public decimal Compute()
19+ {
20+ // Large orders earn the discount.
21+ _basePrice = _quantity * _itemPrice;
22+ _discount = _basePrice > 1000 ? _basePrice * _order._discountRate : 0;
23+ return _basePrice - _discount;
24+ }
25+ }
26+}
Checkout.csunchanged
11 namespace Shop
22 {
33 public class Checkout
44 {
55 public decimal Total(Order order) => order.Price(3, 10m) + order.Price(1, 2000m);
66 }
77 }

#partial-method-rejected

A partial method is declared in two places, so it is refused rather than choosing a part

refusal
Targetsymbol M:Shop.Order.Weigh(System.Int32)
Arguments
name"Weighing"
Refusespartial-method; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public partial class Order
4 {
5 public partial int Weigh(int items);
6 }
7
8 public partial class Order
9 {
10 public partial int Weigh(int items)
11 {
12 int weight = items * 2;
13 return weight;
14 }
15 }
16 }

#recipe

Moves Price onto a PriceCalculation object held by the order and turns its locals into fields of that object, as the recipe of primitive steps

successrecipe
Recipe
  1. create-type
    name"PriceCalculation"
    file"PriceCalculation.cs"
  2. introduce-field on symbol T:Shop.Order
    type"PriceCalculation"
    name"_priceCalculation"
  3. move-instance-method on symbol M:Shop.Order.Price(System.Int32,System.Decimal)
    via"_priceCalculation"
  4. convert-local-to-field on symbol M:Shop.PriceCalculation.Price(Shop.Order,System.Int32,System.Decimal)
    local"basePrice"
  5. convert-local-to-field on symbol M:Shop.PriceCalculation.Price(Shop.Order,System.Int32,System.Decimal)
    local"discount"
Order.csmodified
11 namespace Shop
22 {
33 public class Order
44 {
5− private decimal _discountRate = 0.1m;
5+ internal decimal _discountRate = 0.1m;
6+ private readonly PriceCalculation _priceCalculation = new PriceCalculation();
67
78 /// <summary>The price of a line of this order.</summary>
89 public decimal Price(int quantity, decimal itemPrice)
910 {
10− // Large orders earn the discount.
11− decimal basePrice = quantity * itemPrice;
12− decimal discount = basePrice > 1000 ? basePrice * _discountRate : 0;
13− return basePrice - discount;
11+ return _priceCalculation.Price(this, quantity, itemPrice);
1412 }
1513 }
1614 }
PriceCalculation.csadded
1+namespace Shop
2+{
3+ public class PriceCalculation
4+ {
5+ private decimal basePrice;
6+ private decimal discount;
7+
8+ /// <summary>The price of a line of this order.</summary>
9+ public decimal Price(Order order, int quantity, decimal itemPrice)
10+ {
11+ // Large orders earn the discount.
12+ basePrice = quantity * itemPrice;
13+ discount = basePrice > 1000 ? basePrice * order._discountRate : 0;
14+ return basePrice - discount;
15+ }
16+ }
17+}
Checkout.csunchanged
11 namespace Shop
22 {
33 public class Checkout
44 {
55 public decimal Total(Order order) => order.Price(3, 10m) + order.Price(1, 2000m);
66 }
77 }

#repeated-local-name-rejected

Two locals in different blocks are both named total, and one field cannot stand for both

refusal
Targetsymbol M:Shop.Order.Price(System.Int32,System.Decimal)
Arguments
name"PriceCalculation"
Refusesrepeated-local-name mentioning “total”; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 public decimal Price(int quantity, decimal itemPrice)
6 {
7 if (quantity > 10)
8 {
9 decimal total = quantity * itemPrice * 0.9m;
10 return total;
11 }
12 else
13 {
14 decimal total = quantity * itemPrice;
15 return total;
16 }
17 }
18 }
19 }

#static-method-nullable

A static void method needs no instance; a local declared without a value is dropped, a reference-type local becomes a nullable field, and a local a lambda captures stays a local

success
Targetsymbol M:Reports.Printer.Print(System.Collections.Generic.IList{System.String},System.String)
Arguments
name"PrintJob"
method"Run"
Projectnullable enable
PrintJob.csadded
1+using System;
2+using System.Collections.Generic;
3+using System.Linq;
4+
5+namespace Reports
6+{
7+ public class PrintJob
8+ {
9+ private readonly IList<string> _lines;
10+ private readonly string _title;
11+ private string? _header;
12+ private int _count;
13+
14+ public PrintJob(IList<string> lines, string title)
15+ {
16+ _lines = lines;
17+ _title = title;
18+ }
19+
20+ public void Run()
21+ {
22+ _header = _title.ToUpperInvariant();
23+ // Counted first so the header can show it.
24+ _count = _lines.Count;
25+ Console.WriteLine(_header + " (" + _count + ")");
26+
27+ var width = 0;
28+ _lines.ToList().ForEach(line => width = Math.Max(width, line.Length));
29+ Console.WriteLine(new string('-', width));
30+ }
31+ }
32+}
Printer.csmodified
1−using System;
21 using System.Collections.Generic;
3−using System.Linq;
42
53 namespace Reports
64 {
75 public static class Printer
86 {
97 public static void Print(IList<string> lines, string title)
108 {
11− var header = title.ToUpperInvariant();
12− // Counted first so the header can show it.
13− int count;
14− count = lines.Count;
15− Console.WriteLine(header + " (" + count + ")");
16−
17− var width = 0;
18− lines.ToList().ForEach(line => width = Math.Max(width, line.Length));
19− Console.WriteLine(new string('-', width));
9+ new PrintJob(lines, title).Run();
2010 }
2111 }
2212 }

#type-already-exists-rejected

A type with the method object's name already exists

refusal
Targetsymbol M:Shop.Order.Price(System.Int32,System.Decimal)
Arguments
name"PriceCalculation"
file"Pricing/PriceCalculation.cs"
Refusestype-already-exists; every file is left unchanged
Checkout.csinput
1 namespace Shop
2 {
3 public class Checkout
4 {
5 public decimal Total(Order order) => order.Price(3, 10m) + order.Price(1, 2000m);
6 }
7 }
Order.csinput
1 namespace Shop
2 {
3 public class Order
4 {
5 private decimal _discountRate = 0.1m;
6
7 /// <summary>The price of a line of this order.</summary>
8 public decimal Price(int quantity, decimal itemPrice)
9 {
10 // Large orders earn the discount.
11 decimal basePrice = quantity * itemPrice;
12 decimal discount = basePrice > 1000 ? basePrice * _discountRate : 0;
13 return basePrice - discount;
14 }
15 }
16 }
PriceCalculation.csinput
1 namespace Shop
2 {
3 public class PriceCalculation
4 {
5 }
6 }

#uses-base-rejected

A call through base cannot be made from the method object

refusal
Targetsymbol M:Shop.Order.Heading(System.Int32)
Arguments
name"HeadingText"
Refusesuses-base; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Document
4 {
5 public virtual string Title() => "Document";
6 }
7
8 public class Order : Document
9 {
10 public override string Title() => "Order";
11
12 public string Heading(int number)
13 {
14 string text = base.Title() + " " + number;
15 return text;
16 }
17 }
18 }

#uses-protected-member-rejected

The method reads a protected member, which the method object cannot reach

refusal
Targetsymbol M:Shop.Order.Tax(System.Decimal)
Arguments
name"TaxCalculation"
Refusesuses-protected-member mentioning “Rate”; every file is left unchanged
Order.csinput
1 namespace Shop
2 {
3 public class Document
4 {
5 protected decimal Rate { get; set; } = 0.2m;
6 }
7
8 public class Order : Document
9 {
10 public decimal Tax(decimal amount)
11 {
12 decimal tax = amount * Rate;
13 return tax;
14 }
15 }
16 }