RefactorMCP catalog

Extract Class

Splits a class that does two jobs: some of its fields, properties and methods move into a new class, and the old class keeps a field holding an instance of it and reaches the moved members through that field.

Recipe

  1. create-type the new class: { "name": "Address", "file": "Address.cs" }.
  2. introduce-field of the new type on the class, which holds a new instance: "target": { "symbol": "T:Shop.Customer" }, "arguments": { "type": "Address", "name": "_address" }.
  3. move-field (or move-property) each field through it: "target": { "symbol": "F:Shop.Customer._street" }, "arguments": { "via": "_address" }. A static field or constant moves with "to": "Address" instead.
  4. move-instance-method each method through it: "target": { "symbol": "M:Shop.Customer.FormatAddress" }, "arguments": { "via": "_address" }, or move-static-method with "to": "Address" for a static one.

Fields and properties move before methods, so a moved method finds the data it uses already in the new class and reads it there rather than through the old class.

Arguments

ArgumentRequiredMeaning
nameyesThe new class's name
membersyesThe fields, properties and methods to move, by name; a method name moves every overload
fieldnoThe name of the field holding the new class; defaults to _ and the class name in camel case
filenoThe new class's file, relative to the solution; defaults to <name>.cs beside the class
stubnotrue (the default) leaves a delegating method for each moved method; false updates its callers

The target is the class, by symbol: "target": { "symbol": "T:Shop.Customer" }.

Precondition

  • The target is a class, and no type of the new name exists in its namespace.
  • Every named member exists and is a field, property or ordinary method.
  • The class has no member with the field's name.
  • Each move meets the precondition of Move Field, Move Property, Move Instance Method or Move Static Method: for example a method is not virtual and uses no protected member, and without stubs every caller can reach the field.

Transformation

  • The new class is public, empty apart from what moves into it, in the class's namespace.
  • The class gains private readonly <Name> <field> = new <Name>(); after its fields.
  • Each member moves as its primitive moves it: uses of a moved field go through the new field (_street becomes _address._street), private members become internal, and a moved method's uses of what stayed behind go through a parameter of the old class.

Preserved

  • Behaviour of every use of the moved members, and of the class's public surface when stubs are kept.
  • Comments above moved members, which move with them.

Limitations

  • The field always holds a new instance created with the class; it is not passed in through a constructor.
  • Moved private members become internal rather than staying private.
  • Code that copies the class field by field, such as a serializer, sees the moved state inside the new object rather than on the class.

Error codes

CodeMeaning
not-a-classthe target is a struct, interface or other type
no-membersno members were named
member-not-founda named member does not exist in the class
member-not-movablea named member is not a field, property or ordinary method
type-already-existsa type of the new name exists (step 1)
name-conflictthe class already has a member with the field's name (step 2)
polymorphic-methoda method to move is virtual, abstract, an override or an interface implementation
uses-protected-membera method to move uses a protected member of the class
via-not-accessiblewithout stubs, a caller of a moved method cannot reach the field

A refusal in any step leaves every file as it was, and names the step that refused.

Cases

·

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

#fields-and-method

Moves the address fields and their formatting into a new Address class in one call, with the same result as the recipe

success
Targetsymbol T:Shop.Customer
Arguments
name"Address"
members["_street", "_town", "FormatAddress"]
field"_address"
file"Address.cs"
Address.csadded
1+namespace Shop
2+{
3+ public class Address
4+ {
5+ // Where letters go.
6+ internal string _street = "";
7+
8+ internal string _town = "";
9+
10+ public string FormatAddress()
11+ {
12+ return _street + ", " + _town;
13+ }
14+ }
15+}
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 public string Name = "";
6−
7− // Where letters go.
8− private string _street = "";
9− private string _town = "";
6+ private readonly Address _address = new Address();
107
118 public void MoveTo(string street, string town)
129 {
13− _street = street;
14− _town = town;
10+ _address._street = street;
11+ _address._town = town;
1512 }
1613
1714 public string FormatAddress()
1815 {
19− return _street + ", " + _town;
16+ return _address.FormatAddress();
2017 }
2118 }
2219 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public string Header(Customer customer) => customer.Name + "\n" + customer.FormatAddress();
66 }
77 }

#member-not-found-rejected

A named member does not exist in the class

refusal
Targetsymbol T:Shop.Customer
Arguments
name"Address"
members["_street", "_postcode"]
Refusesmember-not-found mentioning “_postcode”; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name = "";
6
7 // Where letters go.
8 private string _street = "";
9 private string _town = "";
10
11 public void MoveTo(string street, string town)
12 {
13 _street = street;
14 _town = town;
15 }
16
17 public string FormatAddress()
18 {
19 return _street + ", " + _town;
20 }
21 }
22 }

#name-conflict-rejected

The class already has a member with the field's name, so adding the field refuses after the new class was created, and the new class is removed again

refusal
Targetsymbol T:Shop.Customer
Arguments
name"Address"
members["_street", "_town", "FormatAddress"]
field"Name"
Refusesname-conflict mentioning “step 2 (introduce-field)”; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name = "";
6
7 // Where letters go.
8 private string _street = "";
9 private string _town = "";
10
11 public void MoveTo(string street, string town)
12 {
13 _street = street;
14 _town = town;
15 }
16
17 public string FormatAddress()
18 {
19 return _street + ", " + _town;
20 }
21 }
22 }

#not-a-class-rejected

A struct copies its fields on assignment, so it is refused rather than given a field holding the extracted object

refusal
Targetsymbol T:Shop.Customer
Arguments
name"Address"
members["Street", "Town", "FormatAddress"]
Refusesnot-a-class; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public struct Customer
4 {
5 public string Street;
6 public string Town;
7
8 public string FormatAddress() => Street + ", " + Town;
9 }
10 }

#polymorphic-method-rejected

A virtual method cannot move, so the move refuses after the class, the field and both fields' moves have been done, and every file is put back

refusal
Targetsymbol T:Shop.Customer
Arguments
name"Address"
members["_street", "_town", "FormatAddress"]
Refusespolymorphic-method mentioning “step 5 (move-instance-method)”; every file is left unchanged
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name = "";
6
7 private string _street = "";
8 private string _town = "";
9
10 public void MoveTo(string street, string town)
11 {
12 _street = street;
13 _town = town;
14 }
15
16 public virtual string FormatAddress()
17 {
18 return _street + ", " + _town;
19 }
20 }
21 }

#recipe

Moves the address fields and their formatting into a new Address class reached through a field, as the recipe of primitive steps

successrecipe
Recipe
  1. create-type
    name"Address"
    file"Address.cs"
  2. introduce-field on symbol T:Shop.Customer
    type"Address"
    name"_address"
  3. move-field on symbol F:Shop.Customer._street
    via"_address"
  4. move-field on symbol F:Shop.Customer._town
    via"_address"
  5. move-instance-method on symbol M:Shop.Customer.FormatAddress
    via"_address"
Address.csadded
1+namespace Shop
2+{
3+ public class Address
4+ {
5+ // Where letters go.
6+ internal string _street = "";
7+
8+ internal string _town = "";
9+
10+ public string FormatAddress()
11+ {
12+ return _street + ", " + _town;
13+ }
14+ }
15+}
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
55 public string Name = "";
6−
7− // Where letters go.
8− private string _street = "";
9− private string _town = "";
6+ private readonly Address _address = new Address();
107
118 public void MoveTo(string street, string town)
129 {
13− _street = street;
14− _town = town;
10+ _address._street = street;
11+ _address._town = town;
1512 }
1613
1714 public string FormatAddress()
1815 {
19− return _street + ", " + _town;
16+ return _address.FormatAddress();
2017 }
2118 }
2219 }
Invoice.csunchanged
11 namespace Shop
22 {
33 public class Invoice
44 {
55 public string Header(Customer customer) => customer.Name + "\n" + customer.FormatAddress();
66 }
77 }

#type-already-exists-rejected

A class with the new name already exists, so the first step refuses

refusal
Targetsymbol T:Shop.Customer
Arguments
name"Address"
members["_street", "_town", "FormatAddress"]
Refusestype-already-exists mentioning “step 1 (create-type)”; every file is left unchanged
Address.csinput
1 namespace Shop
2 {
3 public class Address
4 {
5 public string Line { get; set; } = "";
6 }
7 }
Customer.csinput
1 namespace Shop
2 {
3 public class Customer
4 {
5 public string Name = "";
6
7 // Where letters go.
8 private string _street = "";
9 private string _town = "";
10
11 public void MoveTo(string street, string town)
12 {
13 _street = street;
14 _town = town;
15 }
16
17 public string FormatAddress()
18 {
19 return _street + ", " + _town;
20 }
21 }
22 }

#without-stubs-nullable

Without stubs, calls of a moved method go through the new field; a constant moves by type name, and nullable annotations are kept

success
Targetsymbol T:Shop.Customer
Arguments
name"Address"
members["FormatAddress", "MaxLineLength", "_street", "_town"]
stubfalse
Projectnullable enable
Address.csadded
1+namespace Shop
2+{
3+ public class Address
4+ {
5+ internal const int MaxLineLength = 40;
6+
7+ internal string? _street;
8+
9+ internal string? _town;
10+
11+ internal string FormatAddress()
12+ {
13+ var text = (_street ?? "") + ", " + (_town ?? "");
14+ return text.Length > MaxLineLength ? text.Substring(0, MaxLineLength) : text;
15+ }
16+ }
17+}
Customer.csmodified
11 namespace Shop
22 {
33 public class Customer
44 {
5− private const int MaxLineLength = 40;
6−
7− private string? _street;
8− private string? _town;
5+ private readonly Address _address = new Address();
96
107 public string? Postcode { get; set; }
118
129 public void MoveTo(string street, string town)
1310 {
14− _street = street;
15− _town = town;
11+ _address._street = street;
12+ _address._town = town;
1613 }
1714
18− public string Label() => FormatAddress() + " " + Postcode;
19−
20− private string FormatAddress()
21− {
22− var text = (_street ?? "") + ", " + (_town ?? "");
23− return text.Length > MaxLineLength ? text.Substring(0, MaxLineLength) : text;
24− }
15+ public string Label() => _address.FormatAddress() + " " + Postcode;
2516 }
2617 }