Why Wrappers Are So Useful
Now, if you read my previous posts, you will know that I have mentioned wrappers several times, and said that they are a very powerful but underrated tool. If you have never used wrappers before, this might seem like a fuzzy statement, because at first glance it might look like a lot of wasted work.
Let “Create” be thy name
The job of a wrapper is to wrap existing functionality into a new and simpler interface. This could be a RSDiskFile class, wrapping basic file disk functionality into an interface which meets the naming and interface requirements of your application. Remember, the specific details of how you want the interface to look, is entirely up to you and your preferences, as long as you obey the basic rules, and as long as you have defined a consistent and uniform way to access your classes.
public class RSDiskFile
{
private RSDiskFile();
public static RSDiskFile Create(string filePath);
public Destroy();
public bool Valid { get; }
public long Length { get; }
public byte[] ReadAsRawData();
public string ReadAsString();
public string[] ReadAsLines();
public Image ReadAsImage();
public void WriteAsRawData(byte[] data);
public void WriteAsString(string data);
public void WriteAsLines(string[] data);
public void WriteAsImage(Image data);
}
Note: Some implementation details are hidden for clarity.
As you can see, I like to use a factory method pattern to create classes (a static method returning an instance of the class), as it is infinitely more flexible and readable than a normal constructor. Because of that, I declared the normal constructor private, so that you don’t accidentally use it. I also added a Destroy method, and while modern garbage collection pretty much makes it obsolete in most cases, there might be situations where it is useful. If Create, for instance, opened a serial port, it would make sense that Destroy closed it. Unless of course you used an Open and Close pattern, but that would again come down to your coding and implementation style.
Then I added two readonly properties. You could add more if you want. They can either be set when the class is created, or you could implement getters. I like to set them when the class is created, if possible, so that it is only done once.
Finally I added some read and write methods, which comply to my standards of naming.
If you read existing literature on wrappers, you will often encounter terms like thin wrappers and thick wrappers. I don’t particularly distinguish between these, as the bondaries are fluid. Thin wrappers means that the wrapper does not introduce additional code or functionality. It just renames existing calls. Thick wrappers mean, that the wrapper starts to introduce added code and functionality. As a rule of thumb, you should make wrappers as thin as humanly possible, the point will become obvious later. While you will often have to add some code, keep it at an absolute minimum.
IMPORTANT
A wrapper should never inherit the class it wraps. As you in many languages can't override and hide methods and properties, inheritance defeats the idea of hiding implementation details.
My own implementation would look something like:
public class RSDiskFile
{
private string _filePath;
public bool Valid { get; }
public long Length { get; }
private RSDiskFile(string filePath)
{
_filePath = filePath;
Valid = false;
Length = 0;
if (File.Exists(_filePath) == false) return;
try
{
FileStream stream = File.Open(_filePath, ... );
Valid = true;
Length = stream.Length;
stream.Close();
}
catch
{
}
}
static RSDiskFile Create(string filePath)
{
return new RSDiskFile(filePath);
}
public byte[] ReadAsRawData()
{
return File.ReadAllBytes(_filePath);
}
public string ReadAsString()
{
return File.ReadAllText(_filePath);
}
public string[] ReadAsLines()
{
return File.ReadAllLines(_filePath);
}
public Image ReadAsImage()
{
return Image.FromFile(_filePath);
}
}
Note: Write methods are omitted for clarity.
I will get more into factory methods when I talk about class design, so dont worry about it now, and as you can see, the factory method (for now) simply calls the constructor.
You will notice that I made the constructor (factory method) exception safe. This is because I am not a strong believer in exceptions. They are messy, can cause all kinds of havoc, scatters the code, and are in worst case used as control flow. I will write a serious rant about it some day.
Bottom line is, that if the constructor is safe, everybody are safe, and you can write 100% exception free code like:
RSDiskFile file = DSFileDisk.Create(filePath);
if (file.Valid == true)
{
return file.ReadAsString();
}
return STRING_EMPTY;
This can be further improved, but that will be for another post, for now the takeaway here is, that by making a wrapper over some very basic file functionality, you created a very easy to read, consistent, fool proof, exception safe, rockstar worthy, way of reading all your files from disk. And it even returns a default value. <sobs from joy>
But wait, there is more.
Abstraction
Let us say you are writing a game, and for that you need an audio library. Let us call it SimpleSound, and all it can do is load and play an audio file. The interface looks like:
public SimpleSound();
public int Load(string filePath);
public void Play(int sound);
Usage would look like.
SimpleSound player = new SimpleSound();
int soundIndex = player.Load(soundFile);
player.Play(soundIndex);
player.Play(soundIndex);
Let us assume this plays the audio twice. Needless to say, if something goes wrong, the entire application will crash, giving the gamer a very bad day.
Now, using the same naming and style as RSDiskFile, we create a wrapper named RSAudioFile.
public static RSAudioFile Create(string filePath);
public bool Valid { get; }
public void Play();
And the implementation:
RSAudioFile audio = RSAudioFile.Create(filePath);
if (audio.Valid == true)
{
audio.Play();
audio.Play();
}
Apart from this obviously being 100% crash proof (constructors are safe), you also now see a clear pattern. A consistency in the application that wrappers can give you. But that is actually not the best part.
Imagine you suddenly can’t use SimpleSound anymore, and in stead have to use AdvancedSound, the interface being completely different. Your game is 2 days away from release, and you used SimpleSound a petrillion times in your game. Changing it will affect pretty much everything. Testing will be a nightmare.
Instead of having to go into fetal-position under your desk, all you have to do is make a new RSAudioFile wrapper for AdvancedSound, and you are good to go. You can even use the same test for the new RSAudioFile as you used for the SimpleSound wrapper. If the new test passes, you do not have to change one single line of code in the rest of the application. I mean.
How good can this thing get?
Keeping it skinny
Now, I mentioned above, that you should try to keep your wrappers as thin as possible. Imagine if RSAudioFile for SimpleSound was not a simple wrapper, but a big fat class with tons and tons of added audio functionality. In that case, it would be much harder to replace it, and would require much more testing. The chances that you would get unexpected side effects would also be much greater. Instead, if you wanted to add more audio functionality, you would create a new class (maybe RSAudio), and if you read the post about The Lego Principle, you would know that this new block would be placed on top of RSAudioFile, feeding methods down, and receiving data up.
This leads us to the conclusion, that wrappers should be introduced as early as possible, and wrap the least amount of functionality possible. Dont wrap stuff you dont need, you can always do that later.
Be gone pesky details
The final big takeaway for wrappers is the ability to hide complicated implementation details. If AdvancedAudio, for example, had a lot of complicated functionality, all this would be hidden in the wrapper. Let us say the play function looked like
PlayBack(float startTime, float endTime, int loops, bool stereo);
The constructor (factory method) of RSAudioFile, would then have to load the length of the audio file, and the implementation would look something like:
public void Play()
{
player.PlayBack(0, _audioLength, PLAY_ONCE, PLAY_MONO);
}
Obviously there is nothing stopping you from adding more advanced features to your wrapper, if the class you are wrapping supports it. Your application will still be safe, and you can implement the new features at your own pace. Remember though, do not add your own functionality unless forced.
Takeaway
Wrappers are extremely powerful, and wrapping more, is better than wrapping less. It maintains a very high level of readability, and encapsulates functionality for very easy replacement. It hides implementation details, and makes code in large applications, much easier to change and maintain.
/Lars
Comments
Post a Comment