RefactorMCP catalog

Decompose Conditional

Extracts the condition of an if statement into a method that names it, and each branch into a method that names what it does, so the statement reads as a sentence.

Recipe

Extract Method three times, on the else branch, the then branch and the condition. Each new method is placed straight after the containing method, so extracting from the bottom up leaves them in the order the statement reads.

  1. extract-method on the statements of the else branch, with name the else method's name.
  2. extract-method on the statements of the then branch.
  3. extract-method on the condition. A selection covering exactly one expression becomes a method returning its value, here a bool.
"steps": [
  { "refactoring": "extract-method", "target": { "file": "Tariff.cs", "selection": "marker" }, "arguments": { "name": "SummerCharge" } },
  { "refactoring": "extract-method", "target": { "file": "Tariff.cs", "range": "26:17-26:70" }, "arguments": { "name": "WinterCharge" } },
  { "refactoring": "extract-method", "target": { "file": "Tariff.cs", "range": "24:17-24:57" }, "arguments": { "name": "NotSummer" } }
]

Only the first step can be marked, so the later steps select by range, which describes the file as the steps before left it. The plan orders the steps condition first; extracting the branches first gives the same code with the methods in reading order.

Arguments

ArgumentMeaning
conditionNamethe name of the method that evaluates the condition
thenNamethe name of the method for the statements run when the condition holds
elseNamethe name of the method for the else branch; left out, the else branch stays as it is

The target is the if statement, by a caret on its if keyword.

Precondition

  • The caret is on the if keyword of an if statement in a class's block-bodied method.
  • With elseName, the statement has an else branch that is not another if statement.
  • No branch is empty.
  • Each part meets Extract Method's precondition: a branch assigns no local that is read after it, declares none used after it, and returns early only with a value.
  • The class has no member with any of the names.

Transformation

  • The condition becomes a call of a private bool method returning it.
  • The statements of each branch become a call of a private method holding them, which returns the branch's value when the branch returns one. A branch without braces is replaced by its call.
  • Each method takes the locals and parameters its part reads, in the order it first reads them, and is static when the containing method is.
  • The methods follow the containing method: the condition, the then branch, then the else branch.

Preserved

  • Which branch runs, and what it does, for every input.
  • Comments inside a branch move with its statements; a comment leading a branch's first statement stays above the call.

Limitations

  • An else if chain is decomposed one if at a time; the else branch of a chain is refused rather than extracted whole.
  • A branch that assigns a local read after the statement is refused rather than turned into a method returning the value.
  • Conditionals in constructors, accessors, local functions and lambdas are not covered.

Error codes

CodeMeaning
not-an-ifthe caret is not on an if statement
no-else-branchelseName is given but the statement has no else branch
else-if-branchthe else branch is another if statement
empty-brancha branch has no statements to extract
assigned-local-used-aftera branch assigns a local read after it
declared-local-used-aftera branch declares a local used after it
returns-earlya branch returns from a void method before its end
name-conflictthe class already has a member with one of the names
not-in-methodthe statement is not in a method

Cases

·

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

#assigned-local-rejected

Refuses when a branch assigns a local read after the if statement, which Extract Method cannot carry out of the new method

refusal
TargetTariff.cs, the /*^*/ caret
Arguments
conditionName"NotSummer"
thenName"WinterCharge"
elseName"SummerCharge"
Refusesassigned-local-used-after mentioning “'charge'”; every file is left unchanged
Tariff.csinput
1 using System;
2
3 namespace Billing
4 {
5 public class Tariff
6 {
7 private DateTime _summerStart;
8 private DateTime _summerEnd;
9 private decimal _winterRate;
10 private decimal _summerRate;
11
12 public decimal Charge(DateTime date, int quantity)
13 {
14 decimal charge;
15 /*^*/if (date < _summerStart || date > _summerEnd)
16 charge = quantity * _winterRate;
17 else
18 charge = quantity * _summerRate;
19 return charge;
20 }
21 }
22 }

#braceless-keeps-else

A branch without braces is replaced by the call; with no else name the else branch stays as it is

success
TargetLedger.cs, the /*^*/ caret
Arguments
conditionName"OutOfRange"
thenName"Reject"
Ledger.csmodified
11 using System.Collections.Generic;
22
33 namespace Accounts
44 {
55 public class Ledger
66 {
77 private readonly List<int> _rejected = new List<int>();
88 private readonly List<int> _accepted = new List<int>();
99 private int _limit = 100;
1010
1111 public void Record(int amount)
1212 {
13− /*^*/if (amount < 0 || amount > _limit)
14− _rejected.Add(amount);
13+ if (OutOfRange(amount))
14+ Reject(amount);
1515 else
1616 _accepted.Add(amount);
1717 }
18+
19+ private bool OutOfRange(int amount)
20+ {
21+ return amount < 0 || amount > _limit;
22+ }
23+
24+ private void Reject(int amount)
25+ {
26+ _rejected.Add(amount);
27+ }
1828 }
1929 }

#branches-that-return

Extracts the condition and both branches, which return values, as the recipe does

success
TargetTariff.cs, the /*^*/ caret
Arguments
conditionName"NotSummer"
thenName"WinterCharge"
elseName"SummerCharge"
Tariff.csmodified
11 using System;
22
33 namespace Billing
44 {
55 public class Tariff
66 {
77 private readonly DateTime _summerStart;
88 private readonly DateTime _summerEnd;
99 private readonly decimal _winterRate;
1010 private readonly decimal _winterServiceCharge;
1111 private readonly decimal _summerRate;
1212
1313 public Tariff(DateTime summerStart, DateTime summerEnd, decimal winterRate, decimal winterServiceCharge, decimal summerRate)
1414 {
1515 _summerStart = summerStart;
1616 _summerEnd = summerEnd;
1717 _winterRate = winterRate;
1818 _winterServiceCharge = winterServiceCharge;
1919 _summerRate = summerRate;
2020 }
2121
2222 public decimal Charge(DateTime date, int quantity)
2323 {
24− /*^*/if (date < _summerStart || date > _summerEnd)
24+ if (NotSummer(date))
2525 {
26− return quantity * _winterRate + _winterServiceCharge;
26+ return WinterCharge(quantity);
2727 }
2828 else
2929 {
30− return quantity * _summerRate;
30+ return SummerCharge(quantity);
3131 }
32+ }
33+
34+ private bool NotSummer(DateTime date)
35+ {
36+ return date < _summerStart || date > _summerEnd;
37+ }
38+
39+ private decimal WinterCharge(int quantity)
40+ {
41+ return quantity * _winterRate + _winterServiceCharge;
42+ }
43+
44+ private decimal SummerCharge(int quantity)
45+ {
46+ return quantity * _summerRate;
3247 }
3348 }
3449 }

#else-if-rejected

Refuses to extract an else branch that is another if statement, which is decomposed on its own

refusal
TargetGrader.cs, the /*^*/ caret
Arguments
conditionName"IsDistinction"
thenName"Distinction"
elseName"Other"
Refuseselse-if-branch; every file is left unchanged
Grader.csinput
1 namespace School
2 {
3 public class Grader
4 {
5 public string Grade(int mark)
6 {
7 /*^*/if (mark >= 70)
8 return "distinction";
9 else if (mark >= 40)
10 return "pass";
11 else
12 return "fail";
13 }
14 }
15 }

#no-else-branch-rejected

Refuses a name for the else branch when the if statement has none

refusal
TargetLedger.cs, the /*^*/ caret
Arguments
conditionName"OutOfRange"
thenName"Reject"
elseName"Accept"
Refusesno-else-branch; every file is left unchanged
Ledger.csinput
1 using System.Collections.Generic;
2
3 namespace Accounts
4 {
5 public class Ledger
6 {
7 private readonly List<int> _rejected = new List<int>();
8
9 public void Record(int amount)
10 {
11 /*^*/if (amount < 0)
12 {
13 _rejected.Add(amount);
14 }
15 }
16 }
17 }

#not-an-if-rejected

Refuses a caret that is not on an if statement

refusal
TargetLedger.cs, the /*^*/ caret
Arguments
conditionName"HasMore"
thenName"Step"
Refusesnot-an-if; every file is left unchanged
Ledger.csinput
1 namespace Accounts
2 {
3 public class Ledger
4 {
5 private int _balance;
6
7 public void Drain(int step)
8 {
9 /*^*/while (_balance > 0)
10 {
11 _balance -= step;
12 }
13 }
14 }
15 }

#recipe

Runs the recipe: Extract Method on the else branch, the then branch and the condition, in that order so the methods read in order

successrecipe
Recipe
  1. extract-method on Tariff.cs, the /*[*/ … /*]*/ selection
    name"SummerCharge"
  2. extract-method on Tariff.cs, range 26:17-26:70
    name"WinterCharge"
  3. extract-method on Tariff.cs, range 24:17-24:57
    name"NotSummer"
Tariff.csmodified
11 using System;
22
33 namespace Billing
44 {
55 public class Tariff
66 {
77 private readonly DateTime _summerStart;
88 private readonly DateTime _summerEnd;
99 private readonly decimal _winterRate;
1010 private readonly decimal _winterServiceCharge;
1111 private readonly decimal _summerRate;
1212
1313 public Tariff(DateTime summerStart, DateTime summerEnd, decimal winterRate, decimal winterServiceCharge, decimal summerRate)
1414 {
1515 _summerStart = summerStart;
1616 _summerEnd = summerEnd;
1717 _winterRate = winterRate;
1818 _winterServiceCharge = winterServiceCharge;
1919 _summerRate = summerRate;
2020 }
2121
2222 public decimal Charge(DateTime date, int quantity)
2323 {
24− if (date < _summerStart || date > _summerEnd)
24+ if (NotSummer(date))
2525 {
26− return quantity * _winterRate + _winterServiceCharge;
26+ return WinterCharge(quantity);
2727 }
2828 else
2929 {
30− /*[*/return quantity * _summerRate;/*]*/
30+ return SummerCharge(quantity);
3131 }
32+ }
33+
34+ private bool NotSummer(DateTime date)
35+ {
36+ return date < _summerStart || date > _summerEnd;
37+ }
38+
39+ private decimal WinterCharge(int quantity)
40+ {
41+ return quantity * _winterRate + _winterServiceCharge;
42+ }
43+
44+ private decimal SummerCharge(int quantity)
45+ {
46+ return quantity * _summerRate;
3247 }
3348 }
3449 }

#recipe-assigned-local-rejected

The recipe stops at its first step when Extract Method refuses a branch that assigns a local read afterwards, and nothing changes

refusalrecipe
Recipe
  1. extract-method on Tariff.cs, the /*[*/ … /*]*/ selection
    name"SummerCharge"
  2. extract-method on Tariff.cs, range 16:17-16:49
    name"WinterCharge"
Refusesassigned-local-used-after mentioning “'charge'”; every file is left unchanged
Tariff.csinput
1 using System;
2
3 namespace Billing
4 {
5 public class Tariff
6 {
7 private DateTime _summerStart;
8 private DateTime _summerEnd;
9 private decimal _winterRate;
10 private decimal _summerRate;
11
12 public decimal Charge(DateTime date, int quantity)
13 {
14 decimal charge;
15 if (date < _summerStart || date > _summerEnd)
16 charge = quantity * _winterRate;
17 else
18 /*[*/charge = quantity * _summerRate;/*]*/
19 return charge;
20 }
21 }
22 }

#statement-branches

Branches of several statements become void methods; a comment leading a branch stays above its call, and the locals the branches read are passed in

success
TargetShipping.cs, the /*^*/ caret
Arguments
conditionName"IsHeavy"
thenName"ShipByFreight"
elseName"ShipByPost"
Shipping.csmodified
11 using System;
22 using System.Collections.Generic;
33
44 namespace Warehouse
55 {
66 public class Shipping
77 {
88 private readonly List<string> _log = new List<string>();
99
1010 public void Ship(string parcel, int grams)
1111 {
1212 var label = parcel.ToUpperInvariant();
13− /*^*/if (grams > 30000 && !label.StartsWith("LOCAL"))
13+ if (IsHeavy(grams, label))
1414 {
1515 // Freight needs a pallet.
16− _log.Add("pallet");
17− _log.Add("freight " + label);
16+ ShipByFreight(label);
1817 }
1918 else
2019 {
21− _log.Add("post " + label); // cheapest
20+ ShipByPost(label);
2221 }
2322
2423 Console.WriteLine(label);
2524 }
25+
26+ private bool IsHeavy(int grams, string label)
27+ {
28+ return grams > 30000 && !label.StartsWith("LOCAL");
29+ }
30+
31+ private void ShipByFreight(string label)
32+ {
33+ _log.Add("pallet");
34+ _log.Add("freight " + label);
35+ }
36+
37+ private void ShipByPost(string label)
38+ {
39+ _log.Add("post " + label); // cheapest
40+ }
2641 }
2742 }