RefactorMCP catalog

Replace Inheritance with Delegation

For a class that inherits from another only to reuse some of its members: the class holds an instance of its old base class in a field, reaches what it used to inherit through the field, forwards the inherited members other code uses, and stops deriving from the old base class.

Recipe

  1. introduce-field of the base type on the class: "target": { "symbol": "T:Shop.Stack" }, "arguments": { "type": "List<int>", "name": "_list" }.
  2. add-delegating-member forwarding to the base class, for each inherited member other code uses through the class: "target": { "symbol": "T:Shop.Stack" }, "arguments": { "member": "Count", "via": "base" }, then "member": "this" for the indexer. Each is declared new, and the class's own uses of the inherited member are written with base.
  3. replace-base-uses-with-field, so the class's uses of its inherited members, those in the new forwarding members included, go through the field: "target": { "symbol": "T:Shop.Stack" }, "arguments": { "field": "_list" }.
  4. change-base-type removing the base class, which also drops the new modifiers that no longer hide anything: "target": { "symbol": "T:Shop.Stack" }.

The plan's recipe, Introduce Field, delegate each used inherited member, Change Base Type, has no single primitive for its middle step. It is split here into forwarding members added while the base class is still there, which changes nothing for their callers, and one step that moves every use of the base class part onto the field at once, which is what keeps each step behaviour-preserving.

Arguments

ArgumentRequiredMeaning
fieldnoThe name of the field holding the old base class; defaults to _ and the base class name in camel case

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

Precondition

  • The target is a class, not static, not a record and not partial, with a base class other than object, which may be a library class.
  • No member of the class or its bases has the field's name.
  • The class overrides no member of the base class, since the base class would go on calling its own version.
  • The class uses no protected member of the base class, which a field cannot reach.
  • Inherited events, and inherited methods with type parameters or ref, out, params or optional parameters, are not used by other code.
  • The result compiles: no code converts the class to its old base class or to an interface it only had through it, and no code relies on an extension method of the old base class.

Transformation

  • The class gains private readonly List<int> _list = new List<int>(); as its first member, the type written as the base list wrote it. When a constructor passes arguments to the base class, the field has no initializer, and each constructor that does not chain to another assigns it first, passing on the arguments it passed to the base (_catalogue = new Catalogue(title);).
  • Inside the class, uses of inherited members through an implicit this, this or base go through the field: this[Count - 1] becomes _list[_list.Count - 1]. Inherited static members are qualified by their class.
  • Each inherited member that other code uses through the class, including its subclasses, is forwarded by a member of the class with the same name, as accessible as the inherited one, after the field: properties and fields as properties, get-only unless other code sets them, indexers likewise, and methods by expression-bodied methods.
  • The base class is removed from the base list; interfaces stay.

Preserved

  • Every use of the class's own members and of the forwarded members, which reach the same code as before.
  • Comments and interfaces on the declaration.

Limitations

  • Members of object the old base class overrides, such as ToString, are not forwarded, so their results can change.
  • Forwarding members are expression-bodied, and their accessor lists are written on one line.
  • A new modifier on a member that hid an inherited one is left in place.

Error codes

CodeMeaning
not-a-classthe target is not a non-static class
no-base-classthe class derives only from object
partial-typethe class is declared in several parts
name-conflictthe class or a base already has a member with the field's name
overrides-base-memberthe class overrides a member of the base class
uses-protected-memberthe class uses a protected member of the base class
unsupported-memberother code uses an inherited event
unsupported-methodother code uses an inherited method with type parameters or ref, out, params or optional parameters
breaks-compilationthe result would not compile, for example because code converts the class to its old base

Cases

·

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

#base-constructor-arguments

A constructor that passed arguments to the base class creates the field's instance with them; a property other code sets is forwarded with a setter, and the class keeps its interface

success
Targetsymbol T:Music.Playlist
Playlist.csmodified
11 using System;
22
33 namespace Music
44 {
5− public class Playlist : Catalogue, IDisposable
5+ public class Playlist : IDisposable
66 {
7− public Playlist(string title) : base(title)
7+ private readonly Catalogue _catalogue;
8+
9+ public int Size => _catalogue.Size;
10+
11+ public string Title { get => _catalogue.Title; set => _catalogue.Title = value; }
12+
13+ public Playlist(string title)
814 {
15+ _catalogue = new Catalogue(title);
916 }
1017
1118 public Playlist() : this("Untitled")
1219 {
1320 }
1421
15− public void Queue(string song) => base.Add(song);
22+ public void Queue(string song) => _catalogue.Add(song);
1623
1724 public void Dispose()
1825 {
1926 }
2027 }
2128 }
Catalogue.csunchanged
11 using System.Collections.Generic;
22
33 namespace Music
44 {
55 public class Catalogue
66 {
77 private readonly List<string> _songs = new List<string>();
88
99 public Catalogue(string title)
1010 {
1111 Title = title;
1212 }
1313
1414 public string Title { get; set; }
1515
1616 public int Size => _songs.Count;
1717
1818 public void Add(string song) => _songs.Add(song);
1919 }
2020 }
Client.csunchanged
11 namespace Music
22 {
33 public static class Client
44 {
55 public static int Run()
66 {
77 using var playlist = new Playlist("Road trip");
88 playlist.Queue("Song");
99 playlist.Title = playlist.Title + " (edited)";
1010 return playlist.Size;
1111 }
1212 }
1313 }

#converted-to-base-rejected

A client passes the stack where a List<int> is expected, which it no longer is

refusal
Targetsymbol T:Shop.Stack
Refusesbreaks-compilation mentioning “Stack”; every file is left unchanged
Client.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public class Client
6 {
7 public int Run()
8 {
9 var stack = new Stack();
10 stack.Push(1);
11 return Total(stack);
12 }
13
14 private static int Total(List<int> numbers) => numbers.Count;
15 }
16 }
Stack.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 // Last in, first out.
6 public class Stack : List<int>
7 {
8 public void Push(int value) => Add(value);
9
10 public int Pop()
11 {
12 var top = this[Count - 1];
13 RemoveAt(Count - 1);
14 return top;
15 }
16 }
17 }

#library-base

A stack that inherited from List<int> holds a list instead; its own code uses the list, and the count and indexer other code uses are forwarded

success
Targetsymbol T:Shop.Stack
Stack.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 // Last in, first out.
6− public class Stack : List<int>
6+ public class Stack
77 {
8− public void Push(int value) => Add(value);
8+ private readonly List<int> _list = new List<int>();
9+
10+ public int Count => _list.Count;
11+
12+ public int this[int index] { get => _list[index]; set => _list[index] = value; }
13+
14+ public void Push(int value) => _list.Add(value);
915
1016 public int Pop()
1117 {
12− var top = this[Count - 1];
13− RemoveAt(Count - 1);
18+ var top = _list[_list.Count - 1];
19+ _list.RemoveAt(_list.Count - 1);
1420 return top;
1521 }
1622 }
1723 }
Client.csunchanged
11 namespace Shop
22 {
33 public class Client
44 {
55 public int Run()
66 {
77 var stack = new Stack();
88 stack.Push(1);
99 stack.Push(2);
1010 stack[0] = 5;
1111 return stack.Pop() + stack.Count + stack[0];
1212 }
1313 }
1414 }

#name-conflict-rejected

The name asked for the field is already a member the playlist inherits

refusal
Targetsymbol T:Music.Playlist
Arguments
field"Title"
Refusesname-conflict; every file is left unchanged
Catalogue.csinput
1 using System.Collections.Generic;
2
3 namespace Music
4 {
5 public class Catalogue
6 {
7 private readonly List<string> _songs = new List<string>();
8
9 public Catalogue(string title)
10 {
11 Title = title;
12 }
13
14 public string Title { get; set; }
15
16 public int Size => _songs.Count;
17
18 public void Add(string song) => _songs.Add(song);
19 }
20 }
Client.csinput
1 namespace Music
2 {
3 public static class Client
4 {
5 public static int Run()
6 {
7 using var playlist = new Playlist("Road trip");
8 playlist.Queue("Song");
9 playlist.Title = playlist.Title + " (edited)";
10 return playlist.Size;
11 }
12 }
13 }
Playlist.csinput
1 using System;
2
3 namespace Music
4 {
5 public class Playlist : Catalogue, IDisposable
6 {
7 public Playlist(string title) : base(title)
8 {
9 }
10
11 public Playlist() : this("Untitled")
12 {
13 }
14
15 public void Queue(string song) => base.Add(song);
16
17 public void Dispose()
18 {
19 }
20 }
21 }

#no-base-class-rejected

The report derives only from object, so there is no inheritance to replace

refusal
Targetsymbol T:Shop.Report
Refusesno-base-class; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Report
4 {
5 public string Print(string body) => body;
6 }
7 }

#overrides-base-member-rejected

The report overrides Title, which the document's Heading calls; a document held in a field would call its own Title instead

refusal
Targetsymbol T:Shop.Report
Refusesoverrides-base-member mentioning “Title”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Document
4 {
5 public virtual string Title() => "Document";
6
7 public string Heading() => "== " + Title() + " ==";
8 }
9
10 public class Report : Document
11 {
12 public override string Title() => "Report";
13 }
14 }

#partial-type-rejected

The stack is declared in two parts, which are refused rather than rewritten separately

refusal
Targetsymbol T:Shop.Stack
Refusespartial-type; every file is left unchanged
Stack.csinput
1 using System.Collections.Generic;
2
3 namespace Shop
4 {
5 public partial class Stack : List<int>
6 {
7 public void Push(int value) => Add(value);
8 }
9
10 public partial class Stack
11 {
12 public int Peek() => this[Count - 1];
13 }
14 }

#recipe

The recipe: a field of the base type, members forwarding to the base class for the count and indexer other code uses, the class's uses of its base class moved onto the field, then removing the base class

successrecipe
Recipe
  1. introduce-field on symbol T:Shop.Stack
    type"List<int>"
    name"_list"
  2. add-delegating-member on symbol T:Shop.Stack
    member"Count"
    via"base"
  3. add-delegating-member on symbol T:Shop.Stack
    member"this"
    via"base"
  4. replace-base-uses-with-field on symbol T:Shop.Stack
    field"_list"
  5. change-base-type on symbol T:Shop.Stack
Stack.csmodified
11 using System.Collections.Generic;
22
33 namespace Shop
44 {
55 // Last in, first out.
6− public class Stack : List<int>
6+ public class Stack
77 {
8− public void Push(int value) => Add(value);
8+ private readonly List<int> _list = new List<int>();
9+
10+ public int Count => _list.Count;
11+
12+ public int this[int index] { get => _list[index]; set => _list[index] = value; }
13+
14+ public void Push(int value) => _list.Add(value);
915
1016 public int Pop()
1117 {
12− var top = this[Count - 1];
13− RemoveAt(Count - 1);
18+ var top = _list[_list.Count - 1];
19+ _list.RemoveAt(_list.Count - 1);
1420 return top;
1521 }
1622 }
1723 }
Client.csunchanged
11 namespace Shop
22 {
33 public class Client
44 {
55 public int Run()
66 {
77 var stack = new Stack();
88 stack.Push(1);
99 stack.Push(2);
1010 stack[0] = 5;
1111 return stack.Pop() + stack.Count + stack[0];
1212 }
1313 }
1414 }

#uses-protected-member-rejected

The report calls a protected method of its base class, which it cannot call through a field

refusal
Targetsymbol T:Shop.Report
Refusesuses-protected-member mentioning “Footer”; every file is left unchanged
Report.csinput
1 namespace Shop
2 {
3 public class Document
4 {
5 protected string Footer() => "-- end --";
6 }
7
8 public class Report : Document
9 {
10 public string Print(string body) => body + "\n" + Footer();
11 }
12 }