The Law of Expanding Wrappers
Or, Why It’s Sometimes Better To Inherit Than to Write a Wrapper Class
All Part of the Process
I recently wrote code in C# that needed to call an external program for an answer. No problem! Let’s just instantiate the Process class and be on our merry way!
public static string GetOutputFromProcess(string name, string args) { Process proc = new Process(); ProcessStartInfo sI = new ProcessStartInfo(name, args); sI.RedirectStandardError = true; sI.RedirectStandardInput = true; sI.RedirectStandardOutput = true; sI.UseShellExecute = false; sI.CreateNoWindow = true; proc.StartInfo = sI; proc.Start(); proc.WaitForExit(); return proc.StandardOutput.ReadToEnd(); }
This code sucks, but it works under strict assumptions.
- If the program is guaranteed to always exit, then this function will always return a value.
- If the program runs in less than about 50 milliseconds on Grandma’s machine (the one that you thought was blazing fast back in 1997), there won’t be many noticeable UI hiccups.
The biggest problem (to me) is that this code is not extensible, unless you like 35-parameter functions, or 35 3-parameter functions. I don’t.
Class Act
We use this code on our project for a while, and we finally need to add functionality: some of our programs need to output errors to the terminal for debugging purposes. Let’s make this into a class!
We all know that composition is preferred to inheritance, so let’s avoid evil inheritance and compose this sucker!
class MyProcessManager { private Process proc; public MyProcessManager(string name, string args) { proc = new Process(); ProcessStartInfo sI = new ProcessStartInfo(name, args); sI.RedirectStandardError = true; sI.RedirectStandardInput = true; sI.RedirectStandardOutput = true; sI.UseShellExecute = false; sI.CreateNoWindow = true; proc.StartInfo = sI; } public void UseErrorTerminal(bool use) { proc.StartInfo.CreateNoWindow = !use; proc.StartInfo.RedirectStandardError = !use; } public string Run() { proc.Start(); proc.WaitForExit(); return proc.StandardOutput.ReadToEnd(); } }
This isn’t so bad. This is extensible, testable, and the functionality is simple. Perfect!
Well, not quite. Let’s say that we have a program that takes 7 seconds to run. This will freeze the UI solid without threading. We will need to add delegate handling and enable events in the Process.
Maybe we want to redirect each of the streams at different times.
Maybe we want to change the Process name without changing anything else.
Maybe we want to have access to the standard output and standard error separately while the Process is running.
Enter The Law of Expanding Wrappers:
The Law of Expanding Wrappers
A wrapper class will expand to match the functionality of the wrapped class.
What Does This Imply?
The direct implication is that you can (sometimes) save yourself effort by using inheritance! Rather than adding wrappers in our Process example, isn’t this easier?
class MyCustomProcess: Process { public MyCustomProcess(string name, string args) { ProcessStartInfo sI = new ProcessStartInfo(name, args); sI.RedirectStandardError = true; sI.RedirectStandardInput = true; sI.RedirectStandardOutput = true; sI.UseShellExecute = false; sI.CreateNoWindow = true; StartInfo = sI; } }
The magic here is that you get ALL of the functionality of Process, for free. The whole Process class is at your disposal. Modifying small details is trivial! If you want to add functionality, just write a method to do it!
Even better, this helps stave off the Law of Leaky Abstractions:
All non-trivial abstractions, to some degree, are leaky.
We’ve changed a potentially complex abstraction into a trivial extension. That’s gotta be worth something.
What Doesn’t This Imply?
This does not say that the interface ends up identical.
Maybe you’re passing memory-managing objects instead of pointers to objects. Maybe your interface only has half of the methods, and is far better organized. Great! Good work! There’s no law that says that yucky-but-functional interfaces have to be used.
You might not use every method that the old class offers. Some methods deprecate, and some are simply vestigial. If nobody uses a function, it’s not functionality.
On a related note…
This does not say that the interface ends up worse.
In the example of Process, it is difficult to find a generalized interface that is better than the interface of Process. It’s easy to use, and you can pass around and store the starting info as an entity.
Not all interfaces are created equal. You may be able to generate all functionality from a few parameters. Maybe you can slay redundant parameters with a single blow.
Your interface could even add functionality (like error-checking).
Conclusions
At this simplest level, I make the case that programmers should consider using inheritance instead of composition for wrapper classes. If you do choose the wrapper approach, it should be for the following reasons:
- You understand that you’re going to eventually get the same functionality that the wrapped class offers.
- You still have a damn good reason (like improving the input/output).
Popularity: 7% [?]
