3

There is a discussion at work about the correct use of interfaces in OOP. I have been taught, and always worked from the premise, that interfaces precede concretions and all methods should be dealing in contracts. This is decoupling 101 to me.

I have found that applying this pattern universally teaches junior devs the ropes, sets me up for success down the road ("oh! all of this is tightly coupled! I can't use any of this!"), and takes very little time. It's simple (and worthwhile) to understand that we always, all the time, deal in contracts.

Another fellow is saying that abstraction should only be applied when there are multiple implementations and that doing it all the time makes things confusing for the team. For me, I don't care ;D it's only confusing at first and quickly becomes second nature, and to me is just the proper way of building.

But I wanted to reach out and see if anyone could provide some references / expert texts explaining why or why not.

froodley
  • 51
  • 1
  • 3
  • 4
    The main danger of creating an interface from a single class is that you will get it horribly wrong. Imagine if an ancient Australian defined the interface for IMammal. They'd all have a required `sizeOfPouch()` method... – user949300 Jun 07 '18 at 00:12
  • 6
    Are you confusing "interface" and "concretion" in software design terms with "interface" and "class" in Java/C#? You can program to an interface without actually writing a separate Java `interface`. – user253751 Jun 07 '18 at 00:53
  • 1
    I think "abstraction" has several and different meanings in software engineering. To me, interfaces have nothing to do with abstractions. Abstractions have nothing to do with implementation details. It has to do with design. Conceptualization. Something that you could achieve with or without interfaces. – Laiv Jun 07 '18 at 06:28
  • 2
    Classes, in the types of languages you imply, are both abstractions and implementations of that abstraction. Classes provide an “interface” (API). The interface keyword enables you to specify a class with no implementation, to work around language limitations. From the user’s perspective, neither a class nor an interface is any better or more abstract than the other. Interfaces may make construction more difficult. – Frank Hileman Jun 07 '18 at 12:07
  • @FrankHileman exactly, OP is basically making 2 interfaces for one implementation when it should be the opposite - 1 interface for 2+ implementations. – Meo Jun 07 '18 at 12:15
  • @Meo I agree, interfaces in such languages are only for multiple inheritance. But I don’t like the misuse of “abstraction” in the question. – Frank Hileman Jun 07 '18 at 14:05
  • An abstraction cannot be an implementation, by definition. – froodley Jun 07 '18 at 16:57
  • 1
    @froodley Correct, but a class in such languages is both an abstraction (the operations provided) and an implementation in one textual document. I.e. whereas in other languages they might be separate, such languages allow you to combine them into one unit. They can still be separated... a class can leave members unimplemented; if all are so, it is equivalent to an interface, without the multiple inheritance advantages. – Frank Hileman Jun 08 '18 at 00:22
  • I don't agree that a concrete class is abstract. Do you mean in the sense that it's a piece of computer code and not really a Car? I am referring to abstraction in the sense that I am dealing with something purely in the abstract, as a set of fields and operations it should have, an agreed-upon way of talking about the object which does not depend on receiving any specific implementation. Anything but a purely abstract class is no longer an abstraction, except in the sense that it's not actually a Car. – froodley Jun 08 '18 at 00:31
  • @froodley What do you mean by "concrete"? The terms in computer science, abstract data type, and abstraction levels, have nothing to do with the keyword "abstract" in Java and C#. The keyword would be better called "unimplemented" or in the case of a class "incomplete". Do you mean a data type that provides a constructor? An abstraction is more formally a set of operations as well as the semantics of those operations; in the languages we speak of, we can only specify semantics in documentation. All data types are abstractions -- implementation is not taken into account by users. – Frank Hileman Jun 08 '18 at 15:51
  • @froodley The constructor can only be provided by a class, but the class doesn't have to provide one, either. Is a class with no public constructor "concrete"? Users don't care where the implementation lies; they work only with the abstraction. – Frank Hileman Jun 08 '18 at 15:55
  • 1
    With regards to "decoupling 101" in the original post, an interface that matches the public members of a class 1:1 does not decouple anything, both because there is only one implementation (meaning, coupled to a single class still), and because you must update both at the same time when modifying the public signatures. At the same time, there is no benefit in decoupling something that should be coupled -- decoupling is for situations where the coupling is a problem. Superfluous interfaces are a form of cargo cult design. – Frank Hileman Jun 08 '18 at 16:05
  • If you have ever been on the wrong end of having your software declared to be tightly coupled, and it will cost x millions to decouple, you will understand that I refuse to develop tightly coupled software. Dealing in interfaces definitely does decouple your software even with only one implementation. Any consumer can replace your library with another that provides any implementation or facade over its own implementation that provides the same interface. I really have no idea where you're coming from; it all seems like sloth and slop to me. – froodley Jun 08 '18 at 16:10
  • As to cargo cult, I really don't agree that it serves no purpose. I believe you are correctly dealing in abstractions rather than specific implementations and that what you're saying is like saying making URLs configurable instead of hard-coding them is cargo cult because they probably won't change. – froodley Jun 08 '18 at 16:12
  • It seems you don't use the same terminology used in computer science in general. Classes are abstractions, even integers and floating point numbers are abstractions. Interfaces do not increase "abstraction": a more abstract data type is one that is more general and is higher in the type hierarchy; the implementation location is irrelevant from an abstraction perspective. – Frank Hileman Jun 08 '18 at 16:12
  • Again, if you have ever spent multiple years and millions address someone's YAGNI slop programming, you may see why I don't agree. – froodley Jun 08 '18 at 16:12
  • Everything in a computer is an abstraction. – froodley Jun 08 '18 at 16:13
  • An interface is more abstract than a concrete class, by definition. – froodley Jun 08 '18 at 16:13
  • It is a conceptual representation of an object rather than a specific implementation of that conceptual representation. – froodley Jun 08 '18 at 16:14
  • I have spent much time removing superfluous interfaces, which is why I label it cargo cult design. The only benefit is to increase the cost to develop software -- good for contractors and employees perhaps, but not for the people paying for the development. – Frank Hileman Jun 08 '18 at 16:15
  • It trivially increases costs initially while saving millions down the line when it's time to replace a library that now has dozens of consumers. – froodley Jun 08 '18 at 16:16
  • You have provided no explanation for this claim. I am telling you from the user's perspective, the only difference between interface (specialized class) and class, is the constructor. You would have to give an example where this cost millions to modify. – Frank Hileman Jun 08 '18 at 16:18
  • Service X has been deprecated because the backend system has been replaced. Service Y is created to serve the same purpose. We provide service Z to the dependency injector, and the children across multiple libraries and apps, expecting only a service that satisfies interface B, are completely unaffected. – froodley Jun 08 '18 at 16:21
  • We provide service Y* – froodley Jun 08 '18 at 16:22
  • alternative: Service X is injected directly everywhere. It no longer works at all. Every downstream consumer has to be altered, some in other companies, some in software that was developed by developers who have left the company. – froodley Jun 08 '18 at 16:23
  • Explain why Y benefits from an abstract class exactly. The abstraction is identical correct? No code changes. There are constructor calls in millions of places, keeping in mind that classes don't have to provide constructors either? Dependency injection is far older than C# and Java. – Frank Hileman Jun 08 '18 at 16:23
  • If I say you have to give me a ServiceX object, I cannot give you a ServiceY object. The compiler will prevent it. If I say you have to give me an InterfaceB object, you can give me either one. – froodley Jun 08 '18 at 16:24
  • 1
    I don't see your logic. If the API is identical, the swap is seamless, regardless of the use of an interface, concrete class, or abstract class. The only difference is construction, which can be isolated in many different ways, not just via DI. – Frank Hileman Jun 08 '18 at 16:26
  • InterfaceB provides value only when you use both services at the same time (multiple inheritance) in the same process. But you can also gain that benefit by using a base class. – Frank Hileman Jun 08 '18 at 16:29
  • What language do you work in? You cannot inject ServiceY into a class expecting ServiceX in any strongly-typed language. – froodley Jun 08 '18 at 17:30
  • If replacing one type with another, you either use a base class, or change the original type. Any OO language would work. You are describing a situation where you either need both types at the same time, or there are different organizations authoring components (plug ins). – Frank Hileman Jun 08 '18 at 17:43
  • If replacing one type for another, I change nothing but which factory I provide the injector in the config. It spins up another class that implements interfaceB and no code is changed at all. – froodley Jun 08 '18 at 17:47
  • In general, expensive changes are not name changes to types, but invasive changes to APIs. DI and interfaces do nothing to help. Your scenario is unrelated to saving work. – Frank Hileman Jun 08 '18 at 18:35
  • @froodley in language do you work? – Meo Jun 08 '18 at 23:54

5 Answers5

7

I don't want to even know if I'm using an interface (the keyword kind). My drive function takes a Car. Whether that's concrete or not is not any of its business.

This is why I'm annoyed by C#'s ICar convention. Get that pointless I noise outta my face. Java isn't much better. Oh sure, by convention, I'm allowed to name an interface, abstract class, or concrete class Car in the source code but if I change from one to the other I have to recompile everything that uses it!

All I want is to express what drive()s needs are in the type system. I have no desire for drive() to know what it's talking to beyond knowing that whatever it is, it knows how to listen.

By the way, if you have good tests, a language with duck typing gives you all this for free.

But since I have to use these languages as I find them I tend to use role interfaces in them. Which means I don't write drive(Car car) I write drive(DriverControls driverControls) and whatever wants to accept steering, accelerating, and breaking messages over the DriverControls protocol is free to do so.

So if that's what you're talking about I'm with you. If you're one of those fanatics that insists every class like Car have an ICar counterpart you can go jump in a lake.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 3
    In Java, your DriverControls interface would commonly be named Drivable. – Jan Van den bosch Jun 07 '18 at 09:04
  • Basically for the reason you gave, I am. I don't want to deal in some specific thing called a Car that does things some one specific way. To me that is just one-off, trash code. I want to deal in interactions with a contract. I know what I'm getting knows about .drive() and .steer(), and returns me a string, and I don't need to know whether it's a car or a boat. – froodley Jun 07 '18 at 17:03
  • 1
    @froodley Agreed. Which means the real debate should be about the trouble that can be caused when you use concrete classes first and later refactor them to interfaces. It's hard to do this and respect the open closed principle. Worse is how easily it is for awareness of `Car`s concrete status to spread. I explored trying to head that off while staying concrete [here](https://softwareengineering.stackexchange.com/q/257976/131624). It's not pretty. – candied_orange Jun 07 '18 at 18:46
4

I agree with your coworker.

Creating good abstractions is hard. Like, really, really HARD!

Abstractions add complextiy to software. They make it harder to navigate the code and reason about it. They make things harder to change, as changes across abstractions often require changes in multiple places.

An interface that just copies what methods the class has is not a good abstraction. Only time you can tell you have good abstraction is that you can imagine (or actually have) multiple different implementations.

Whenever you are creating an abstraction, you need to ask yourself a question "Is it really worth it complicating the code so I can decouple these pieces?"

Euphoric
  • 36,735
  • 6
  • 78
  • 110
  • 1
    Creating good abstractions is hard because they remove complexity. Bad abstractions instead add to it. – Deduplicator Jun 07 '18 at 12:07
  • Creating great abstractions is hard, creating abstractions that serve the purpose of decoupling is not that hard. If my consumers can replace my module or class with another module or class that can serve up the same signature, I feel I've done the job correctly and they are not tightly coupled to what I've done. – froodley Jun 07 '18 at 23:29
  • 3
    @froodley If your interface and class have the same signature, you haven't decoupled anything, other than construction. – Frank Hileman Jun 08 '18 at 23:10
4

You are missing one big piece of the puzzle:

Using interfaces everywhere makes your code much harder to navigate

Assume you have a large codebase with hundreds of classes that has grown over the years.

Usually, when you want to know what happens in a method, you just click on it and the IDE will jump to its code ...that is, if it's a class. If it's an interface, you'll jump to the interface, then you have to figure out what class is instanciated, and this might take a while, and go there, and go inside another of its method and again land on an interface, so you have to figure out again what was actually instanciated, which can take a while, etc.

Basically, navigating the code become increasingly frustrating because of superflous interfaces.

My rule of thumb: use an interface when you indeed have multiple different implementations behind, not just for the sake of it.


EDIT:

My "much harder" was probably exagerated. I should simply have written "harder" or "inconvinient".

To illustrate this, in eclipse. With a method from a class:

  • Ctrl+click jumps inside the method (1 click).

With a method from an interface, it would be:

  • Ctrl+click
  • move on top
  • right click on interace name
  • open type explorer
  • select class
  • open outline
  • go to method

...So, yeah, you can do it with good IDE support, but it's still quite annoying.

dagnelies
  • 5,415
  • 3
  • 20
  • 26
  • 1
    Any IDE worth its salt knows how to jump to implementations. This is in no way a valid point unless you're using poor tools. – Kayaman Jun 07 '18 at 10:26
  • @Kayaman Not if you use poor abstractions, try to find that one specific implementation by its Runnable interface. – Meo Jun 07 '18 at 11:27
  • 1
    @Meo we're not talking about `Runnable` here, that's a faulty made up example similar to "what if you want to find a specific subclass of `Object`". There's no problems in finding a specific implementation of `MyInterface`. Unless of course you've decided to have all your custom classes implement `MyInterface` for no reason, but then there are bigger problems involved. – Kayaman Jun 07 '18 at 11:32
  • @Kayaman if you overgeneralize everything, then you will inevitably face the same problem. If having an interface is the norm, someone will sooner or later save the time and use existing one where they should not and then IDE will offer illogical implementations. – Meo Jun 07 '18 at 11:37
  • @Meo It would be realistic only in a situation where the rule is "every class must implement an interface", without any condition about **which** interface. I don't see that very likely in a professional environment. I mean if we're working in a hostile environment the interfaces won't do any good, because someone can knowingly or unknowingly write incorrect implementations. Does that mean interfaces are useless, because they don't guarantee that implementations are correct? – Kayaman Jun 07 '18 at 11:49
  • @Kayaman if every class would have its own interface, or the tightest fitting interface needed, then I would agree with you that the IDE should handle it just fine. And you know what is the tightest fitting interface? The class itself. Expecting no navigation cost from overused interfaces is just naive. – Meo Jun 07 '18 at 11:58
  • @Meo I'm in no way advocating that *every* class needs to implement an interface. It should be made clear to any junior developers as to what kind of classes warrant an interface (such as services) and what don't (e.g. entities, value objects). If there's no architecture or guidelines, then it doesn't really matter as the codebase would be in bad shape anyway. – Kayaman Jun 07 '18 at 12:06
  • @Kayaman No, what you are saying is that jumping from "superflous interfaces" to implementation or back has no cost. Wrong. – Meo Jun 07 '18 at 12:10
  • @Meo don't put words in my mouth and then say "Wrong". I'm saying there's no reason to avoid interfaces with a single implementation for example. I'm saying don't have all classes implement an interface, since that's just silly (especially if it causes all classes to implement `Runnable` due to laziness). I'm saying use your experience (or ask someone more experienced) when making design decisions. I'm not sure whether the cost you're talking about is development cost or runtime cost, but in any case that's negligible. It's like micro-optimization by avoiding interfaces. – Kayaman Jun 07 '18 at 12:19
  • @Kayaman I was referring to your first comment. The truth is that "navigating the code become increasingly frustrating because of superflous interfaces." and no tool can change that. You wrote that it is not a valid point, which is wrong. – Meo Jun 07 '18 at 12:24
  • @Meo I think the post was edited after my comment. I don't advocate interfaces for every class, and I don't advocate an architecture so bad that you can't follow what's happening. – Kayaman Jun 07 '18 at 12:30
  • @Kayaman we would see if it was edited, it was not. – Meo Jun 07 '18 at 12:33
  • 1
    Hi ho! Take it easy guys! ;) My "much harder" was probably exagerated. I should simply have written "harder" or "inconvinient". To illustrate this, with a class method in eclipse, Ctrl+click jumps inside the method (1 click). With an interface, it would be Ctrl+click, move on top, right click on interace name, open type explorer, select class, open outline, go to method. So, yeah, you can do it with IDE support, but it's still 7 clicks or so, which is still quite annoying. – dagnelies Jun 07 '18 at 13:40
  • It's harder. It's also harder not to hard-code passwords into the codebase or put business logic in your models. It's stricture. It's a "do you want a job, or not" kind of thing to me. – froodley Jun 07 '18 at 23:22
  • 2
    @froodley: IMHO, adding interfaces systematically is just adding useless noise. It doesn't improve decoupling, nor does it ensure that the underlying implementation respects the contract. It just adds unnecessary indirections, noise and annoys seasonned developers. There are a lot of cases where interfaces are useful, but applying the systematically is IMHO just stupid. – dagnelies Jun 08 '18 at 12:09
  • It's not useless at all. If I hand you a class or a library that deals only in concretions, you are tightly coupling yourself to my work, and we have both failed out of the box. In my experience, it annoys impatient junior developers, while seasoned developers want to deal in contracts and realize it's suicide not to. – froodley Jun 08 '18 at 12:52
  • 1
    @froodley Whether the lib uses interfaces or classes, I'm tying myself to those. I doesn't matter. If I call `myLib.doThat(someArg)`, it does not matter if `myLib` is an interface or a class. If I truly what to stay somewhat independent of it, I would have to build a wrapper around it. However, in most cases, adding such abstractions is more a hindrance than useful. It's pretty rare to switch dependencies, and if you do, you'll usually notice that your wrapper doesn't fit the other lib very well, leading to changes in two places instead of one. – dagnelies Jun 08 '18 at 13:00
  • It absolutely does matter. If you can replace myLib with someLib that implements the same signature without changing your code at all, much less spending 2 million dollars replacing the thousands of hard references you made to myLib, then what I gave you was a language instead of a machine you are tied to forever. There are higher and harder and more expensive lands of decoupling to accomplish even looser association, but if you deal only in abstractions your software is by default very loosely coupled and can be replaced without any changes to consuming code. – froodley Jun 08 '18 at 17:44
  • @froodley Replacing a dependency: once every few years. Navigating/understanding/refactoring the code: every single day. Place interfaces everywhere if you fancy it, I don't care, but please maintain your stuff in the long run too. – dagnelies Jun 10 '18 at 20:07
1

Decoupling is not a good thing by itself. It is only a good thing where you need it, and bad everywhere else. If you are using an interface on a place where should be high cohesion, then you are doing it wrong.

Every interface has a cost and a value, if you are not considering it and blindly make them everywhere, then you are doing it simply wrong.

Meo
  • 111
  • 2
  • That's your take, mine is quite the opposite. High cohesion conceptually within a module is still achieved when you deal with contracts that are closely related. I am of the opinion that building based on contracts all the time removes any ambiguity and enforces the right kind of thinking, while resulting in a highly-swappable codebase "for cheap" compared to trying to decouple later, when many consumers already depend on your concretions. But again, I'm looking for some references to reading material on the subject. – froodley Jun 07 '18 at 23:26
  • 1
    @froodley From the user's point of view, the only dependency difference between what you call a "concrete" class and an "interface" is the constructor dependency. You can construct a class that provides a constructor. You can't construct specialized classes that provide no constructor, such as an interface. An interface and a class are identical from the users point of view, otherwise. – Frank Hileman Jun 08 '18 at 00:27
  • An interface is a contract. A class is a machine that produces some implementation of that contract. These are very, very different. I am not convinced at all that dealing in concrete machines is the same as dealing in contracts. – froodley Jun 08 '18 at 12:54
  • @froodley A contract is a set of operations and semantics. An interface is simply a set of operations, a set of signatures, that looks exactly the same as a class, from the user's perspective. As far as I can tell, what you call "concrete" is the presence of a constructor in a class. Decoupling would mean that you can change the API independently of the implementation. A class can do this, an interface can do this, but real decoupling means using several interfaces or a dynamic language. – Frank Hileman Jun 08 '18 at 23:14
0

I program data business applications

Those generally doesn't suite for the classic "true" OOP where the object implements its own behaviour because of all business rules that depends of the current connected user and so on. Instead I have the well known anemic model + services, because I didn't find anything that suit more to my needs, except for some independant technicals components.

My services have always interfaces, because even if I have one implementation, there will be a time where I want to mock them. I abstract a little but not too much since at the time I do it, I don't know that much how much I could need and how to do it properly, and my time is better spênt that wasting time on that (YAGNI/KISS).

Of course since I start with only one implementation, my interface is pretty much a copy of whats my implementation need to expose but that's on purpose. If I need to have another implementation that will by nature force my to refactor my interface to a more generic one, and I will do it when it's need to be.

Note : I use regular name for the interface of my services I use (no "I" or whatever), because the consumer of those service really don't care they're interfaces as said @candied_orange

Walfrat
  • 3,456
  • 13
  • 26
  • In Java you could mock without interfaces exactly the same as with them (using Mockito and such), unless you would want to implement the class manually of course. – Meo Jun 07 '18 at 12:26