DISCLAIMER: This post is part of an initial investigation. So please point out any errors in my evaluation of the problem and solution.
Today at work, a co-worker and I got into a discussion regarding inheriting methods vs helper classes. I sided on the inheriting method. I argued that helper classes could lead to a “kitchen sink” helper or a lot of classes that developers would need to sift through to find the method they need.
A little context may be needed here. On our project, we have a base test case class that has methods for creating domain objects. These methods are to be used by individual test methods in child classes. The methods help build up targeted domain objects for each test case. The discussion revolved around compile-time, code speed and code size.
So tonight I decided to do some investigation to see if I was just talking crap. I fully admit that I talk a lot of it at times. To help educate myself, I dove into using the MSIL Disassembler (Ildasm.exe)*. I wrote a sample project that had 3 styles of code architecture; inheritance, static helpers and non-static helpers. The results were interesting.
Reviewing the Hello() method of the 3 types. Pay attention to the bold sections.
Child
.method public hidebysig instance class TestCompile.DomainObject
Hello(string world) cil managed
{
// Code size 33 (0x21)
.maxstack 2
.locals init ([0] class TestCompile.DomainObject '<>g__initLocal0',
[1] class TestCompile.DomainObject CS$1$0000)
IL_0000: nop
IL_0001: newobj instance void TestCompile.DomainObject::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldstr "1"
IL_000d: callvirt instance void TestCompile.DomainObject::set_Id(string)
IL_0012: nop
IL_0013: ldloc.0
IL_0014: ldarg.1
IL_0015: callvirt instance void TestCompile.DomainObject::set_Name(string)
IL_001a: nop
IL_001b: ldloc.0
IL_001c: stloc.1
IL_001d: br.s IL_001f
IL_001f: ldloc.1
IL_0020: ret
} // end of method Parent::Hello
Non-Static Helper
.method public hidebysig instance class TestCompile.DomainObject
Hello(string world) cil managed
{
// Code size 33 (0x21)
.maxstack 2
.locals init ([0] class TestCompile.DomainObject '<>g__initLocal0',
[1] class TestCompile.DomainObject CS$1$0000)
IL_0000: nop
IL_0001: newobj instance void TestCompile.DomainObject::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldstr "1"
IL_000d: callvirt instance void TestCompile.DomainObject::set_Id(string)
IL_0012: nop
IL_0013: ldloc.0
IL_0014: ldarg.1
IL_0015: callvirt instance void TestCompile.DomainObject::set_Name(string)
IL_001a: nop
IL_001b: ldloc.0
IL_001c: stloc.1
IL_001d: br.s IL_001f
IL_001f: ldloc.1
IL_0020: ret
} // end of method Helper1::Hello
Static Helper
.method public hidebysig static class TestCompile.DomainObject
Hello(string world) cil managed
{
// Code size 33 (0x21)
.maxstack 2
.locals init ([0] class TestCompile.DomainObject '<>g__initLocal0',
[1] class TestCompile.DomainObject CS$1$0000)
IL_0000: nop
IL_0001: newobj instance void TestCompile.DomainObject::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldstr "1"
IL_000d: callvirt instance void TestCompile.DomainObject::set_Id(string)
IL_0012: nop
IL_0013: ldloc.0
IL_0014: ldarg.0
IL_0015: callvirt instance void TestCompile.DomainObject::set_Name(string)
IL_001a: nop
IL_001b: ldloc.0
IL_001c: stloc.1
IL_001d: br.s IL_001f
IL_001f: ldloc.1
IL_0020: ret
} // end of method StaticHelper1::Hello
The parts in bold describe the code size, max deep of the stack and the local variables created. Pretty much identical. Looking strictly at this data it appears that there is no gain in moving the methods to separate classes.
The same is true for the Goodbye() method.
Next, let’s take a look at the test class method.
Child
.method public hidebysig instance void DoSomething() cil managed
{
// Code size 26 (0x1a)
.maxstack 2
.locals init ([0] class TestCompile.DomainObject hello,
[1] class TestCompile.DomainObject goodbye)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldstr "child 2"
IL_0007: call instance class TestCompile.DomainObject TestCompile.Parent::Hello(string)
IL_000c: stloc.0
IL_000d: ldarg.0
IL_000e: ldstr "child 2"
IL_0013: call instance class TestCompile.DomainObject TestCompile.Parent::Goodbye(string)
IL_0018: stloc.1
IL_0019: ret
} // end of method ChildTest::DoSomething
Non-Static Helper
.method public hidebysig instance void DoSomething() cil managed
{
// Code size 34 (0x22)
.maxstack 2
.locals init ([0] class TestCompile.DomainObject hello,
[1] class TestCompile.DomainObject goodbye)
IL_0000: nop
IL_0001: newobj instance void TestCompile.Helper1::.ctor()
IL_0006: ldstr "child 2"
IL_000b: call instance class TestCompile.DomainObject TestCompile.Helper1::Hello(string)
IL_0010: stloc.0
IL_0011: newobj instance void TestCompile.Helper2::.ctor()
IL_0016: ldstr "child 2"
IL_001b: call instance class TestCompile.DomainObject TestCompile.Helper2::Goodbye(string)
IL_0020: stloc.1
IL_0021: ret
} // end of method HelperTest::DoSomething
Static Helper
.method public hidebysig instance void DoSomething() cil managed
{
// Code size 24 (0x18)
.maxstack 1
.locals init ([0] class TestCompile.DomainObject hello,
[1] class TestCompile.DomainObject goodbye)
IL_0000: nop
IL_0001: ldstr "child 2"
IL_0006: call class TestCompile.DomainObject TestCompile.StaticHelper1::Hello(string)
IL_000b: stloc.0
IL_000c: ldstr "child 2"
IL_0011: call class TestCompile.DomainObject TestCompile.StaticHelper2::Goodbye(string)
IL_0016: stloc.1
IL_0017: ret
} // end of method StaticHelperTest::DoSomething
It appears that static helper classes might be the winner. There are 2 less operations than the inheritance model and uses a little less memory. The difference appears to be the ldarg.0 operation. Since instance methods have an implicit parameter (“this”), it is loaded first onto the stack.
I don’t fully understand all that I am seeing in the MSIL output. But from this simple example it appears that I was wrong from a technical perspective. I concede on that side of the argument. However, my other concerns about the human developer are still valid (ie: kitchen-sink and/or a maze of confusing helpers).
The human side can be addressed. The technical side has data to back it up.
* The java equivalent to the MSIL decompiler is the “javap” command. I would love to try this experiment in java to see if the results are similar.