Since learning object oriented programming I’ve been confused about how to deal with multiple ways of storing data that have different levels of functionality and efficiency. I’m kind of new to this so I’m probably missing something or approaching this wrong.
Say I’m writing a hypothetical image editing program (I’m not, but this was the best example I could come up with) and I’m concerned about performance — it deals with very large files. The program can handle grayscale and color images, and both are commonly used. Grayscale images are more efficient to work with because they take less memory and can be manipulated more quickly, but they don’t support all of the operations that color images do (like adjusting the hue). I would like to use grayscale images whenever possible.
If I am designing a class that represents an image along with some useful utilities, should I:
- Make an
Image
interface with all possible commands andColorImage
/GrayscaleImage
implementations, and just makeGrayscaleImage
fail when it encounters something it can’t do? - Make a single
Image
class with multiple modes of operation? - Make
ColorImage
extend fromGrayscaleImage
, because color images add more functionality? - Make
GrayscaleImage
extend fromColorImage
, because grayscale images are a subset of color images? - Make
ColorImage
wrap aGrayscaleImage
and add color information to it? - Do something else?
None of those options seem quite right to me. The first and the second would require a lot of duplicate code for the image utility functions; the third and fourth don’t seem right conceptually (a color image is definitely not a grayscale image, but grayscale images don’t extend the functionality of color images); and the fifth would be an annoying way to store image info because I wouldn’t be able to get the exact RGB values. And what would I do if I came up with other ways of working with images more efficiently, like indexed colors?
In general, when I have a more efficient way of storing and working with data but that doesn’t support all of the functionality, what should I do?
2
Stop worrying about efficiency and start concerning yourself with creating a good abstraction that is applicable to the domain and complete to the client/user/caller’s needs.
To do this, we first identify what capabilities client/user/caller needs. Generally speaking, for a good abstraction, all these capabilities need to be found together or else the client/user/caller will have to understand more of your implementation than they ought to when they try to make up the difference.
So, first identify the set of capabilities your users need to operate over this content. For example, consider content creation, search, display, transformation (formats, compression/decompression, audio/video, etc…), collaboration.
Next, design (one or more) interfaces that together present the abstraction, and provide the client/user/caller with these capabilities.
Last, implement these interfaces, using inheritance as needed to reuse or customize implementation/code.
I find that efficiency and good abstraction can often be had together. As you are asking advice about how to design your classes, I submit that this question is more about the design of the right abstraction than it is about efficient implementation. Many of your alternatives touch on one or more of my points. Hopefully, you can see that not all of your alternatives are fully mutually exclusive.
If you design a good abstraction, you’ll be able to substitute implementations of different underlying mechanisms using the same abstraction (e.g. interface(s)). Thus, you can experiment with efficiency.
A grayscale bitmap can be colored just fine. Some other image file formats will have to be re-encoded if they suddenly stop being grayscale. If the user tries to turn your grayscale image into sepia tone you have to decide wether to let them or to tell them no.
Similarly, you will have to decide how to react when requested methods are not valid with your object in a particular state. You can change state, throw an exception, or do nothing. None of these options requires you to change your interface. What you change is the behavior that results when called.
I honestly don’t see how this is about performance. This same issue comes up with many data structures. For example, what does it mean to capitalize an integer? While I hate it when things fail quietly, I think a capitalized 1 is just a 1.