If some of your types are always used together, that is probably a sign that you are missing an abstraction that bundles them. For example, if I always see the types Rectangle
and Color
together, it’s probably a good idea to create a ColoredRectangle
class that combines the two. However, these patterns tend to emerge over time, so it’s hard to actually find them manually.
Reflection can help find these relationships between types. For example, you can look at all the function/method parameter lists in your code and mark all types appearing there as ‘being used together’. Then count how often these tuples appear, and you might have a good candidate for refactoring.
Here’s how to do that in C#. First pick a few assemblies you want to analyze. One way to get them is using Assembly.GetAssembly(typeof(SomeTypeFromYourAssembly))
. Then get all the methods from all the types:
IEnumerable<MethodInfo> GetParameterTypesOfAllMethods(IEnumerable<Assembly> assemblies)
{
var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public
| BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
foreach (var assembly in assemblies)
{
foreach (var type in assembly.GetTypes())
{
foreach (var method in type.GetMethods(flags))
{
yield return method;
}
}
}
}
The flags are important: the default will not include NonPublic and DeclaredOnly. Without those, the code will not report private methods but give you methods from base classes that we do not want here.
Now this is where things become a little more muddy, and specific to your application. I am skipping generated methods with “IsSpecialName”, and then I’m only looking at non-generic class parameters:
foreach (var method in GetParameterTypesOfAllMethods(assemblies))
{
if (method.IsSpecialName)
continue;
var parameterList = method.GetParameters();
var candidates = parameterList
.Select(x => x.ParameterType)
.Where(x => !x.IsGenericParameter)
.Where(x => x.IsClass);
/* more processing here */
}
Then I convert the types to a string using ToString()
to get a nice identifier that includes filled generic parameters. I sort and join the type ids to get a key for my tuple and count the number of appearances in a Dictionary<string, int>
:
var candidateNames = candidates
.Select(x => x.ToString())
.OrderBy(x => x)
.ToList();
if (candidateNames.Count <= 1)
continue;
if (candidateNames.Any(string.IsNullOrWhiteSpace))
continue;
var key = string.Join(",", candidateNames);
if (!lookup.ContainsKey(key))
{
lookup.Add(key, 1);
}
else
{
lookup[key]++;
}
Once that is done, you can sort the resulting lookup, print out all the tuples, and see if there are any good candidates.
There’s much room for improvement with a method like this. For example, skipping non-class types is a pretty arbitrary choice. And you will not find new tuples built from built-in types this way. However, because those types offer very little semantic by themselves, it can be hard to correlate multiple occurrences simply by their types.