Consider the code snippet
public interface Car
{
string getColor();
void Drive();
}
public class CarWithAutomaticTransition : Car
{
public string getColor() { return "Red"; }
public void Drive()
{
// Drive implementation...
}
}
Now consider a car with a manual transmission.
public interface IShiftable
{
void upShift();
void downShift();
}
public CarWithManualTransmission : Car, IShiftable
{
public string getColor() { return "Green"; }
public void drive()
{
// Drive implementation
}
public void upShift()
{
// upShift concrete implementation...
}
public void downShift()
{
// downShift concrete implementation...
}
}
It sounds stupid, but in efforts to reduce the number of "if's" in code, I've been reading about the null object pattern where (in this example) the IShiftable interface could actually be implemented in the automatic transition, but with no implementation. The alternative is to do a run-time-type-check on the object to see if it implements IShiftable.
The problems (tradeoffs) I see are the NullObject implementation seems to be somewhat misleading in that there's an interface with no implementation. However, you don't have to do run-time type checking and could just call that method if you need to.
The interface-specific implementation seems too specialized in that it's difficult to have one client handle all Car implementations be they manual or automatic transmission.
Is there an option I'm overlooking here, or is it a design problem and if so, an example would be appreciated.
1 Answer 1
You can reasonably make a case for either solution, but it would probably depend on the actual situation in code. Its hard to state a preference using the strawman "Car" example.
Since upShift
and downShift
are both no argument side effecting functions, deciding whether to perform those side effects as part of an if or as part of a method dispatch are strictly equivalent.
Depending on your specific language, there are some refinements you could make though.
For one, you can state that Shiftable
things must also be Car
s
public interface Car {
String getColor();
void drive();
}
public interface Shiftable extends Car {
void downShift();
void upShift();
}
This way when you test for the ability to shift, you know that the thing you have will have all the other related car methods.
You could also do this at the interface/trait/"polymorphism defining thing" level with some analogy to default methods.
public interface Car {
String getColor();
void drive();
default void upShift() {}
default void downShift() {}
}
In languages without nominal static types, the common idiom is to test for "capabilities" - usually the presence of duck typed methods on an object - rather than for specific concrete inheritance. So you can also just check for if upShift
exists and if downShift
exists and if so call them.
if car.responds_to(:up_shift)
car.up_shift
end
(I'm sure there are more, but this is all that popped into my head atm)
-
"For one, you can state that Shiftable things must also be Cars" Note that this is somewhat of a shortcut. It relies on the "luck" of not having other vehicles that can shift gears, such as bicycles or motorcycles. While I'm not particularly claiming that the example scenario must invariable also have these, it is important to note that your interface inheritance trick is often going to be a bad or imperfect approach due to invariably tying two separate things together.Flater– Flater2021年01月29日 09:38:17 +00:00Commented Jan 29, 2021 at 9:38
IShiftable
. The difference is that one shifts automatically, while the other does not.shiftUp
method would be largely the same in both an automatic and a manual car, as they both shift gears. It's more thedecideToShiftUp
logic that is different. This is why I mention that it's about imaginary requirements. You expect something completely different than Robert/me for whatshiftUp
would contain.shiftUp
, or is the act of shifting a gear the same even though the decision at which RPM ro shift is different?