RefactorMCP catalog

Constructor Injection

Turns an object a method constructs for itself into a dependency the class receives through its constructor and keeps in a field. Every construction of the class passes a new instance, built as the method built it.

The tool is inject-constructor-dependency. Moving a method parameter to the constructor instead is not this refactoring: callers would lose the value they passed, so it does not preserve behaviour.

Recipe

  1. change-signature on the constructor, adding a parameter of the dependency's type whose value at each construction is the object the method constructed.
  2. introduce-field on the class, adding a field of the dependency's type.
  3. initialize-field-from-constructor-parameter on the constructor, with the new field and parameter.
  4. make-field-readonly on the field.
  5. replace-expression-with-field on the method, replacing the local's construction with the field. Every construction of the class now passes the same construction for the parameter the field is assigned from.
  6. inline-local-variable on the local, which now holds the field.
"steps": [
  {
    "refactoring": "change-signature",
    "target": { "symbol": "M:Shop.OrderService.#ctor(System.String)" },
    "arguments": { "parameters": [ { "name": "prefix" }, { "name": "mailer", "type": "Mailer", "value": "new Mail.Mailer(\"smtp.example.com\")" } ] }
  },
  { "refactoring": "introduce-field", "target": { "symbol": "T:Shop.OrderService" }, "arguments": { "type": "Mailer", "name": "_mailer" } },
  { "refactoring": "initialize-field-from-constructor-parameter", "target": { "symbol": "M:Shop.OrderService.#ctor(System.String,Shop.Mail.Mailer)" }, "arguments": { "field": "_mailer", "parameter": "mailer" } },
  { "refactoring": "make-field-readonly", "target": { "symbol": "F:Shop.OrderService._mailer" } },
  { "refactoring": "replace-expression-with-field", "target": { "symbol": "M:Shop.OrderService.Place(System.String)" }, "arguments": { "expression": "new Mailer(\"smtp.example.com\")", "field": "_mailer" } },
  { "refactoring": "inline-local-variable", "target": { "symbol": "M:Shop.OrderService.Place(System.String)" }, "arguments": { "local": "mailer" } }
]

The plan's recipe has no step making the field readonly; Replace Expression with Field needs it, so that only the constructor sets the field. The recipe covers a class with one constructor; the dedicated implementation also gives a class without one a constructor, and does every step as one change.

Arguments

ArgumentMeaning
parameterthe constructor parameter's name; the local's name when left out
fieldthe field's name; the local's name with a leading underscore when left out

The target is the local holding the constructed object, by a caret on its declaration or any use, or in a later step by the method's symbol with arguments.local naming it.

Precondition

  • The local is declared on its own, in a block of an instance method of a class without a primary constructor, and initialised with new and no object initializer. It is never assigned again.
  • The construction's arguments read no local, parameter or instance member and have no side effects, so every caller of the constructor can pass the same construction.
  • The class has at most one instance constructor, and it does not call another with this(...).
  • The class has no member with the field's name, and the constructor no parameter with the parameter's name.
  • The result compiles: a derived class that relies on the constructor without passing the dependency is refused.

Transformation

  • A private readonly field of the local's type is added after the class's other fields.
  • The constructor takes the dependency as a parameter after its required parameters and assigns it to the field at its end. A class without a constructor of its own gets one after the field, public, or protected for an abstract class.
  • Every construction of the class, including base(...) calls and target-typed new, passes the construction the method made, its type qualified as each file needs.
  • The method uses the field wherever it used the local, and the declaration is removed; comments above it stay in place.

Preserved

  • What the method does with the object, as long as the object behaves the same when it is shared.

Limitations

  • One object now serves every call of the method on an instance, where each call used to construct its own. That is the point of the refactoring, but an object that keeps state between uses behaves differently, and this is not checked.
  • The object is constructed when the class is, rather than when the method runs, so a constructor with side effects runs earlier and even when the method is never called.
  • Classes with several constructors are refused rather than choosing one.

Error codes

CodeMeaning
not-a-constructionthe local is not initialised by constructing an object
not-in-instance-methodthe local is not in an instance method of a class without a primary constructor
not-declared-alonethe local is not declared on its own in a block
assigned-after-declarationthe local is assigned after its declaration
argument-depends-on-methodan argument of the construction reads the method's state
object-initializerthe object is constructed with an initializer
several-constructorsthe class has several constructors
chained-constructorthe constructor calls another with this(...)
name-conflictthe class already has a member with the field's name
duplicate-parameterthe constructor already has a parameter with the parameter's name
not-a-localthe target is not a local variable

Cases

·

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

#argument-depends-on-method-rejected

Refuses an object constructed from the method's parameter, which the constructor's callers do not have

refusal
TargetOrderService.cs, the /*^*/ caret
Refusesargument-depends-on-method mentioning “'host'”; every file is left unchanged
OrderService.csinput
1 namespace Shop
2 {
3 public class Mailer
4 {
5 public Mailer(string host)
6 {
7 Host = host;
8 }
9
10 public string Host { get; }
11 }
12
13 public class OrderService
14 {
15 public string Place(string order, string host)
16 {
17 var /*^*/mailer = new Mailer(host);
18 return mailer.Host + order;
19 }
20 }
21 }

#existing-constructor

A mailer the method constructs becomes a constructor parameter kept in a field; a construction in another namespace passes a new mailer, its type qualified as that file needs

success
TargetOrderService.cs, the /*^*/ caret
Checkout.csmodified
11 namespace Shop.Web
22 {
33 public class Checkout
44 {
55 public void Complete(string order)
66 {
7− var service = new OrderService("web-");
7+ var service = new OrderService("web-", new Mail.Mailer("smtp.example.com"));
88 service.Place(order);
99 }
1010 }
1111 }
OrderService.csmodified
11 using Shop.Mail;
22
33 namespace Shop
44 {
55 public class OrderService
66 {
77 private readonly string _prefix;
8+ private readonly Mailer _mailer;
89
9− public OrderService(string prefix)
10+ public OrderService(string prefix, Mailer mailer)
1011 {
1112 _prefix = prefix;
13+ _mailer = mailer;
1214 }
1315
1416 public void Place(string order)
1517 {
1618 // Mail goes out as soon as the order is placed.
17− var /*^*/mailer = new Mailer("smtp.example.com");
18− mailer.Send(_prefix + order);
19+ _mailer.Send(_prefix + order);
1920 }
2021 }
2122 }
Mailer.csunchanged
11 using System.Collections.Generic;
22
33 namespace Shop.Mail
44 {
55 public class Mailer
66 {
77 private readonly string _host;
88 private readonly List<string> _sent = new List<string>();
99
1010 public Mailer(string host)
1111 {
1212 _host = host;
1313 }
1414
1515 public void Send(string message)
1616 {
1717 _sent.Add(_host + ": " + message);
1818 }
1919 }
2020 }

#no-constructor

A class without a constructor of its own gets one taking the dependency, and the given names are used for the parameter and the field

success
TargetGreeter.cs, the /*^*/ caret
Arguments
parameter"time"
field"_time"
Greeter.csmodified
11 namespace Shop
22 {
33 public class Clock
44 {
55 public int Hour()
66 {
77 return 12;
88 }
99 }
1010
1111 public class Greeter
1212 {
13+ private readonly Clock _time;
14+
15+ public Greeter(Clock time)
16+ {
17+ _time = time;
18+ }
19+
1320 public string Greet(string name)
1421 {
15− var clock = new Clock();
16− return (/*^*/clock.Hour() < 12 ? "Good morning, " : "Hello, ") + name;
22+ return (_time.Hour() < 12 ? "Good morning, " : "Hello, ") + name;
1723 }
1824
1925 public static string Sample()
2026 {
21− return new Greeter().Greet("Ann");
27+ return new Greeter(new Clock()).Greet("Ann");
2228 }
2329 }
2430 }

#not-a-construction-rejected

Refuses a local that holds the result of a call rather than a newly constructed object

refusal
TargetOrderService.cs, the /*^*/ caret
Refusesnot-a-construction; every file is left unchanged
OrderService.csinput
1 using System;
2
3 namespace Shop
4 {
5 public class OrderService
6 {
7 public string Stamp(string order)
8 {
9 var /*^*/now = DateTime.Now;
10 return order + now.Ticks;
11 }
12 }
13 }

#recipe

Runs the recipe: Change Signature adds the dependency to the constructor, Introduce Field adds a field of its type, the constructor assigns it from the parameter and it becomes readonly, the construction in the method becomes the field, and the local is inlined

successrecipe
Recipe
  1. change-signature on symbol M:Shop.OrderService.#ctor(System.String)
    parameters[{"name": "prefix"}, {"name": "mailer", "type": "Mailer", "value": "new Mail.Mailer(\"smtp.example.com\")"}]
  2. introduce-field on symbol T:Shop.OrderService
    type"Mailer"
    name"_mailer"
  3. initialize-field-from-constructor-parameter on symbol M:Shop.OrderService.#ctor(System.String,Shop.Mail.Mailer)
    field"_mailer"
    parameter"mailer"
  4. make-field-readonly on symbol F:Shop.OrderService._mailer
  5. replace-expression-with-field on symbol M:Shop.OrderService.Place(System.String)
    expression"new Mailer(\"smtp.example.com\")"
    field"_mailer"
  6. inline-local-variable on symbol M:Shop.OrderService.Place(System.String)
    local"mailer"
Checkout.csmodified
11 namespace Shop.Web
22 {
33 public class Checkout
44 {
55 public void Complete(string order)
66 {
7− var service = new OrderService("web-");
7+ var service = new OrderService("web-", new Mail.Mailer("smtp.example.com"));
88 service.Place(order);
99 }
1010 }
1111 }
OrderService.csmodified
11 using Shop.Mail;
22
33 namespace Shop
44 {
55 public class OrderService
66 {
77 private readonly string _prefix;
8+ private readonly Mailer _mailer;
89
9− public OrderService(string prefix)
10+ public OrderService(string prefix, Mailer mailer)
1011 {
1112 _prefix = prefix;
13+ _mailer = mailer;
1214 }
1315
1416 public void Place(string order)
1517 {
1618 // Mail goes out as soon as the order is placed.
17− var mailer = new Mailer("smtp.example.com");
18− mailer.Send(_prefix + order);
19+ _mailer.Send(_prefix + order);
1920 }
2021 }
2122 }
Mailer.csunchanged
11 using System.Collections.Generic;
22
33 namespace Shop.Mail
44 {
55 public class Mailer
66 {
77 private readonly string _host;
88 private readonly List<string> _sent = new List<string>();
99
1010 public Mailer(string host)
1111 {
1212 _host = host;
1313 }
1414
1515 public void Send(string message)
1616 {
1717 _sent.Add(_host + ": " + message);
1818 }
1919 }
2020 }

#several-constructors-rejected

Refuses a class with several constructors, since it is not clear which should take the dependency

refusal
TargetGreeter.cs, the /*^*/ caret
Refusesseveral-constructors; every file is left unchanged
Greeter.csinput
1 namespace Shop
2 {
3 public class Clock
4 {
5 public int Hour()
6 {
7 return 12;
8 }
9 }
10
11 public class Greeter
12 {
13 private readonly string _greeting;
14
15 public Greeter()
16 {
17 _greeting = "Hello";
18 }
19
20 public Greeter(string greeting)
21 {
22 _greeting = greeting;
23 }
24
25 public string Greet(string name)
26 {
27 var /*^*/clock = new Clock();
28 return clock.Hour() + _greeting + name;
29 }
30 }
31 }