9

[I'm using the term interface here with C#'s interface in mind. I won't tag the question with a C# tag because it really isn't a C# question.]

In my work we do unit and integration tests but we don't follow TDD. The tests are often written after the classes.

In this work, I often see interfaces that matches the whole classes public interface (in the way we end up with a class X implementing an interface IX, which contains every single public method of X).

When I asked the devs about why they did that that way -- as to me it seems to only increase complexity of the code by adding seemingly unnecessary lines of code (assuming every class already has its own public interface, then why decouple them if the interface will be exactly the same?), the answer was that it is made that way to be able to mock the class when testing other components in the code that depends on this new class X that is being created.

I know the ideal would be to segregate X's own public interface into multiple different interfaces and only do that when it makes sense to have X's functionalities abstracted (ie. in cases where we may end up having more than one implementation of those functionalities).

But then a few question came to my mind...

  • Are the devs in my work correct about that approach? To me it really seems very wrong -- even though I can't argue a better solution to that.
  • Why do we often read we should only create abstractions when polymorphism will be used in a given class that is being created? Doesn't this contradict the Dependency Inversion Principle? It will often lead to classes that don't implement any interface, forcing implementations to be coupled with each other. Besides, how do we mock such classes when implementing tests?
10
  • 6
    Asking one question at a time (instead of two in one) may increase the chance for a question not to get "needs more focus" close votes. The format of this site works better when sticking to "one question - one answer". Moreover, when you write a sentence like "Why we often read we should only create abstractions when polymorphism will be used in a given class that is being created?", better give references to such a statement. However, this is not a guarantee.
    – Doc Brown
    May 23, 2022 at 7:22
  • 2
    I've commonly seen this practice in C# or Java done by former C++ programmers, who treat their interfaces almost like C++ header files. They're used to the whole "declare all functions here, and define them all there" mode of thinking. May 23, 2022 at 13:32
  • 2
    I haven't voted at all, but the title certainly provokes downvotes: Why is bloat/trickiness/unidiomatic code/excessive terseness/... bad? Ehm, you really ask why excess is bad? May 23, 2022 at 18:01
  • 1
    @Deduplicator hmm yes? The same as in thin vs fat domain models... Fat models are preferred over thin ones. Don't know why you pointed that... May 23, 2022 at 18:05
  • 1
    @underthevoid There are reasons to go for a fat versus a thin one, but neither term inherently means excess, and thus bad. May 23, 2022 at 18:06

3 Answers 3

9

The interface segregation principle (ISP) stands regardless one's desire to test classes. But if you never bothered to decouple (because there was no apparent need) and then you must write tests, the quickest way will be to just make that 1-to-1 interface and be done with it. Which will then be updated with every public method added to the class. This is what I have seen a lot too.

So your co-workers are probably right when they cite testing as the reason for things having become that way. Although it would have been better to have meaningful abstractions in the model first and then write tests against those.

Regarding your second bullet point, that basically states there is no point in having an abstraction if there is (and will be) only one implementation, which makes sense. The ability to test was never a consideration to OO modeling and rightfully so. So sometimes developers get pragmatic. It is ugly but it may be worth it if the test is really important. Did you encounter methods yet that are commented with "this method is used only for testing"?

2
  • 1
    However, what if there is one implementation but multiple callers who only need parts of that functionality? Say X has two methods mthd1 and mthd2, but classes P and Q both take an X, but P only calls mthd1, and Q only calls mthd2. There will be only one implementation of X, though. Should one add interfaces to appease ISP & DIP? May 28, 2022 at 1:37
  • 1
    @The_Sympathizer Possibly. A class having two different use cases could indicate that two separate interfaces would be appropriate. Or even two different classes. Or two descendants from a common base class. The way to go would depend on the meaning of the use cases and your practical needs. Appeasing a principle should not be the goal, the principles are guidelines that help you spot common mistakes and suggest solutions for common problems. Applying one should make your life or that of your successor easier. May 28, 2022 at 6:26
0

Bloat is always bad, because it costs resources (time, money, attention, efficiency, space) without giving value.

Now in the specific case of bloated interfaces, they violate the Interface segregation principle.

In simple terms, it means that any interface should only bundle the interface for doing one thing, and one thing only. Thus, you aren't forced to provide a fake-implementation for quacking, eating grass, and flying, just because you also want to model a dog, which might only share running around. This stronger focusing of the interface also allows the compiler to detect more logic errors as type errors, facilitating correctness.

What does the common Mirror-interface (or God-interface) do?
Well, it's just to enable mocking a specific class, so everything it does is relevant. It's a solution for a specific technical limitation of certain tools. As long as it is used just for that, everything can just stutter along.

Unless and until you have reason to want a specific interface to do a specific thing, and thus know what it should do, don't try to design it. YAGNI saves much wasted time and code bloat.

2
  • 1
    If classes which have different overlapping subsets of abilities implement a common interface which combines those abilities with CanXXX properties, then a common wrapper class may be used to handle all of them, exposing the same subsets of features as the wrapped classes. Omitting members like count from the Java/.NET Enumerable/IEnumerable interfaces makes it very awkward to produce an efficient class which can behave as a concatenation of two or more enumerable collections.
    – supercat
    May 23, 2022 at 21:51
  • @supercat Or you could also have a bigger interface subsuming enumerable and countable. But yes, sometimes having a method with such a beautiful contract as "Will frob if frobbable, and otherwise throw not-implemented" and thus opting out of static verification is the most appropriate. For every rule, there is an exception. I don't think it detract from my point though. Especially as the OP asks mainly about interfaces mirroring the whole implementing class. May 23, 2022 at 22:33
0

In this work, I often see interfaces that matches the whole classes public interface (in the way we end up with a class X implementing an interface IX, which contains every single public method of X).

This is because such a class usually exists specifically to provide a concrete implementation of said interface, and tends not to have any unrelated implementations as they wouldn't be accessible if not part of the interface.

It's also not quite correct that such a class has no additional features. One major part is that the selection of which dependencies need to be injected are solely decided by the concrete class, not the interface.
Sufficiently complex operations will also likely lead to several private submethods in that class; when relevant.

When I asked the devs about why they did that that way -- as to me it seems to only increase complexity of the code by adding seemingly unnecessary lines of code

There is a very important to make here. I'm bolding it because it is an essential guideline to remember across your entire career as a software developer.

Complexity is not measured in lines of code.

A properly abstracted codebase will contain significantly more lines of code than a monolithic god class, but it will be easier to read and maintain, because it has been broken down into separate small bite-sized and easily digestible chunks of logic. This makes it much easier to look up a specific behavior or change a specific thing without affecting other things in your codebase.

When you build a codebase from scratch, there is a bit of effort that goes into how you structure and relate all these individual components. It's easy (and understandable) to find this needlessly complicated when you're not familiar with this approach.

However, that structure is generally a build-once effort. When your application is past the architectural setup stage, and you're in the latter stages of development or code maintenance, your focus will shift from how these components relate to one another to what is in each specific component (i.e. the actual implementation of the business requirements).

The initial cost of properly separating your problem domain into neat compartmentalized components will pay significant dividends in the long run, in terms of readability and maintanability of the code, (unit) testability of the behavior, and subsequently the ability to protect against the introduction of bugs and mistakes in your codebase. The larger a codebase gets, the easier it is to cause an unintended effect in another part of your codebase whenever you make a change. The cleaner your approach, the lower this chance is.


If you'll allow an analogy, your point of view is not unlike a medieval builder wondering why modern day builders spend so much time digging a hole instead of just starting to build the house that they're supposed to build. This is because medieval builders are not familiar with foundations (nor concrete), and they don't quite understand why you'd go to all this effort, because they don't see the benefits.

However, as you keep building a bigger and bigger house, the medieval builder's house would start to become unmanageable because its earthen foundation cannot reasonably support the kinds of skyscrapers that modern day builders build.

The problem is that to avoid this problem, you need to have laid a concrete foundation before you get to the stage where the problem of having a too large building becomes apparent. You cannot just slap a house together and fix the foundation only when the house becomes too big.

Right now, you're not seeing into the future far enough to see the problems that this interface segregation will prevent (or significantly suppress). You really should trust more senior developers on this. When you complete this project, you will see how the interfaces helped you along the way, and when you start the next project, you'll understand why you're implementing all these additional steps that today look unneccessary to you.

3
  • Good information overall but I guess you might got the question a little bit off. I was not asking about why interface segregation is good, I already know the reasons why we should segregate bigger interfaces into smaller, more cohesive ones. The question was about the cases when we have a class which has all of its public methods put in only 1 interface only for the sake of possibly mocking that class implementation. May 28, 2022 at 10:23
  • @underthevoid: Not every interface needs to be broken down, YAGNI still applies. ISP is not about forcing the use of smaller interfaces at all times, it's about avoiding interfaces where a class ends up implementing it partially and leaving a few unimplemented duds. That is a sign that the implemented bits and the duds really don't belong in a single interface.
    – Flater
    May 28, 2022 at 19:37
  • @underthevoid: Mockability is a great concrete example but it is not the sole use case for interfaces. Your question focuses on interfaces that contain all methods of a class, but the focus is wrong. It's not about whether a class has multiple interfaces (or public methods not backed by an interface and some that are); it's about separating the contract from the concretion, regardless of which % of the class is dedicated towards one specific interface. It's not about an interface distilled from a class, it's about a class that implements a given interface. That is conceptually different.
    – Flater
    May 28, 2022 at 19:40

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.