3

NOTE: This question is reposted from SO because it violates community guidelines for being opinion-based.

I have two classes that are similar in nature but they have different functional signatures. I am considering between having two interfaces vs having one. Let me illustrate:

Approach 1: two interfaces

public interface RaceCar {

    CompletableFuture<Double> drive(final Wheel arg1, Tactic tactic);

}

public interface Bus {

    CompletableFuture<Double> drive(final String someOtherKey, final Controller arg2);
}

One abstract class AbstractCar to share logic between the two:

public abstract AbstractCar { 
    // Add shared logic here... 
} 

And the implementation.


public class RaceCarImpl extends AbstractCar implements RaceCar {
... 
}


public class BusImpl extends AbstractCar implements Bus {
...
}

Results: 2 interfaces, 1 abstract, 2 implementation = 5 files.

Approach 2: One interface, two methods

public interface Vehicle { 

    CompletableFuture<Double> drive(final Wheel arg1, Tactic tactic);

    CompletableFuture<Double> drive(final String someOtherKey, final Controller arg2);

} 
public abstract AbstractVehicle implements Vehicle {
    // Sharing code here 
} 
public class RaceCar extends AbstractVehicle { 

    CompletableFuture<Double> drive(final Wheel arg1, Tactic tactic) { 
        // Implement this ... 
    } 

    CompletableFuture<Double> drive(final String someOtherKey, final Controller arg2) { 
        throw new IllegalStateException("Not implemented");  
    } 

} 


public class Bus extends AbstractVehicle { 

    CompletableFuture<Double> drive(final Wheel arg1, Tactic tactic) { 
        throw new IllegalStateException("Not implemented");  
    } 

    CompletableFuture<Double> drive(final String someOtherKey, final Controller arg2) { 
        // Implement this 
    } 

} 

Results: 1 interfaces, 1 abstract, 2 implementation = 4 files.

Verdict

I think having 1 interface is superior because it requires less files.

Which approach above incurs less technical debt in the long term?

6
  • 2
    A question closed for being opinion based on SO is likely to be closed here for the same reason. Moreover, I think despite the question was closed there, it got already some suitable answers, don't expect to get a better one here.
    – Doc Brown
    Oct 4, 2022 at 20:48
  • Asking "what after your thoughts?" is asking for opinions, which is universally off-topic across the StackExchange network. Asking which approach incurs less technical debt is very open-ended as well. Is there a particular problem you are trying to solve with the two different designs? Oct 4, 2022 at 23:50
  • @GregBurghardt Thank you. I took out "What are your thoughts"
    – Tinker
    Oct 5, 2022 at 6:20
  • 2
    @Christophe: ok, I voted to reopen. Still I don't like the fact we now have a crosspost on SO and here. And what I really don't like are questions which replace their original abstractions by cars, vehicles, "drive" methods - that makes it too contrived. These terms hide too much about the original context, which would be necessary to give an answer the OP could really apply to their original problem.
    – Doc Brown
    Oct 5, 2022 at 19:27
  • 1
    If the number of classes is so important, why 2 interfaces? Why not just the Abstract class and the 2 concrete? 3 is even superior than 4. You have to reason about why do you have interfaces and if they have the proper name.
    – Laiv
    Oct 6, 2022 at 7:15

4 Answers 4

6

Option 2 would be relevant only if you you would expect all vehicles to provide both functions.

But if your functions are only relevant to some subclasses, option 2 would violate the Interface Segregation Principle, which would be a very bad idea.

By the way, a good design is not judged on a quantitative base, but on its ability to solve a problem and to evolve when needed.

2
  • 1
    Not to mention that option 2 violates the Liskov Substitution Principle when using the sub classes through their abstract parent without even involving the interfaces. Oct 5, 2022 at 11:27
  • Option 2 is backed by the false assumption that the fewer number of classes the better... That way to approach OOP has all the recipes for the disaster. In OOP the economy of scarcity causes lot of issues. Like making everything super generic and abstract.
    – Laiv
    Oct 6, 2022 at 7:20
4

The number of files is irrelevant.

Option 2 is pointless. You want an interface to cover a particular aspect. If you start combining behaviors from different classes into a single interface you are violating the interface segregation principle. And the result will be useless.

2
  • Agreed 100%. Just a note for anyone reading this. Whether violating SIP is important or not is up to one self to decide. Things matter when things matter.
    – Laiv
    Oct 6, 2022 at 7:23
  • Of course, what an aspect covers can be different in different situations. Thus, an aspect might have to be split, or broadened, as the code base evolves. The more local it is, the less painful any change. Oct 7, 2022 at 17:22
1

When you use an interface X, you say “I don’t really care how an instance behaves, as long as it does the things the interface promises”.

And that isn’t true for the bus and race car. You need to treat them differently. You don’t want to and you can’t determine the “tactic” of a bus, but you need to determine it correctly for the race car. And for the race car it’s the same. Having an interface that both classes cannot implement properly, and where “instance implements interface X” doesn’t give the caller the information it needs, that’s rubbish.

And you don’t need 4 files. If you have closely related items you can put them into one or two files depending on the language (Swift has situations when you have to put several classes in the same file).

0

tl;dr Since Bus and Car differ only by the argument that they need to .drive(), you can just make that argument generic (Approaches 3 and 4). Alternatively, you might make the method itself a generic-lambda (Approach 5).


Approach 3: Describe the distinction generically.

Looks like the main difference between your examples is what information that they need to drive. You can do that with generics.

Basically:

  1. Bus and RaceCar are both Vehicle's that .drive().

  2. They differ only by what argument they need to .drive().

  3. So, make that argument generic.

// Language: Java.
public abstract class Vehicle <T extends I_DrivingMode> {
    public abstract CompletableFuture<Double> Drive(T drivingMode);
}

public class Bus extends Vehicle<I_BusDrivingMode>
{
    @Override
    public CompletableFuture<Double> Drive(I_BusDrivingMode drivingMode) {
        // ....
    }
}

public class RaceCar extends Vehicle<I_RaceCarDrivingMode>
{
    @Override
    public CompletableFuture<Double> Drive(I_RaceCarDrivingMode drivingMode) {
        // ....
    }
}

public interface I_DrivingMode {
}

public interface I_BusDrivingMode extends I_DrivingMode {
    String getSomeOtherKey();
    Controller getController();
}
public interface I_RaceCarDrivingMode extends I_DrivingMode {
    Wheel getWheel();
    Tactic getTactic();
}

public class SimpleBusDrivingMode implements I_BusDrivingMode {
    public SimpleBusDrivingMode(String someOtherKey, Controller controller) {
        super();
        this._someOtherKey = someOtherKey;
        this._controller = controller;
    }
    
    private String _someOtherKey;
    @Override
    public String getSomeOtherKey() {
        return _someOtherKey;
    }

    private Controller _controller;
    @Override
    public Controller getController() {
        return _controller;
    }
}
public class SimpleRaceCarDrivingMode implements I_RaceCarDrivingMode {
    public SimpleRaceCarDrivingMode(Wheel wheel, Tactic tactic) {
        super();
        this._wheel = wheel;
        this._tactic = tactic;
    }
    
    private Wheel _wheel;
    @Override
    public Wheel getWheel() {
        return _wheel;
    }

    private Tactic _tactic;
    @Override
    public Tactic getTactic() {
        return _tactic;
    }
}

Approach 4: Describe the distinction generically, with Tuple<>'s.

Like Approach 3, but lazier in languages that have sufficient generics, e.g. C#.

// Language: C#.
public abstract class Vehicle<T_DrivingArgs>
{
    public abstract double Drive(T_DrivingArgs drivingArgs);
}
public class RaceCar : Vehicle<(Wheel wheel, Tactic tactic)>
{
    public override double Drive((Wheel wheel, Tactic tactic) drivingArgs)
    {
        throw new NotImplementedException();
    }
}
public class Bus : Vehicle<(string someOtherKey, Controller controller)>
{
    public override double Drive((string someOtherKey, Controller controller) drivingArgs)
    {
        throw new NotImplementedException();
    }
}

Logically, this is much the same as Approach 3, just using Tuple<>'s as anonymous stand-ins for the argument-class's.


Approach 5: Describe the distinction generically, with delegates/lambdas.

This can be cleaner with generic-lambdas.

Two versions below:

  1. Minimal version.

  2. Expanded version with interface's and another abstract class.

// Language: C#.
public abstract class Vehicle<T_DrivingMethod> where T_DrivingMethod : System.Delegate
{
    public abstract T_DrivingMethod Drive { get; }
}
public class RaceCar : Vehicle<Func<Wheel, Tactic, double>>
{
    public override Func<Wheel, Tactic, double> Drive => (Wheel wheel, Tactic tactic) =>
    {
        throw new NotImplementedException();
    };
}
public class Bus : Vehicle<Func<string, Controller, double>>
{
    public override Func<string, Controller, double> Drive => (string someOtherKey, Controller controller) =>
    {
        throw new NotImplementedException();
    };
}

Alternatively, to define interface's and another abstract class:

public interface I_Vehicle { }
public interface I_Vehicle<T_DrivingMethod> : I_Vehicle where T_DrivingMethod : System.Delegate
{
    T_DrivingMethod Drive { get; }
}
public interface I_RaceCar : I_Vehicle<Func<Wheel, Tactic, double>> { }
public interface I_Bus : I_Vehicle<Func<string, Controller, double>> { }
public abstract class Vehicle : I_Vehicle { }
public abstract class Vehicle<T_DrivingMethod> : Vehicle, I_Vehicle<T_DrivingMethod> where T_DrivingMethod : System.Delegate
{
    public abstract T_DrivingMethod Drive { get; }
}
public class RaceCar : Vehicle<Func<Wheel, Tactic, double>>, I_RaceCar
{
    public override Func<Wheel, Tactic, double> Drive => (Wheel wheel, Tactic tactic) =>
    {
        throw new NotImplementedException();
    };
}
public class Bus : Vehicle<Func<string, Controller, double>>, I_Bus
{
    public override Func<string, Controller, double> Drive => (string someOtherKey, Controller controller) =>
    {
        throw new NotImplementedException();
    };
}

Optionally, the delegates can be named by:

  1. Add named-delegate definitions:

    public delegate double DriveRaceCarDelegate(Wheel wheel, Tactic tactic);
    public delegate double DriveBusDelegate(string someOtherKey, Controller controller);
    
  2. Replacements:

    • Replace Func<Wheel, Tactic, double> with DriveRaceCarDelegate.

    • Replace Func<string, Controller, double> with DriveBusDelegate.

Your Answer

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

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