I'm doing a bit of refactoring work for a software that use hardware, specifically cameras, to gather images and process them in different ways. A few different cameras are supported, and there will likely be different ones in the future, and therefore I thought it would be a good idea to hide the implementation of those cameas behind an interface, ICamera
, acting like some kind of Facade, providing the base functionality that any camera might have, and thus letting the main application logic remain untouched when any new camera is added.
Now, these cameras can have different features, and new cameras might be introduced in the future. I thought about defining additional interfaces, such as IZoom
for cameras that can zoom, so that the implentation of that zoom functionality can be hidden and any user interface elements can be reused.
However, now I'm a bit confused regarding "who" should keep track of these different interfaces (for example someone needs to tell the GUI to draw its control for an interface). I'm suspecting I'm closing in on a design problem.
So, my problem is that I want to prepare the software architecture for future hardware features, but at the same time letting the base functionality remain "untouched". Am I missing something basic, or am I trying too much? Is the problem creational or structural?
I visualized my idea below:
I've read up on a few structural design patterns (such as Visitor or Decorator) but my initial feeling is that they're not right for this problem. Please prove me wrong!
-
I would follow the YAGNI & KISS principles for now; if you "future proof" your code now in the wrong way (if you make the wrong assumptions before you have enough information to make the right design choices), you are going to end up with rigid code.Filip Milovanović– Filip Milovanović02/19/2020 17:40:25Commented Feb 19, 2020 at 17:40
-
The thing is that OOP abstractions need to be behavioral - ICamera needs to somehow be able to generalize some behavior of the cameras, so that it's possible to write client code in a generalized way, against that interface. If that's not the case, then maybe that particular hierarchy isn't the right way to go. Maybe you'll have cleaner code if you work with concrete types. Maybe there's some other abstraction that'll make all the difference. YAGNI let's you wait and see - but only if you reflect on how the code changes, and take the opportunity to restructure it when appropriate.Filip Milovanović– Filip Milovanović02/19/2020 17:40:29Commented Feb 19, 2020 at 17:40
2 Answers 2
Can you nest them?
i.e.
Interface IZoom
{
Tuple<int, int> Range {get;}
// ...
}
Interface ICamera {
IZoom Zoom {get;}
// ...
}
So if there is no zoom the property is null? It doesn't make the cleanest of code, since you have to test all the time if the feature is present. On the other hand, if you are drawing the item, you have to test for the presence of the interface anyways.
Sometimes it helps to have no-op elements, like
class NoZoom: IZoom
{
Tuple<int, int> Range => new Tuple<50, 50> // 50mm lens with no zoom
}
-
In my experience, this is the cleanest solution. The drawback that you need to have explicit tests is also a huge advantage: the types are much more explicit than with casts, and you can use adapter objects instead of complex class hierarchies. Your point about using the null object pattern is very good as well!amon– amon02/19/2020 13:52:21Commented Feb 19, 2020 at 13:52
-
I guess could be a decent strategy, considering the YAGNI and KISS principles mentioned in the comments above. Like you mention, I have to test for the presence of the interface (or however I represent different features) anyway.Mattias– Mattias02/20/2020 07:50:51Commented Feb 20, 2020 at 7:50
Typically, having multiple implementations of some feature would suggest a Strategy pattern: https://en.wikipedia.org/wiki/Strategy_pattern
In your case, the situation is a bit more complicated, because one class (for each type of camera) might be able to do multiple things: record, zoom, flash, etc. What a specific camera can do, is a responsibility that you have to assign to some class. You could consider assigning the responsibility to the camera itself. Any consumer (e.g. your UI or RecordingController) of the ICamera interface queries the features a camera supports and acts accordingly.
This is done, e.g. in the Bluetooth protocol: a bluetooth device can be queried what "profiles" (features) it supports, from a specified list.
Combined with the Strategy Pattern, you could create an interface (your ICamera interface) like this (using C# syntax):
public interface ICamera
{
void Connect();
void Disconnect();
void Start();
void Stop();
void Zoom(Direction zoomDirection, int zoomDistance);
void Flash();
CameraFeatures GetCameraFeatures();
}
This interface would have to contain all of the features that a camera might have. The superset of all cameras.
The CameraFeatures could be a structure/class anything you want to use to like:
public struct CameraFeatures
{
bool SupportsZoom;
int MaxZoomDistance;
bool SupportsFlash;
}
If you add a camera that only has existing features, you only have to implement a class for that Camera.
The downside to this pattern is that when you're adding a new feature (like you mean with ICoolFutureStuff), will require you to change the interface and the CameraFeatures struct and therefore the implementation of all of your camera classes and probably the UI because you have to use that feature too. Depending on how often you add features and how often you add cameras, you should make a decision. My guess would be that you add cameras way more often than you add features, so this would make sense.
Hope this helps.
Quido.
-
This suggestion reminds me of the GenICam interface that most of these cameras use, where one can query the cameras for feature lists.Mattias– Mattias02/20/2020 07:54:21Commented Feb 20, 2020 at 7:54
Explore related questions
See similar questions with these tags.