0

For example, I have an interface Requests that basically lists all the requests a client can make to a server program. Now an obvious problem arises where I have to add a new request to the program i.e. the interface. Existing implementing classes would then have to implement the newly added method in order to compile and that can't be any good. How then can interfaces be made to comply with the Open-Closed Principle?

  • Interfaces don't comply with OC principle, implementations do. – BobDalgleish Mar 4 at 16:49
2

What you have there is a wrong abstraction. The idea with abstractions is to arrange the code so that less stable components (code that changes more often over time) depend on components that are comparatively more stable. This is not something that you are necessarily going to get right from the start, as initially you probably do not understand the domain and codebase change patterns enough - so you'll have to course-correct until you get it roughly right, in the sense that the structure supports business needs well. There are other types and levels of abstraction in programming, but these that are closely related to the domain have to be based on business needs - you can't just come up with some "armchair philosophy" idealization and hope it will all work out.

So if you have other code depend on the Requests interface (callers via reference, implementers via realization), and this interface is not relatively stable, then the abstraction of "here's a list of specific requests that I support" is not going to work for you. You have to look closer at the domain and find a better abstraction.

Now, I realize that the example you've given may be contrived, just to illustrate the issue, but let's suppose it's not.

The fact that you keep adding requests means that the calling code cannot depend on the concrete request type. One way to approach that is to use the Command pattern - if it is possible to come up with a generalized request concept that is useful, then you can have your client code depend on a list of these generalized requests (and not know about their concrete types). Note that that means that you'll have to rethink how your code fits together conceptually. Maybe you'll have to move some logic from the client into each concrete request. Maybe some of the requests will have to be parameterized by injected behavior. Maybe you'll have to somehow separate out the logic that picks and coordinates these requests. But if you can pull it off, then you have a relatively stable abstraction where adding new requests doesn't affect client code (Open-Closed Principle), and where the degree of separate evolvability has increased.

That's just some food for thought; since none of us here has enough knowledge about your domain (simply because we're not on your project), don't take this as a recommendation - you (and your team) will have to take a closer look and figure out what works based on your knowledge of the domain (and on the ability to expand that knowledge).

P.S. In our fast-paced culture, ideas spread easily, but some of the substance becomes lost in the process. Here's Robert C. Martin's discussion of OCP published in C++ Report back in 1996: (waybackmachine link). The thing about OCP is that it has to be strategic:

It should be clear that no significant program can be 100% closed. [...] In general, no matter how “closed” a module is, there will always be some kind of change against which it is not closed.

Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close his design. This takes a certain amount of prescience derived from experience. The experienced designer knows the users and the industry well enough to judge the probability of different kinds of changes. He then makes sure that the open-closed principle is invoked for the most probable changes.

Design is all about trade-offs: you get something, but you also pay some price - you have to weigh out the pros and cons.

  • All the answers below are really great (particularly amon's exposition of the expression problem), but what really nailed it for me here was the emphasis on aiming for a "stable" abstraction – Garikai Mar 3 at 15:04
2

If you seriously want to follow the O/C principle, then you shouldn't add methods to a published interface. You could create a new interface which inherits from the existing but adds the new method.

Adding a new method to an interface is a modification, which should be avoided. Creating a new interface which extends the existing is an extension.

  • Understood, though I don't see it as a solution that would scale in the long run. Perhaps interfaces are just not suitable for the use case I gave but I just don't see any other reasonable alternative (besides maybe an abstract class--which I just have uncomfortable vibes about) – Garikai Mar 3 at 13:46
1

Changing an interface or base class violates the Open/Closed principle, and there is no way how that can be made compatible.

If you design a system in a way that satisfies the Open/Closed principle, then you can make some changes to the system without having to change the source code all over the place. Note:

  • This doesn't mean arbitrary changes are possible.
  • You still need to create suitable extension points up front. You can't generally retrofit the OCP without changing things first.
  • The OCP is most useful when you have clear API boundaries in your system, for example for the API of a library. The OCP has dubious value within a system, in particular within an application where you can just edit the code to accommodate your changes.

In your case there are two distinct interfaces:

  • the interface offered by some server. Adding new operations that the server supports is fine.
  • the interface in your source code that requires implementing classes to provide certain functionality. Requiring new functionality to be offered is not fine.

Sometimes, the latter aspect can be solved by not having an interface with all provided operations, but by having a separate object for each operation.

Compare this solution which allows new API providers to be implemented, but not new operations to be added:

interface API {
  void operation1();
  void operation2();
  void operation3();
}

class ImplementationA implements API { ... }
class ImplementationB implements API { ... }
class ImplementationC implements API { ... }

Compare with this solution which allows new operations to be implemented, but not new API providers:

// The API implementations are entirely separate.
class ProviderA { ... }
class ProviderB { ... }
class ProviderC { ... }

// Each operation must know how to execute itself on each API implementation.
interface Operation {
  void executeOnA(ProviderA a);
  void executeOnB(ProviderB b);
  void executeOnC(ProviderC c);
}

class Operation1 implements Operation { ... }
class Operation2 implements Operation { ... }
class Operation3 implements Operation { ... }

In practice, you cannot have both operations and API implementations extensible at the same time. This extensibility problem is also know as the expression problem, and attempted solutions are a really hairy and typically require advanced generics.

So in practice you have to pick one direction in which you want to keep your design extensible, with the understanding that extensions in other directions require your design to be modified.

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.