16

One thing I've long struggled with being able to grasp properly is, when designing a program in an object-oriented language, where and how should explicitly named/defined interfaces be added? In particular, I have heard at least the following statements on the matter:

  • The "SOLID principles", of which the last three (if not four!) at least are only applicable in the presence of interfaces, but it may be possible to interpret "D" as a stipulation of requirement of an interface.
  • "Always 'program to an interface, not an implementation'" - while this doesn't necessarily require explicitly using a interface-class inheritance but can be understood as "program to a specification of functionality not a way of implementing such", it does seem that in practice it tends to result in explicit interfaces (though this may depend on language), and finally
  • "Loose coupling" - this is perhaps the most overtly "interface-requiring" rule/stipulation that I can see, in that two things are loosely coupled precisely if that there is some intermediate abstraction between them so that changes in one are insulated from changes in the other.

However, if one tries to follow all these principles scrupulously, then it seems one must inherently end up with a large amount of explicit interfaces in a program of any nontrivial size. Now that's not necessarily "right" or "wrong" by itself, but it then leads to the following observations that have chronically created doubt in my mind:

  • Virtually nobody I've seen actually writes a program this way. That said, pretty much all code I've ever seen in my entire life has been that released for public dissemination so either open source projects or a minor section of formerly-proprietary projects. If the majority of code in the world is proprietary and this is where the most "well-designed" code can be found (which might make some sense, i.e. the best coders go to work for the best code houses which will naturally try to very closely and jealously guard their code to maximize profitability under existing business and political paradigms), then it may be natural I have never seen it; but

  • It seems in many very natural cases within the code itself the presence of interface is self-defeating because you have to access the explicit class in the same place you use it anyways. For example, suppose that C++ std::vector inherited an IVector interface (note: something like this actually exists in the C#/.NET, which I've also used - there, System.Collections.Generic.List inherits System.Collections.Generic.IEnumerable<>). Being rigorous, to keep coupling loose, I should ideally use IVector, for there might come some reason to create a second class implementing IVector (maybe I want a vector that's implemented differently, say that is actually a list, but has the same interface and I don't want to rewrite those code stretches). However, if a data member of a class I'm making is a vector (very common!), it has to be created somewhere, and it "feels silly" to always dependency inject a purely internal storage buffer that nobody on the outside necessarily needs to know explicitly, so then it's simplest to write my Whatsit's constructor to simply construct an std::vector directly. But then the Whatsit class already "knows" now about std::vector, the concrete class, and so it seems kind of silly to be trying to keep treating it only as IVector when the "cat's already out of the bag", so to speak.

  • One thing that I do know is an antipattern is where you always write a class and identical interface, e.g. every class is intentionally paired 1:1 in a doublet IWhatsit/Whatsit. However, that doesn't seem to really answer the questions in my mind, because while yes, sure, this may be the case, we have plenty of guidelines in the above 3 for how to design interfaces - in particular SOLID's O, I, and D principles do a good job (L is more for implementors). I don't make IWhatsit but instead analyze what my callers who receive Whatsit objects need, and then make interfaces accordingly. The problem is really the first 2 considerations.

Again, none of these are really contradictions - they're just things that "don't feel right", "design smells" maybe. So how should one best think about these issues to develop a good, solid and consistent way of thinking and working with interfaces and other such explicit language abstractions? Esp. given my chronic indecision and obsessive analysis of this topic has led many a project to never be completed due to constant rewriting/revision on trying to get the understanding of these principles right despite extensive reading of countless online pages, discussions, code bases, and even some books.

(After all, while "anything consistent is good" may not necessarily always be true, it does seem much more likely to my mind at least that "anything inconsistent is bad" is much closer to universal applicability, i.e. switching between [the fictitious] IVector and std::vector just because one happens to "feel" more right that day than on a previous day, in the same project, when it "seems" like it could go either way.)

3

7 Answers 7

23

I don't think there's really a simple answer that can do a substantially better job than the statements of the principles and practices that you've already encountered.

I know you're aware of this on some level, but I think what contributes to the confusion is that the word "interface" got a very specific connotation thanks to the popularity of Java, and later C#. There, "interface" denotes an "interface type", a language-specific notion declared via the interface keyword.

But the ideas underlying these heuristics and principles are applicable across languages, and in fact, many of them are not fundamentally OO-specific (although some of them are often expressed in OO terms).

An interface has another meaning that's more fundamental, and that predates the one stated above. An interface is the client-facing "API" of a component. A concrete class has an interface - it's the set of its public methods and properties. A function's "interface" is its signature (return type + parameters). A concrete or an abstract base class, by virtue of having an interface (as any other class does), defines a polymorphic top-level interface for it's descendants. The purely abstract class (or the Java/C# interface) is just a special case of that. A C# delegate (or a function-typed variable, or a function pointer) can be seen as a polymorphic interface for a family of functions. The aggregate root in DDD defines an interface for the whole aggregate. A module's interface is comprised of a number of classes and/or free functions and (parameter or return) types meant to work together. And so on.

Interfaces are about defining how other code should interact with an object (or some other construct), and about separating that "contract" from the internals, with respect to client code.

The principles / heuristics you listed (SOLID, "program to an interface", loose coupling) don't use the term "interface" in the narrower Java/C# sense.

These are not absolutes - there's a bit of an art to it

"Always 'program to an interface, not an implementation"

"Program to an interface, not an implementation" is largely attributed to the authors of the 1994 Design Patterns book; the idea itself probably predates it. There they introduce it by discussing the benefit of programing against an interface (in the traditional sense of the word) defined by an abstract class.

Note that the original statement doesn't contain "always" - it's not a command, it's advice. It exists in a larger context of design considerations, and you have to make a judgement-call regarding the extent to which you want to apply it.

The "SOLID principles", of which the last three (if not four!) at least are only applicable in the presence of interfaces.

I'd like to point out some things. The DIP does not actually use the word "interface". The term used is "abstraction". There's a reason for that: an interface is just one kind of abstraction - one that you'll commonly make use of, but not the only one.

For example, consider the Strategy Pattern - you implement an overall algorithm that defines, dependency inversion–style, an abstraction (what's called a required interface) for specific strategies. Client code then either has to pick an existing strategy to inject, or to provide its own implementation of that interface, in order to make use of the algorithm. Now consider many of the LINQ methods in C# - let's take the Where as an example. Where defines a generic filtering algorithm and it requires a predicate lambda that provides an externally injected filtering strategy. Your code has to either pick an existing predicate or roll its own. You see how, in terms of the overall structure, it's exactly the same as the strategy pattern? Yet the abstract strategy interface here is not a traditional interface at all.

This also provides an example of programming to an interface, and of the dependency inversion principle. Microsoft engineers that implemented this method had no way of knowing what kind of collection you'd be filtering, or how you'd want to filter it. Their code has to call your code, but cannot depend on it. Instead, both their code and your code depend on two key abstractions - one is the IEnumerable<T>, the other one is a Func<T, bool> and its associated predicate semantics.

One could ostensibly argue it's also an example of the interface segregation principle. In a silly hypothetical scenario, you could imagine a generic filtering algorithm that required an IFilterableEnumerable<T>, where, to use the library, you'd have to wrap a collection into this "augmented enumerable" that, when iterated over, can tell the library code if the element should be kept or not. That would be so cumbersome to use, and much less flexible. Instead, the Where method takes as its arguments two separate things - it segregates its dependencies into two concepts with clearly defined roles and a narrower set of responsibilities (remember, it's an extension method for IEnumerable<T> - which is actually just a static method that takes an IEnumerable<T> as its first parameter). Note also that this segregation does not prevent you to use our hypothetical IFilterableEnumerable<T> for both parameters. Again, a silly example, but you'll encounter such overly bundled interfaces "in the wild".

Similarly, many functions in C++ <algorithm> library rely on a number of abstractions such as iterators, execution policies, and various other things like predicates and comparers. All those define/provide interfaces in this broader sense.

Now, what I've been talking about so far is all library code, but I think you can see how you can apply these same principles in your own code when you have a layered or a modularized ("componentized") architecture, where you want to control the coupling and the direction of dependencies.

But wait, there's more

In the same vain, LSP is not about Java/C# interface-s at all. It isn't fundamentally about interfaces in the broader sense either. And it isn't about inheritance per se (although that will often be the mechanism for subtyping).

As originally stated by Barbara Liskov and Jeannette Wing, it's about types and their behavioral specification, and it defines what it means for something to be a subtype. In today's context, that something can be a derived class, or a lambda/function injected into some wider context, or a JavaScript object that doesn't inherit anything at all, but can still be used polymorphically because of duck-typing, or any such thing.

Essentially, LSP states that something can be considered a subtype of some other type if it can be shown that it exactly adheres to the abstract behavioral specification associated with that other type. The abstract behavior does not need to have a preexisting implementation to be well-defined; it's not about what the code actually does line by line, it's about what a sensible implementation should look like in a given context.

For example, suppose you write some library code, meant to be called by others, that makes use of the IComparer<T> interface via dependency injection. The key point is that not all of the semantics of this type are encoded in the interface itself; the languages we use are typically not expressive enough for that. This is why we write documentation, and things like unit tests (this is the sense in which unit tests are a runnable specification).

Your code will expect any implementation of IComparer<T> that's passed to it to exhibit certain sensible behaviors, the exact nature of which will depend on what your code is actually meant to be used for. E.g, it might require that, if an implementation's Compare method indicates that a < b and b < c, it should also indicate that a < c, for any a, b and c. In fact, suppose your code relies on this, and some other such "sensibility constraints", and that you've described this in your library's documentation. This defines an abstract behavioral specification for an IComparer<T> with respect to your library code - even though IComparer<T> by itself has no implementation (and does nothing in the literal sense).

If someone calls your library with an implementation that doesn't adhere to what you've specified, they've broken LSP with respect to your library (or with respect to that specification). Their code is not substitutable for an IComparer<T> in that context. The behavior of your code when given a non-compliant IComparer<T> implementation is undefined. If they choose to pass such an implementation to your code, it may produce unexpected results, or it may crash. Or it may work and do exactly what they wanted by chance - until you release the next version, and an entirely internal change breaks their code.

Note also that nothing in this scenario inherently requires the comparer abstraction to be an interface - it could, say, be a concrete class that provides a default implementation, that you could inherit from, and override. The substitutability terminology fits better in this context. (Yes, this would be less flexible since C# only allows a single base class, but I'm talking in principle.) In fact, in some languages and circumstances, the abstract type, if simple, might not have a direct representation in the code at all - e.g., the singature of the compareFunction parameter used by JavaScript's Array.sort is only specified in the documentation, along with the associated semantics (whereas in TypeScript or C# you'd have an explicit type for the function).

Where is all the well-designed code?

"If [... proprietary code ...] is where the most "well-designed" code can be found"

I doubt that. To me, it looks like the situation is a little different. It's not that superbly designed code is hidden behind closed source, it's that it really is not that common - because of a number of factors.

One is that these principles are not understood by the overall coding community as well as you might think. This is hard stuff, deeper and more involved than it looks on the surface. This state of affairs is perhaps not that surprising because of the boom that the software engineering industry has experienced. The huge influx of new people meant that at any point in time the percentage of really experienced people was tiny - and that it was hard to proliferate knowledge with the appropriate amount of depth (as this requires both reach and time). It also means that we keep reinventing the wheel. If you try to find information online, there's a lot of confusion and inconsistencies, and misconceptions that you have to wade through to get to the good bits.

The other one is just the practicalities and pressures of everyday business. When you need to ship the product, and you don't really see a path towards a more elegant design, and the deadline is looming - you ship the product. What should be done instead is a broad and difficult topic that has been and will continue to be the subject of many discussions and differing opinions.

And, ultimately, even the best programmers are just mortal humans - they are not going to produce stellar code all the time and in every circumstance. And not all projects require the same level of design (e.g. a one-off tool that you'll never update again is not going to benefit from an elaborate layered architecture).

It comes down to developing a deeper understanding

"However, if a data member of a class I'm making is a vector (very common!), it has to be created somewhere, and it "feels silly" to always dependency inject a purely internal storage buffer that nobody on the outside necessarily needs to know explicitly, so then it's simplest to write my Whatsit's constructor to simply construct an std::vector directly. But then the Whatsit class already "knows" now about std::vector, the concrete class, and so it seems kind of silly to be trying to keep treating it only as IVector when the "cat's already out of the bag", so to speak."

You're absolutely right. You would not always and systematically dependency-inject things just for the sake of it. This is why you have to know (or decide, or design) what your class is for. What its contract is - what's client facing, and what's internal. If the std::vector is something that's purely there to support the internal details of your implementation, then by all means, create it internally. If your class (or a method, or a free function) provides some higher level functionality that needs to be reusable with different kinds of collections, then inject a pair of iterators. Heck, you can have a scenario where you inject the iterators, but also create an std::vector internally to maintain a local copy of some range, or to use as a temp storage, or whatever.

One thing that I do know is an antipattern is where you always write a class and identical interface, e.g. every class is intentionally paired 1:1 in a doublet IWhatsit/Whatsit.

Yes - and, notice, there's a theme here: if you systematically follow a practice without understanding the reasoning behind it, you end up creating spaghetti code in a systematic way - with a very consistent look to it.

13
  • 4
    Your last point: "It comes down to developing a deeper understanding" is definitely 110% on the money. The trouble is, the amount of resources available for getting that seems seriously constrained, and your second to last point about "where all the well designed code" actually is, shows the problem there. But thanks for this very extensive answer. I'm accepting this answer with the caveat that the others also contain very important puzzle pieces, too. Commented Jul 16, 2022 at 18:43
  • Btw, I also was aware of the idea that an interface isn't just the explicit one, but rather where do you use such explications and/or if/when any of the principles mandate them when they exist in the language. Commented Jul 16, 2022 at 18:43
  • Good answer. Some suggestions - I'm not sure if they're worth the extra length - explicitly discussing parametric polymorphism and comparing languages implementing interfaces as concepts vs invasive inheritance. Commented Jul 17, 2022 at 16:12
  • 3
    @The_Sympathizer: It doesn't take enormous resources to develop a deeper understanding. All it takes is a few books, the willingness to ask questions, and a bit of common sense sprinkled with a fair amount of pragmatism. The SOLID principles are just that: principles. They are not laws, edicts or mandates. Use them to inform your development process, not dictate it. Commented Jul 18, 2022 at 13:04
  • 1
    @RobertHarvey I find your comment to be unhelpful and bordering on ignorant. If someone's having trouble understanding something, it does no good to tell them "all it takes". Maybe this sort of thing is easy enough for you, but even as an academic type, I've found that self-guided education is a significant effort. And as far as "common sense" is concerned, if you have to tell someone to try it, it must not be so common after all. It's rarely helpful to say, but can easily become condescending. Commented Jul 18, 2022 at 14:09
11

In a nutshell, you should define an interface when you need the ability to map that interface (i.e. capability) to one or more implementations.

What does that phrase mean?

As an example, if you're unit testing and need to pass a stub or mock into a class for testing purposes, then that constructor parameter should be defined as an interface so that you can pass an object that appropriately "simulates" the correct behavior.

This is also the basis for the Dependency Inversion principle; instead of a class taking concrete dependencies on other classes and instantiating them directly (thereby creating tight coupling), the class implements dependencies by allowing conforming objects to be passed into the constructor via interfaces.

In your List example, it's not a given that you need access to the List's entire set of capabilities. If you simply need to iterate over the list, it is sufficient to pass the list as an IEnumerable, and you're exercising the Liskov Substitution Principle by doing so. You don't have to couple yourself to the entire List class.

9
  • So does this mean it doesn't go against any good programming rules to otherwise directly pass & use concrete classes where you don't reasonably foresee a need to swap objects? I.e. you shouldn't "default" to passing inputs to functions and class constructors as interfaces unless you foresee multiple similar types of object like that going in? If there is no need for multiple implementations, having a hard dependency in, say, the user interface layer on logic-layer objects ("business logic" objects) is okay? As I've heard some suggest the separation between layers should be fully abstracted. Commented Jul 16, 2022 at 1:47
  • How then are we to properly understand DIP, which seems to be a sort of admonition to "create interfaces!" (or other suitably "abstract" decouplers)? Commented Jul 16, 2022 at 1:48
  • If you create a class that will only ever be used as one implementation of a capability (or will only ever be used in one other class), then you may not need interfaces, because you don't need implementation-swapping capabilities. You asked for a guiding principle; there it is. Commented Jul 16, 2022 at 1:53
  • There are also DI systems that bind an interface to an implementation through some sort of configuration, either in an aggregate root, or by decorating the classes with attributes, or using a configuration file. For those situations, yes, you always need constructor parameters that are interfaces, to allow the DI container to build your object graph by substituting the proper implementations. Commented Jul 16, 2022 at 1:55
  • 4
    @The_Sympathizer Most of the SOLID principles are context-dependent -- i.e. they depend upon the specific application domain and the different kinds of people (e,g, users, stakeholders, developers) who will determine how a project's requirements will change in future. The problems that SOLID set out to solve aren't really about what the code needs to do "right now"; a key driver behind SOLID is the understanding that code written and delivered into users' hands today will need to be rewritten/refactored in the future, so it's about minimising the risk of that going wrong. Commented Jul 16, 2022 at 7:15
7

You say you get nervous and lose time over-thinking the use of interfaces. And you seem to be seeking permission or an excuse to stop doing that.

I do not believe you need permission from anyone to stop doing something that does not feel helpful. So I suggest you stop immediately applying interfaces that do not present a clear benefit to you.

I am serious. You demonstrated you understand perfectly well what interfaces are about and what the benefits are supposed to be. While these are moot in your application, just ignore them and don't feel guilty. In time as the application grows the introduction of some interfaces will likely become appealing. That would be soon enough to introduce them and it won't be disruptive to what you have, the rework for the part that calls for interfaces should be smooth.

Now you will still have to deal with you OCD regarding inconsistency. Because you will then have some interfaces for classes that feel good, but other classes do not implement any interfaces yet! Are you near a window? You may want to look outside for a while and wonder why the trees have leaves and the birds don't. And whether that is a problem or not.

3

When, where, and how to introduce interfaces depends on whether decoupling an object from its collaborators is useful. The decision-making process that leads to "I need more than one implementation" begins with evaluating how beneficial it is to hide the implementation details between collaborating objects. What do you gain right now by introducing an abstraction (see also YAGNI)?

Decoupling objects becomes useful when components should be isolated from one another for testing purposes. This can happen because:

  • A collaborator has side effects, like persisting something to a database, web service, or file system. Tests that are not isolated from side effects are non-deterministic. Tests can potentially fail for reasons other than a fault in the logic of the system under test.

  • Initializing a collaborator is complex, which makes setting up a test more difficult. The added complexity of an interface is balanced by the convenience of simpler test setup.

  • Executing a function on a collaborator is resource-intensive in terms of CPU or memory usage. Sometimes you just need to return a predetermined response without bogging the system down and making your tests run too slow.

Test mocks are valid interface implementations. In fact, mocks are frequently the first interface implementations you write when practicing test driven development.

Testing, of course, is not the only reason to introduce an interface. The Dependency Inversion Principle states that high level and low level modules should interact with each other via interfaces. Typically the interfaces will be defined in the lower module for use in the higher module. The DIP does not define what a software module is, nor does it define "higher" or "lower". As the implementor of a software system, you decide what constitutes a module, and what "high" and "low" means with respect to module dependencies.

The DIP focuses on the interaction between classes that sit at the boundaries between modules. It says nothing about the interaction between classes within the same module. Concrete classes directly interacting with each other inside the same module is common. This implies classes within the same module do not necessarily need to be decoupled using interfaces.

In short: introduce interfaces at the boundaries between software modules. Changes to the internals of one module should not effect the others as long as the interfaces do not change.

There are many reasons you could conclude "I need an interface here." There is no general decision tree or flow chart you can follow that leads you to this conclusion. The two most common reasons or areas I come across are to promote testing, and to establish a stable contact for behavior that crosses the boundary between two application modules. Most other reasons tend to be situational with few similarities.

3
  • Sure. I'm not asking for a "decision tree" but rather clearing up the understanding of the various concepts involved - especially DIP's imperative language. Commented Jul 16, 2022 at 4:54
  • 1
    @The_Sympathizer: this answer does address the DIP. The problem with the DIP is that this principle is so abstract that it almost becomes meaningless. The term "module" is very abstract. The DIP is only understandable after you, as the author and designer of a specific software system, define what a module means for that specific software system. Commented Jul 16, 2022 at 11:16
  • @Breg Burghardt : Thanks. So then the DIP is a "mandate" to create interfaces, but you have to plug in the parameters as best suited to your project. Commented Jul 16, 2022 at 18:37
3

Every class has interfaces either explicit (implement some interface or conforms to some protocol) or implicit (features usable from the outside, hiding the internals).

You might read a lot of advices about using interfaces or programming to interfaces which refer to implicit interfaces. One example is GoF: most of the examples in that book are C++ code, sometimes with pure abstract classes (the C++ equivalent to interface), often, just referring to an implicit class.

So don't get mislead by ambiguity of words: you always need to think about interfaces when creating classes. This is not overenginering. It's just OOP. You would go for the extra effort of explicit interfaces in the following situations:

  • a class in your design should be replaceable with equivalent classes;
  • implicit interfaces or part of them, are to be reused, on objects of a broad family of potentially unrelated classes;
  • deliberate use of language support to force you think in terms of abstraction.

If you get nervous or stuck regarding the granularity of explicit interfaces and if further segregation is needed, then take a deep breath and just go on with a first version of your new interface. If you find out later that the interface should be split, you can always refactor.

6
  • This is right, and this adds an extra dimension viz. ISP (the "I" in "SOLID"): under that, we wouldn't necessarily want every consumer of that class to have 100% access to all its methods, right? (I.e. it's a "minimum knowledge principle".) Hence because passing it directly gives the full interface, we have to split the interface and that would mean adding explicit interfaces for passing, no? So then we do still end up with many if not most classes having interfaces. That's what I'm asking about: when to use explicit, separate interface specifications. Commented Jul 16, 2022 at 7:00
  • @The_Sympathizer I see. But ISP is a mean to an end, and not an end in itself. If you need to pass an object of a given class, you will not just create an interface for the sake of ISP. You'll use ISP if you decided that you want to reuse some code with a large family of classes sharing a common interface. So your question seems to have two questions: 1) when to use explicit interfaces 2) when stop decomposition of the interfaces in more elementary ones. I've reworded accordingly.
    – Christophe
    Commented Jul 16, 2022 at 7:39
  • Thanks, so again it really comes down to swapping code or otherwise needing interfaces as motivated by some other motivator. if you intend to insulate two objects or regions, the various interface principles, including ISP, come into play to shape the designing of those interfaces. But if you do not intend to use insulation, then the "inherent" interfaces do not need to be split up. And thus really, only DIP can be thought of as having any mandate for the creation of interfaces, and as explained elsewhere here, that one refers to module boundaries, not necessarily class boundaries. Commented Jul 16, 2022 at 7:51
  • @The_Sympathizer DIP is just a special case of reuse: you came up with a nice class (e.g. a storage manager) want to reuse it in different contexts, but you conceive that equivalent storage managers could also be used: 1) you create an interface; 2) how to get the object that implements the interface in the right place: DIP 3) would the interface require equivalent storage managers to implement something that is not needed for the purpose: ISP. If you knew it for sure from the start create the right interface from the start. If not just go through the steps one refactoring at a time
    – Christophe
    Commented Jul 16, 2022 at 8:08
  • So then DIP isn't a mandate to create interfaces at all, but yet another structural rule for when you do want to use interfaces? Commented Jul 16, 2022 at 8:11
2

it "feels silly" to always dependency inject a purely internal storage buffer that nobody on the outside necessarily needs to know explicitly

This is the crux of the issue. Interfaces are intended for public usage, where the consumer of an object should not rely on the concrete type.

Private implementations of a type that never leaves the class, nor have more than one concrete implementation, do not require an interface.

Commonly, data structures also tend to skip interfaces as they have no discernible logic for which it would be relevant to provide more than one implementation.

2

You write some code, say a function f. At some point you realise “My function f needs an object here that can do the following things…”

Before people learned about interfaces, they would have created a class C that could do all the required things, and add a parameter of type C to f, and add an instance of C to f’s parameter list.

That is a bit inflexible, but you can also create subclasses of C and pass instances of those subclasses. And then you realise you are actually interested in the subclasses only, and you make C an abstract base class.

And then you learn about interfaces and realise this is all nonsense. You were never interested in any classes, subclasses and so on. You just were interested in some very specific functionality. Any object supplying the functionality would be just fine. Even objects belonging to totally different and unrelated classes.

An interface just groups requirements and collects them under one name. So you define an interface xyzinterface which can do x, y and z, and your function f gets an argument of type “xyzinterface” and the caller can pass any object that implements xyzinterface. A class can then claim to implement an interface, so that (1) the compiler can check that it indeed implements all the function needed, and (2) you can’t pass objects that just by coincidence have methods with the right names.

So you define an interface when you realise that you require some functionality but nothing else. That realisation may come when you write the function f, or it may come later, after you wrote f with a class instance as a parameter, and you realise that the class actually implements much more than you needed. If someone tells you to change a class parameter to an interface parameter just because, for ONE class it makes little difference. No need to change it. You can do that at any time (later when you need it).

There are very simple interfaces like “equatable” (supports == and !=) or “hashable” (supports a function hash()). These two are enough to create an inefficient set/dictionary of equatable objects, or an efficient set/dictionary of hashable objects. You would never have an object that implements these interfaces and nothing else. You can have an interface that has the same functionality as some full blown class, but usually an interface is just for some very limited functionality.

1
  • This is the only answer here that is any good at this time. I'd like to emphasize the simple part: Interfaces are introduced primarily as parameter types. It's how you specify something that your class or method requires or accepts, without unnecessary restrictions on how clients might implement those things. Commented Jul 18, 2022 at 13:25

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.