RefactorMCP catalog

Separate Query from Modifier

Splits a method that both changes state and returns a value into a modifier that makes the change and a query that returns the value, and makes every caller call both. Afterwards asking for the value no longer changes anything.

Recipe

  1. extract-method on the statements that change state, with name the modifier's name.
  2. extract-method on the returned expression, with name the query's name. The method now calls the modifier and returns the query.
  3. change-accessibility on the query, then on the modifier, giving each the method's accessibility so its callers can reach them.
  4. inline-method on the method. Each call becomes a call of the modifier followed by the call's statement with the query in its place.
"steps": [
  { "refactoring": "extract-method", "target": { "file": "Account.cs", "selection": "marker" }, "arguments": { "name": "Debit" } },
  { "refactoring": "extract-method", "target": { "file": "Account.cs", "range": "16:20-16:28" }, "arguments": { "name": "Balance" } },
  { "refactoring": "change-accessibility", "target": { "symbol": "M:Bank.Account.Balance" }, "arguments": { "accessibility": "public" } },
  { "refactoring": "change-accessibility", "target": { "symbol": "M:Bank.Account.Debit(System.Decimal)" }, "arguments": { "accessibility": "public" } },
  { "refactoring": "inline-method", "target": { "symbol": "M:Bank.Account.Withdraw(System.Decimal)" } }
]

The plan's "redirect callers" is Inline Method on the method once it only calls the two parts. The recipe needs the query last, as in a method that changes state and then returns what it changed; for a method that stores its value in a local first, inlining keeps that local at each call, where the dedicated implementation calls the query into the caller's own variable. Inline Method keeps a discarded result of a call, so a discarded call keeps a call of the query; the dedicated implementation knows the query has no side effects and drops it.

Arguments

ArgumentMeaning
queryNamethe name of the method that returns the value
modifierNamethe name of the method that changes state

The target is the method, by symbol.

Precondition

  • The method returns a value from a block body, and returns only at its end. It is not virtual, abstract, an override, an interface implementation, async or an iterator, and is declared in a class.
  • The body has one of two shapes: statements that change state followed by return value;, or var result = value;, statements that change state, and return result;, where those statements do not read result.
  • The returned value has no side effects: no call, object creation, assignment, increment or await.
  • Each part meets Extract Method's precondition, and the class has no member with either name.
  • Every reference to the method is a call that starts its statement: the whole statement, the initializer of a local declaring nothing else, the value assigned to a simple target, or, when the value is computed before the change, not the value returned.
  • Each call's receiver is a simple expression and its arguments have no side effects, since both are evaluated once for each part.

Transformation

  • The modifier is a method of the method's accessibility holding the statements that change state, returning nothing. The query is a method of the same accessibility returning the value. Each takes the parameters it reads. The query is placed first, where the method was.
  • Each call becomes a call of each part, on the same receiver with the arguments it passed for their parameters, in the order the method ran them. A call whose value was discarded only calls the modifier. A call that was the body of an if or loop without braces gets a block.
  • The method is deleted.

Preserved

  • The state changes and the value each caller receives, in the same order.
  • Comments above a call's statement stay above the first of its new statements.

Limitations

  • A method whose query and modifier are interleaved, or that returns from several places, is refused rather than untangled.
  • A call inside a larger expression is refused rather than given a local for the value.
  • Overloads and overrides are not separated together.

Error codes

CodeMeaning
returns-nothingthe method returns nothing, so there is no query
no-modifierthe method has no statements that change state before it returns
query-has-side-effectsthe returned value has side effects of its own
modifier-reads-resultthe statements that change state read the local the method returns
returns-earlythe method returns before its last statement
polymorphic-methodthe method is virtual, an override or an interface implementation
expression-bodied-memberthe method is expression-bodied
call-in-expressiona call is part of a larger expression
returned-before-modifiera call returns a value the query must compute before the modifier runs
receiver-has-side-effectsa call is made on an expression that would be evaluated twice
argument-has-side-effectsa call passes an argument with side effects
method-group-referencethe method is used as a method group
name-conflictthe class already has a member with one of the names

Cases

·

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

#call-in-expression-rejected

Refuses when a call sits inside a larger expression, where it cannot become a statement calling the modifier

refusal
Targetsymbol M:Bank.Account.Withdraw(System.Decimal)
Arguments
queryName"Balance"
modifierName"Debit"
Refusescall-in-expression; every file is left unchanged
Account.csinput
1 namespace Bank
2 {
3 public class Account
4 {
5 private decimal _balance;
6
7 public decimal Withdraw(decimal amount)
8 {
9 _balance -= amount;
10 return _balance;
11 }
12
13 public decimal Twice()
14 {
15 return Withdraw(5m) * 2;
16 }
17 }
18 }

#modifier-then-query

Separates a method that changes state and then returns a value, as the recipe does; callers in another file call the modifier and then the query

success
Targetsymbol M:Bank.Account.Withdraw(System.Decimal)
Arguments
queryName"Balance"
modifierName"Debit"
Account.csmodified
11 namespace Bank
22 {
33 public class Account
44 {
55 private decimal _balance;
66 private int _withdrawals;
77
88 public Account(decimal balance)
99 {
1010 _balance = balance;
1111 }
1212
13− public decimal Withdraw(decimal amount)
13+ public decimal Balance()
14+ {
15+ return _balance;
16+ }
17+
18+ public void Debit(decimal amount)
1419 {
1520 _balance -= amount;
1621 _withdrawals++;
17− return _balance;
1822 }
1923
2024 public int Withdrawals()
2125 {
2226 return _withdrawals;
2327 }
2428 }
2529 }
Teller.csmodified
11 namespace Bank
22 {
33 public class Teller
44 {
55 public decimal Serve(Account account)
66 {
7− var left = account.Withdraw(10m);
7+ account.Debit(10m);
8+ var left = account.Balance();
89 if (left < 0)
9− return account.Withdraw(-10m);
10+ {
11+ account.Debit(-10m);
12+ return account.Balance();
13+ }
1014 return left;
1115 }
1216 }
1317 }

#query-has-side-effects-rejected

Refuses when the value returned is computed by a call, which may change state itself

refusal
Targetsymbol M:Tickets.Dispenser.Issue
Arguments
queryName"Current"
modifierName"Record"
Refusesquery-has-side-effects; every file is left unchanged
Dispenser.csinput
1 namespace Tickets
2 {
3 public class Dispenser
4 {
5 private int _issued;
6 private int _next;
7
8 public int Issue()
9 {
10 _issued++;
11 return Take();
12 }
13
14 private int Take()
15 {
16 return _next++;
17 }
18 }
19 }

#query-then-modifier

A method that stores its value in a local before changing state becomes a query called before the modifier; a call whose value is discarded only calls the modifier

success
Targetsymbol M:Stacks.Pile.Pop
Arguments
queryName"Peek"
modifierName"RemoveTop"
Pile.csmodified
11 using System.Collections.Generic;
22
33 namespace Stacks
44 {
55 public class Pile
66 {
77 private readonly List<string> _items = new List<string>();
88
9− internal string Pop()
9+ internal string Peek()
1010 {
11− var top = _items[_items.Count - 1];
11+ return _items[_items.Count - 1];
12+ }
13+
14+ internal void RemoveTop()
15+ {
1216 _items.RemoveAt(_items.Count - 1);
13− return top;
1417 }
1518
1619 public void Push(string item)
1720 {
1821 _items.Add(item);
1922 }
2023
2124 public string Juggle()
2225 {
2326 // Throw the top one away.
24− this.Pop();
27+ this.RemoveTop();
2528 string next;
26− next = Pop();
27− var last = Pop();
29+ next = Peek();
30+ RemoveTop();
31+ var last = Peek();
32+ RemoveTop();
2833 return next + last;
2934 }
3035 }
3136 }

#recipe

Runs the recipe: Extract Method on the statements that change state and on the returned value, give both the method's accessibility, then Inline Method so callers call both

successrecipe
Recipe
  1. extract-method on Account.cs, the /*[*/ … /*]*/ selection
    name"Debit"
  2. extract-method on Account.cs, range 16:20-16:28
    name"Balance"
  3. change-accessibility on symbol M:Bank.Account.Balance
    accessibility"public"
  4. change-accessibility on symbol M:Bank.Account.Debit(System.Decimal)
    accessibility"public"
  5. inline-method on symbol M:Bank.Account.Withdraw(System.Decimal)
Account.csmodified
11 namespace Bank
22 {
33 public class Account
44 {
55 private decimal _balance;
66 private int _withdrawals;
77
88 public Account(decimal balance)
99 {
1010 _balance = balance;
1111 }
1212
13− public decimal Withdraw(decimal amount)
13+ public decimal Balance()
1414 {
15− /*[*/_balance -= amount;
16− _withdrawals++;/*]*/
1715 return _balance;
16+ }
17+
18+ public void Debit(decimal amount)
19+ {
20+ _balance -= amount;
21+ _withdrawals++;
1822 }
1923
2024 public int Withdrawals()
2125 {
2226 return _withdrawals;
2327 }
2428 }
2529 }
Teller.csmodified
11 namespace Bank
22 {
33 public class Teller
44 {
55 public decimal Serve(Account account)
66 {
7− var left = account.Withdraw(10m);
7+ account.Debit(10m);
8+ var left = account.Balance();
89 if (left < 0)
9− return account.Withdraw(-10m);
10+ {
11+ account.Debit(-10m);
12+ return account.Balance();
13+ }
1014 return left;
1115 }
1216 }
1317 }

#recipe-declared-local-rejected

The recipe stops at its first step when the statements selected as the modifier declare the local the method returns, and nothing changes

refusalrecipe
Recipe
  1. extract-method on Pile.cs, the /*[*/ … /*]*/ selection
    name"RemoveTop"
  2. inline-method on symbol M:Stacks.Pile.Pop
Refuseserror mentioning “The extracted block declares 'top'”; every file is left unchanged
Pile.csinput
1 using System.Collections.Generic;
2
3 namespace Stacks
4 {
5 public class Pile
6 {
7 private readonly List<string> _items = new List<string>();
8
9 public string Pop()
10 {
11 /*[*/var top = _items[_items.Count - 1];
12 _items.RemoveAt(_items.Count - 1);/*]*/
13 return top;
14 }
15 }
16 }

#returned-before-modifier-rejected

Refuses a call that returns the value when the query must run before the modifier, since the return would have to come between them

refusal
Targetsymbol M:Stacks.Pile.Pop
Arguments
queryName"Peek"
modifierName"RemoveTop"
Refusesreturned-before-modifier; every file is left unchanged
Pile.csinput
1 using System.Collections.Generic;
2
3 namespace Stacks
4 {
5 public class Pile
6 {
7 private readonly List<string> _items = new List<string>();
8
9 public string Pop()
10 {
11 var top = _items[_items.Count - 1];
12 _items.RemoveAt(_items.Count - 1);
13 return top;
14 }
15
16 public string Discard()
17 {
18 return Pop();
19 }
20 }
21 }

#returns-nothing-rejected

Refuses a void method, which has no query to separate

refusal
Targetsymbol M:Bank.Account.Deposit(System.Decimal)
Arguments
queryName"Balance"
modifierName"Credit"
Refusesreturns-nothing; every file is left unchanged
Account.csinput
1 namespace Bank
2 {
3 public class Account
4 {
5 private decimal _balance;
6
7 public void Deposit(decimal amount)
8 {
9 _balance += amount;
10 }
11 }
12 }