0

I am faced with an interesting OOD problem: I have an interface with 3 methods:

interface TestInterface {
   String action1();
   String action2();
   String action3();
} 

and 3 classes that implement this interface, so I have a very simple code:

TestInterface testInt;

if(something) {
   testInt = createObject1();
} else if(somethingElse) {
   testInt = createObject2();
} else {
   testInt = createObject3();
}


if(againSomething) {
    testInt.action1();
} else if(againSomethingElse) {
    testInt.action2();
} else {
    testInt.action3();
}

This works fine, however I need to add additional action, but this action only makes sense in the context of one of the classes that implement TestInterface and not in the other two, so if I would to continue the pattern, I would have to add it to the interface, and make a dummy implementation in other classes, but at least the rest of the code would look nice:

interface TestInterface {
   String action1();
   String action2();
   String action3();
   String action4();
}



...
if(someSpecificAction) {
   testInt.action4();
}

I'm not quite keen on that kind of design, so I'm looking for an alternative. I was thinking about creating another interface, just with that method, but I was wondering if I can do better than this code below ?

interface TestInterface {
   String action1();
   String action2();
   String action3();
}

interface NewInterface {
   String action4();

}

TestInterface testInt;

if(something) {
   testInt = createObject1();
} else if(somethingElse) {
   testInt = createObject2();
} else if(something3) {
   testInt = createObject3();
} else {
   testInt = createObject3();
}


if(againSomething) {
    testInt.action1();
} else if(againSomethingElse) {
    testInt.action2();
} else if(something) {
    testInt.action3();
} else {
    NewInterface newI = (NewInterface)testInt;
    newI.action4();
}

Is there a better way to solve this problem ?

5
  • 2
    would make it so much easier if you provide context
    – Ewan
    Oct 5, 2022 at 19:08
  • 2
    What precisely bothers you about that solution? A lot of people actually recommend Interfaces with exactly one method (functional interfaces) in many contexts. Oct 5, 2022 at 20:17
  • @KilianFoth Bothers me that I don't know if there is a better way to do it, and that casting to a specific interface is not something that happens frequently so it begs the question is there a better way perhaps to design the code. Ewan, ah sorry I thought I provided enough information, is there something specific that I can add ?
    – Zed
    Oct 5, 2022 at 20:28
  • 1
    Would throwing an UnsupportedOperationException (like java collections) make sense? That is one "standard" Java way to handle this situation.
    – user949300
    Oct 5, 2022 at 22:50
  • Hard to tell without the actual context of the problem, but have you considered NewInterface to extend TestInterface? That way, every object that implements NewInterface also has everything of TestInterface but not the other way around. If action4() is a new feature to the TestInterface that is only applicable in certain situations, it might be the right solution. The answer of amon about the perspective of consumers is also definitely worth to consider.
    – Quido
    Oct 6, 2022 at 13:14

2 Answers 2

0

Think interfaces from the perspective of the consumer, not from the perspective of implementing classes.

If all consumers will need action4(), then yes it makes sense to add that method to the interface. But what should implementing classes do that do not provide such an action? This will depend entirely on context, but in many cases it might be feasible to do nothing and/or return some default value. In Java, the interface could also provide such a default implementation.

If only some consumers will need action4(), it probably doesn't make sense to add this operation to the interface. Indeed, creating a separate interface could be more appropriate.

Depending on context, it could make sense to let consumers attempt a cast to discover whether an object can be used via this additional interface. A more general approach is to add a method to the original interface like follows:

interface OriginalInterface {
  ...
  @Nullable default AdditionalInterface asAdditionalInterface() {
    return null;
  }
}

interface AdditionalInterface {
  void action4();
}

...
AdditionalInterface ext = instance.asAdditionalInterface();
if (ext != null) {
  ext.action4();
}

This has the advantage that objects do not have to return themselves, but can return other objects as adapters/decorators/proxies/…. Instead of using a nullable approach, it would also be possible to return null objects providing some default behaviour.

In any case, please do not imitate the misfeature of java.util.Iterator and others to raise an UnsupportedOperationException for unavailable methods. This approach is problematic because it is impossible to query in advance whether an operation is available. If an interface declares a method, and an object implements this interface, then all those methods should be usable. Anything else likely violates the Liskov Substitution Principle.

2
  • One could argue that attempting a cast, or adding a combinatorial explosion of interfaces, is even worse than the simplicity of throwing an UnsupportedOperationException. Good unit test coverage should catch the problem. LSP is one of many design principles. IMO, like most of SOLID, it's highly overrated. The nice thing about design principles and standards is that there are so many to choose from. :-)
    – user949300
    Oct 6, 2022 at 17:54
  • 1
    @user949300 There are certainly multiple valid approaches, but I have found it to be tremendously useful to work together with the type system to prove that the design is free of footguns. For example, using granular interfaces lets me prove that a suitable method exists, without having to rely on example-based tests. The LSP is also fundamentally different from the other “SOLID principles” in that it has a formal academic definition and simply describes how subtyping works, whereas the other principles are more like Robert Martin's opinions about good design.
    – amon
    Oct 7, 2022 at 10:00
0

You could have two interfaces: OldInterface and NewInterface, which is derived from OldInterface and implements an additional new method. Two of your classes implement only OldInterface, and a third implements NewInterface. If your code requires the new method then obviously it can only use objects implementing NewInterface.

Another possibility is that you add two methods to the original interface and don't create a new one: Bool implementsAction4() and String action4(). What's ugly is that all classes implementing the interface MUST have at least a stub implementation for action4() returning "" for example. And the caller must know to check implementsAction4() and no compile time checking is possible.

(Objective C has "optional" methods in interfaces that need no implementation. Before you call an optional method, you have to check whether it is nil or not. Which also means it's easy to know that a method is implemented and use that. So you can disable the menu item that would change the colour of an arbitrary object if that object's "setColor" method is nil. Swift allows you to try to cast objects to interfaces, giving nil if the object doesn't implement the interface. So you can write "if let action4Object = myObject as? NewInterface { call action4Object.action4 }", the as? is a cast that may fail and return nil. as! is a cast that is guaranteed to crash on failure. )

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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