1

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.

6
  • 4
    A manual transmission and an automatic transmission are both IShiftable. The difference is that one shifts automatically, while the other does not. Jan 28, 2021 at 22:56
  • Ha - you're right indeed! Would you say that in the automatic transmission case it could be an empty implementation?
    – cowboydan
    Jan 28, 2021 at 22:58
  • @cowboydan Your question in comments isn't meaningfully helpful. I mean that in the sense that you're asking about someone else's imaginary requirements about an already quite unspecified example with unknown requirements. But what Robert meant (correct me if I'm wrong) is that a shiftUp method would be largely the same in both an automatic and a manual car, as they both shift gears. It's more the decideToShiftUp logic that is different. This is why I mention that it's about imaginary requirements. You expect something completely different than Robert/me for what shiftUp would contain.
    – Flater
    Jan 29, 2021 at 2:02
  • [..] Additionally, there are real world considerations that change the design decision you might make. (a) Most automatics nowadays allow for tiptronic (i.e. human decision but automated shifting maneuver) (b) Does your code meaningfully distinguish between the implementation differences of a manual and automatic gearbox (i.e. the physical device, not the automatic gear shifter on top of it) (c) what about different automatic settings based on weather conditions? Do they affect shiftUp, or is the act of shifting a gear the same even though the decision at which RPM ro shift is different?
    – Flater
    Jan 29, 2021 at 2:06
  • [..] These are just some of many reasons why it's not that productive for two people to talk about their own interpretation of an unspecified requirement, because it's so very likely that things get lost in implicit translation.
    – Flater
    Jan 29, 2021 at 2:06

1 Answer 1

3

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 Cars

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)

1
  • "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
    Jan 29, 2021 at 9:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.