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
change-signature on the constructor, adding a parameter of the dependency's type whose value at each construction is the object the method constructed.
introduce-field on the class, adding a field of the dependency's type.
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.
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
Argument
Meaning
parameter
the constructor parameter's name; the local's name when left out
field
the 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
Code
Meaning
not-a-construction
the local is not initialised by constructing an object
not-in-instance-method
the local is not in an instance method of a class without a primary constructor
not-declared-alone
the local is not declared on its own in a block
assigned-after-declaration
the local is assigned after its declaration
argument-depends-on-method
an argument of the construction reads the method's state
object-initializer
the object is constructed with an initializer
several-constructors
the class has several constructors
chained-constructor
the constructor calls another with this(...)
name-conflict
the class already has a member with the field's name
duplicate-parameter
the constructor already has a parameter with the parameter's name
not-a-local
the 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.
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
1
1
namespaceShop.Web
2
2
{
3
3
publicclassCheckout
4
4
{
5
5
publicvoidComplete(string order)
6
6
{
7
−
var service = newOrderService("web-");
7
+
var service = newOrderService("web-", newMail.Mailer("smtp.example.com"));
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
change-signature on symbol M:Shop.OrderService.#ctor(System.String)
parameters
[{"name": "prefix"}, {"name": "mailer", "type": "Mailer", "value": "new Mail.Mailer(\"smtp.example.com\")"}]