Rewritten Question
I appreciate the feedback and in response to that I'm re-writing my question. I can't give my specific situation (classes, etc), nor do I think that it would be helpful, as I work in a very niche area that wouldn't make much sense to those outside of it, but I will try and use a similar but invented analogous situation to give something more concrete.
I have an application and two libraries of interest here. These are quite mature (about ten years old) and relate to a product that has been selling that long. The application reads files of various types, including images, and makes them searchable and viewable. It also produces a detailed report.
One library (ImageIO) is responsible for reading images. It doesn't just read JPEGs and PNGs, but hundreds of different formats that have been encountered over the years. Formats are continuously being added to it. It can also spit out standard formats like PNGs and JPEGs.
Another library is responsible for the reporting. It doesn't just handle images, but all sorts of file types. It gives a detailed report including a list of all of the metadata used.
When I got handed the code, the main application has a class called Document
which contains, among other things, a list of Image
s. An Image
has some set properties and methods including Height
, Width
, and GetBitmap
. Each of the image types has its own subclass; JpegImage
, PngImage
, TiffImage
, DicomImage
and so on. Most of these have custom properties; camera used, white point, colorspace, title, GPS location and so on. Most have between one and six extra properties. Some properties are common to many types (like exif data), while many image types, particularly the more niche types (like BobsImage
) have properties that are unique to that image type.
Image
// Some methods
int[][] GetBitmap()
// Some properties
int Height
int Width
The main application only uses a few of these properties when they exist. The reporting library reports on them all. There are dozens of properties. There are no special methods, though behind the scenes, some types use some properties for the standard methods. For example, using the aspect ratio for producing the BitMap.
The application uses a magic string to tell the reporting library what sub-class the images really are. The reporting library then uses that to cast the Image
back to it's subclass, and then heaps of ifs and switches to report accordingly.
I was not happy with this architecture. My first attempt was to turn Image
into and an IImage
interface, and then bundle properties into groups and have relevant interfaces for the extra. The IImage
seems to work fine, but the properties are an issue; there were about as many interfaces as properties, and then they were tested with an "is a" style test, which felt like I was pretty much back with the switch statements.
IImage
// Some methods
int[][] GetBitmap()
// Some properties
int Height
int Width
IGps
Double[] GetGps()
My second attempt was to just add bool HasProperty(PropertyId id)
and T GetProperty<T>(PropertyId)
to the IImage
. Then none of the other interfaces were required.
enum PropertyId
GpsData,
ExifData, ...
IImage
// Some methods
int[][] GetBitmap()
// Some properties
int Height
int Width
// New methods
bool HasProperty(PropertyId id)
T GetProperty<T>(PropertyId)
List<PropertyId> GetSupportedProperties()
This really cleaned up the Reporting library; it could enumerate over the GetSupportedProperties
and no ifs or switches. It also means it didn't have to care about the hundreds of sub-classes, and in fact, sub-classes weren't even required. A generic Image class that implemented IImage could be made that just contained a list of properties, types for run-time type checking and values.
It still feels bad. It removes compile-time type checking. For example, var gps = GetProperty<string>(PropertyId.Gps)
would compile, but a Gps is a double array, not a string. So it would throw an exception at runtime.
Also, Flater points out I'm corrupting the point of interfaces, and he is completely right. The reason I'm asking this question is because I think my answer is dirty; it's just the least dirty answer I have. The first and second approaches seemed worse (the original seemed much worse).
The solution would preferably be able to handle adding properties easily. I have no control over what data image formats decide to use. We have not written a single image format; we either get them from specs (like PNG), or as with about 95% of out formats, we reverse engineer them. That is the benefit our software brings; it understands, views and reports on rare file types (including image formats). About 70% of our time goes into reverse engineering new formats, which arrive on our doorstep faster than we can reverse engineer them.
The reverse engineering really hampers forward planning. You might find it hard to believe some of the data that is stored. I'm constantly surprised, and I've been doing this for over a decade. This means that we have to be reactive, as we can't be proactive.
When I used the tree of interfaces (I don't care if they inherit from IImage or from others as needed) I find that I do have fewer interfaces than there are image types, or properties, but still dozens. And checking to see if an object implements an interface doesn't feel much better than calling HasProperty
, but perhaps that is my own subjective issue.
Flater's suggestion seems to line up with my first attempt (the second model) a bit, and Simon B seems to be suggesting my current, second attempt (the third model) is best. I could be reading this wrong. If either is true, I'll live with the dirty feelings; it just felt like there must be some better approach out there, though I haven't found it.
I hope the context, though fake (but only a little fake) helps. I'm sorry I wasn't more clear the first time around. I hope this is better. I appreciate the time people took to help, and I will eventually accept an answer.
Old question kept for reference only
I am refactoring a smelly class and I'm sure I'm making a pig's ear of it. It feels like a common problem, but I can't see a common solution. As the domain is fairly niche, I've changed names etc.
I have an interface, let's say IThing
, which has a few methods and started with a few properties. As time went on, many different IThing
s cropped up with different properties. (IThing
is a sort of interface to multiple different reverse-engineered Things that we have no control over, so the properties are thrust on us.)
We ended up with a pattern of the sort bool HasSpecialNumber
, int SpecialNumber {get; set;}
. This got smelly as we added more and more properties, with every implementation having to implement 20+ methods just to say they don't support a property.
I thought of using a mixin approach, but maybe I'm thinking of this wrongly, because it would involve as many interfaces as properties or combinations of properties, and a lot of casting. It also seems heavy-handed when I'm only providing properties here and the methods are not changing.
An IThing looks sort of like this (C#ish pseudo-ish code)
IThing
// Some methods every Thing supports
DoSomething
DoSomethingElse
// A bunch of properties some Things support
bool HasSpecialNumber { get; }
int SpecialNumber { get; }
bool HasName { get; }
string Name { get; }
... and so on
Apart from the smell, every time a property was added, a whole bunch of classes broke. These all needed to be serialized too, using protobuf-net. Many of these classes were only distinct in that they had special objects.
The next thing we tried was reducing the properties to two methods, with a private method for adding properties.
IThing
// Some methods every Thing supports
DoSomething
DoSomethingElse
// A bunch of properties some Things support
bool HasProperty( PropertyIdEnum propertyId )
T GetProperty<T>( PropertyIdEnum propertyId )
// Private method for adding properties
void AddProperty<T>( PropertyIdEnum propertyId, T value )
This sort of worked. Dozens of properties became two accessor methods, and updating the PropertyIdEnum
didn't break anything. The AddProperty
was used to add properties to a dictionary that mapped the IDs to objects, with a Type
stored alongside to ensure no weird casting errors. But I exchanged compile-time type checking for run-time type checking. Also, protobuf-net doesn't support serializing Object
s or Type
s, though that is an implementation detail.
We ditched the AddProperty
abstraction and went back to dozens of classes. That resolved the protobuff-net issue at the cost of having a lot more classes to worry about. We still lack the compile-time type safety.
I see this issue all over the place in areas I work. For example, ffmpeg and the CODECs they deal with, each with special behaviour. The solutions they use are constrained by backwards compatibility though, an they are working in heavily optimized C while I'm in C#. Is there some pattern or advise for dealing with a run-away set of properties that need to be handled trough a single common interface? If I had control over the properties I would just not be in this situation in the first place, but I don't, so here I am.
int? SpecialNumber
? If your interface needs all those properties is a different question, maybe it does, but that is hard to answer by looking at meaningless terms likeThing
, without knowing the precise requirements.IThing
a supertype and buildThing
s as subtypes? You say you have attributes "all"Thing
s support, and attributes only "some"Thing
s support, that sounds like an inheritance problem to me, am I wrong?HasProperty
approach is fine, and the fact there is no compile-time safety is simply worth it. You can try to balance this by automated tests,