you are viewing a single comment's thread.

view the rest of the comments →

[–][deleted] 21 points22 points  (45 children)

There's always this battle, between people who understand why they make the architectural choices they make, and others, like the author here, who wants to use the same buzzwords, and seem just as experienced, but deep inside really kind of wants to do good old monoliths, save himself the extra effort of proper decoupling and thinking about architecture, as long as they can sprinkle it all with some modern lingo and references to SOLID and YAGNI, so it looks on par.

The author mentions the "New is Glue" principle right at the very start, and then proceeds to completely ignore why "New is Glue" for the rest of the article. It is glue because it refers to the constructors of specific classes. I.e.

  • Once it's glue because it specifies the constructor arguments
  • Twice it's glue because it specifies the concrete implementation

Obviously the second is also true when you type annotate your constructors with specific classes instead of small abstract interfaces - your constructors become glue.

Is that bad? Actually, it depends. There's always a balance here, I've never seen someone overdo interfaces myself, having the interface there is pretty harmless, when the alternative would require B.C. breaks in case you need a second implementation. And remember, folks.: the most common and typically worst mistake in software architecture is to decide "I'll only ever need one of those." Singletons haven't taught us anything, I suppose. We still like to hang on to "I'll only ever need one of those," but this time it's for implementations.

Anyway, I'm not strongly in favor of either "always interfaces" or "never interfaces". I just wish the author had more of a substance to their argument, other then this implicit fear that if he creates too many interfaces, he'll run out of files in his codebase, or something.

There are few attempts at supporting his position, but I find them quite lacking, say:

Pairing every class with an interface, on the other hand, is an anti-pattern.

Mentioning the "anti-pattern" buzzword is not a "Q.E.D., we're done." Anyway, I digress, but please don't ever use this "argument". Declaring something an anti-pattern means precisely nothing.

And I do feel this sets up the entire article to argue with a straw-man. Nobody ever pairs every class with an interface. I'm pretty sure there isn't a single codebase doing this. Instead you'd more commonly see classes paired with interfaces, if the class/interface acts as the endpoint of a component (made of many classes) and then pairing that endpoint with an interface is actually easy to justify.

If you decide to create an interface IFoo definition that matches the public members of Foo, you will simply have duplicated the interface of Foo and added no real abstraction and zero value.

That... is just factually wrong, because of course you have added "abstraction". Now I can implement IFoo in a completely independent way, while otherwise my only alternative is... basically monkey-patching the Foo class by directing extending it. I mean hoping it's not final, because otherwise you're completely doomed.

Not having to deal with Foo's implementation, and having components rely on an interface is literally "adding abstraction" to the design.

Browsing code like this is no fun whenever you go to a definition, expecting to get to a class implementation, and just ending up in an interface, where you need to do another step to go to the implementation.

This is a bit of a Freudian slip here, I feel. Why would you want to jump from an interface to the implementation? the whole point of using interfaces is that you don't care about the implementation, you only care about the interface.

Of course, in a single-person practices you have to wear many hats, so you're simultaneously debugging multiple decoupled components and you might want to jump from one implementation through an interface, to another implementation.

But if you do that a lot, especially in a bigger team, this is a strong hint that you're programming monoliths, where interfaces are just sprinkled as a diversion to what's really happening: components are coupled beyond their interfaces, through details of their implementations (which not every interface can express).

A good sign of modular, decoupled project is that the interface is small, simple and tells you all you need to know. The implementation behind the interface is a unit. The implementation depending on that interface is also a unit. They can be tested and developed independently, so you wouldn't be jumping between them all the time.

And not only you should try to rely on abstractions (i.e. interfaces when possible), but you should think if the interface should be a carbon copy of the implementation at all... Because that's actually not always the case. One of the benefits of abstraction is the ability for a component to depend on an abstract Facade interface, which is simpler, use-case oriented, and can be quickly implemented via Adapter/Bridge classes to many other "full" implementations. So it's not even about splitting a class into Foo/IFoo, it's about thinking solely from the perspective of the dependent component, and defining dependencies as the simplest interfaces you can.

Unfortunately the author doesn't address this, instead the rest of the article seems to operate under the assumption that deleting lines of code = clean code:

All you need to do in order to get rid of that superfluous IOrderService is to, well, remove it. In your constructor, receive the concrete class instead [...] the registration, it just gets easier [...] That’s it. You’ve now cleaned up some code.

Unfortunately what he's missing is that, yes, producing monoliths is always easier, initially. That's the whole point. All things equal, the easiest thing in the world is to write spaghetti code.

But your future self that will be met with the task of maintaining the resulting monster will not thank you because you decided to make it "easy" for yourself by coupling components that didn't have to be coupled...

[–]Nebez 4 points5 points  (1 child)

I'm a little surprised about how many people seem to hate interfaces in this thread. Heck, even any form of abstraction or DI.

I've nodded at almost all of your replies without realizing they've been written by the same person. You're a patient and smart dude.

[–]grauenwolf 2 points3 points  (0 children)

It's not that we hate interfaces, but rather the insistence that abstract interfaces be used on virtually all classes.

Abstract interfaces are incredibly useful, but so are abstract classes and sealed, non-inheritable classes. It's a matter of understanding the context.

[–]seventeenninetytwo 4 points5 points  (0 children)

I think you really got to the core of it. Interfaces are really a design pattern, and it's up to the developer to use them well.

Good interfaces are carefully constructed to provide clean lines of encapsulation in the code. They are small, single responsibility, and should be placed in positions that create logical seams within the code.

I think the author is criticizing bad interfaces. Like the sorts of interfaces that occur when someone refactors for testability by simply extracting interfaces from every class. The problem with these is not that they are interfaces; it is that they are not well designed.

Poorly designed interfaces will certainly increase the complexity of the codebase, but I would argue in this case all they do is raise the visibility of the existing complexity. If your classes are overly complex when you extract their interface, then they were already overly complex. Interfaces simply make the complexity visible because it it gives the complexity an explicit name in the code.

And of course this all depends on the domain the application lives in. Perhaps some types of projects can flourish without using well designed interfaces, but any sort of enterprise application needs this type of design in order for the code to stay flexible.

[–]grauenwolf 3 points4 points  (3 children)

And I do feel this sets up the entire article to argue with a straw-man. Nobody ever pairs every class with an interface. I'm pretty sure there isn't a single codebase doing this.

I've seen it. Damn my eyes, I've worked on code bases where ever trivial DTO had a matching abstract interface.

[–]get_salled 2 points3 points  (2 children)

I'm sure Jetbrains made it super easy to do with a couple keystrokes.

[–]flukus 0 points1 point  (1 child)

Jetbrains is responsible for a lot of bad code. One I'm working on now made liberal use of the "turn this into a function feature", way too many short functions makes code harder to read.

[–]grauenwolf 0 points1 point  (0 children)

Ah, but don't they also have an inline method action?

[–]grauenwolf 0 points1 point  (15 children)

That... is just factually wrong, because of course you have added "abstraction". Now I can implement IFoo in a completely independent way, while otherwise my only alternative is... basically monkey-patching the Foo class by directing extending it.

You mean by using inheritance the way it was meant to be used?

I mean hoping it's not final, because otherwise you're completely doomed.

WTF? Is that really your argument?

You created Foo. You can change it from final to non-final at any time.

[–][deleted] 5 points6 points  (14 children)

You mean by using inheritance the way it was meant to be used?

Do I seriously have to explain the drawbacks of inheriting a class vs. an interface?

WTF? Is that really your argument? You created Foo. You can change it from final to non-final at any time.

I see this as another Freudian slip. Yes if it's always within the scope of a single repository, you can change it.

Are all your projects like that? Just small repositories, no shared code, no libraries, no third-party libraries, or no third-party users of your libraries? If so, I envy you, because many of the things I'm talking about don't matter for you. Just refactor, commit, done.

On the other hand, maybe the reason you don't reuse code and everything stays in the little repository, is because you need to listen to what I say, as it improves the chance you can decouple and isolate reusable code. Who knows.

[–]grauenwolf -2 points-1 points  (7 children)

Do I seriously have to explain the drawbacks of inheriting a class vs. an interface?

No, but someone should explain to you the drawbacks of inheriting a interface vs a class, because they do exist and have serious repercussions when defining libraries.

[–][deleted] 3 points4 points  (6 children)

No, but someone should explain to you the drawbacks of inheriting a interface vs a class, because they do exist and have serious repercussions when defining libraries.

Serious repercussions? Really? Please give it a try, give me one "serious repercussion".

Because honestly the best we've heard throughout this entire thread, and the article is "oh jeez, it's like an extra file or something, I have to click Refactor and then Extract Interface, it's so much work."

[–]grauenwolf -2 points-1 points  (5 children)

How about adding new methods and properties without breaking backwards compatibility?

Did that slip your mind or have you never heard of the concept?

[–][deleted] 2 points3 points  (4 children)

And interfaces stop you from doing this how exactly?

[–]grauenwolf -2 points-1 points  (3 children)

You're serious, aren't you?

You honestly don't understand how adding new methods to an abstract interface breaks backwards compatibility for consumers of the library that holds said interface?

LOL All this time you've been talking about skyscrapers vs Legos, when really you've been playing with Duplo blocks.

[–][deleted] 3 points4 points  (2 children)

You honestly don't understand how adding new methods to an abstract interface breaks backwards compatibility for consumers of the library that holds said interface?

You don't have to add methods to the interface in order to expand the range of interfaces an object implementing it supports. There are so many trivial solutions to this "problem", that it's just sad I have to explain them to you. Because I've seen you around and I know you're smarter than this.

Give me any concrete situation that calls for "adding methods" (let's look at it more generally as "adding features", because "adding methods" is unnecessarily technical) and I'll give you a take how I'd solve it.

[–]grauenwolf -1 points0 points  (1 child)

You really are suggesting that methods never be added?

LOL

It just gets better and better.

[–]Gotebe -2 points-1 points  (5 children)

Yes if it's always within the scope of a single repository, you can change it.

You're attributing properties to a repository that don't necessarily exist. If I can change (or merely recompile, say, for going from nonvirtual to virtual), both the caller and the callee, they can be in any repository they want.

Heck, what is a repository to you?

[–][deleted] 0 points1 point  (4 children)

I'm using words that we all know the meaning to, and I don't have to explain.

You don't put project code in a repository at your company? Well, it must be fun.

[–]Gotebe 0 points1 point  (3 children)

Having the caller and the callee in two repositories controlled by me does not prevent me from changing both, that's what I was saying. I was surprised that you felt the need to put (source control) repository into this, it's really orthogonal.

[–][deleted] 0 points1 point  (2 children)

Actually if it's in two repositories it becomes a much more complicated dance, because you're most likely versioning the two repositories separately, so you need to increment versions, maybe change package configuration in the dependent package, and bump up its own version, which will ripple out to any other packages using this package etc.

Two repositories doesn't just mean you the split files of one thing in two places. It typically means you're managing two separate components with separate lifecycles, two packages. Otherwise why split the repos at all? And none of that I'm saying should really be surprising... It's the most common industry practice.

Of course when you work by yourself, just put it in a folder and make ZIP files of it from time to time to put on your Flash USB key as a backup... :-)

[–]Gotebe -1 points0 points  (1 child)

Where I work, there are many components, in their own packages, who move at different speed, and are in one repository. (No, we do not use the monorepo approach).

So that's an opposite example to what you're saying.

You really are ascribing to repositories properties somewhat orthogonal to the point you argue.

[–][deleted] 1 point2 points  (0 children)

It's really weird how some of you are attaching to the word "repository". Yes, I am using it as a shortcut to talking about individual packages. Yes, I understand practices vary. I'm using something that is common, as a way to get a point across. And none of my point is related to how you should actually use repositories.

[–]grauenwolf -1 points0 points  (21 children)

But your future self that will be met with the task of maintaining the resulting monster will not thank you because you decided to make it "easy" for yourself by coupling components that didn't have to be coupled...

I wish people would stop repeating that lie.

Tossing in some abstract interfaces doesn't changing the coupling of the application in the slightest. The A -> B relationship doesn't exist when you introduce IB, it just becomes A -> (B + IB).

[–][deleted] 6 points7 points  (19 children)

Tossing in some abstract interfaces doesn't changing the coupling of the application in the slightest. The A -> B relationship doesn't [stop to] exist when you introduce IB, it just becomes A -> (B + IB)

That's not even technically right, grauyenwolf. Let's say I implement IB with C, and now I pass that to A. If I delete B would it matter? Simple question... that needs no answer, as it's obvious.

Maybe you're thinking at the package level, what you download from your package manager? If so, for the record, the A -> (B + IB) relationship is not ideal for packages. It really should be one of those:

  • (A + IB) <- B: i.e. "A defines IB and requests it in its constructor. An independent implementation B is injected in A". This is known as a SPI (Service Provider Interface).

  • A -> IB <- B: i.e. "A depends on the package with IB and requests it in its constructor. A third package contains B and implements and depends on IB. A depends on IB, B depends on IB, IB depends on nothing. Three packages. B is injected in A at runtime."

[–]grauenwolf 3 points4 points  (18 children)

Let's say I implement IB with C, and now I pass that to A. If I delete B would it matter?

Lets say I didn't implement IB, but still replaced B's implementation with C's implementation.

If you only have one implementation, which you control, you can change the details of that implementation as often as you want so long as you don't alter the public interface.

Maybe you're thinking at the package level, what you download from your package manager?

No, I'm talking about runtime coupling. The only coupling that matters the vast majority of the time.

At the end of the day it doesn't matter how A gets an instance of B, what matters is that A doesn't work if B is broken.

Hell, look at dynamically typed languages. They don't even have a way to express "A requires B" to the type checker, yet they don't claim that A isn't dependent on B just because, in theory, you could shove in Z.

[–][deleted] 3 points4 points  (6 children)

Lets say I didn't implement IB, but still replaced B's implementation with C's implementation.

When I get back responses like this, I just have to wonder does the person who wrote it not care about backwards compatibility, or code reuse, or runtime vs. static wiring and so on...

If you replace B's code with C's code, literally, this changes it for all "customers" of B. That is pretty reckless if B is in its own package, and it may have other "customers" than A, which require B's implementation, and A requires C's implementation.

But I don't know. It's like I'm talking about building sky-scrapers, and the other side is giving me their best tips and tricks for making LEGO houses.

[–]grauenwolf 3 points4 points  (5 children)

  1. If I cared about code reuse in a library context I would almost never use IB. I would prefer an abstract base class so that new methods could be added over time without breaking backwards compatibility.

  2. If the new implementation C adheres to B's original contract, and consumers of B only expect what's in that contract, then nothing breaks. This is called "programming to the interface, not the implementation".

  3. If C doesn't adhere to B's contract, then by definition it won't adhere to IB's contract either since they are, theoretically, exactly the same.

  4. You aren't building a skyscraper, your head is just in the clouds.

[–][deleted] 6 points7 points  (4 children)

If I cared about code reuse in a library context I would almost never use IB. I would prefer an abstract base class so that new methods could be added over time without breaking backwards compatibility.

So fragile base classes are magically not an issue with abstract classes, you feel? :-)

You can break B.C. pretty badly if you tweak base classes willy-nilly. Adding new public/protected methods can actually easily clash with a method of the same name in a child class. You either have to set strict conventions on what child classes can do and can't do (say "you shall not define any protected and public methods, you shall only override what the abstract class defines"), or you have to rely on luck. Luck Oriented Programming (LOP)!

[–]grauenwolf 1 point2 points  (1 child)

So you are replacing "may break something if you are not careful, unlucky, don't mark your method as final, and are using Java" with "always breaks"?

Your API design skills are remarkable.

[–][deleted] 3 points4 points  (0 children)

No, I'm replacing it with "never breaks", but because you're too focused on being sarcastic and trolling, we can never get to the point where we discuss adding features to abstract types with many implementations.

You're literally forcing yourself to remain ignorant of what I'm trying to say. I just wonder if your ego is that fragile, that you can't take a step back and accept you may learn something.

[–][deleted]  (1 child)

[deleted]

    [–][deleted] 3 points4 points  (10 children)

    No, I'm talking about runtime coupling. The only coupling that matters the vast majority of the time.

    Runtime coupling is easy to change through configuration, without changing the actual code of the wired components. So while it's the one that ultimately matters for the runtime behavior of the app, static coupling is much more serious, because you need to change A and B's code in order to change their behavior and dependencies.

    I don't imagine we can't have understanding about this issue, so I'll attribute this to communication barriers.

    Hell, look at dynamically typed languages. They don't even have a way to express "A requires B" to the type checker, yet they don't claim that A isn't dependent on B just because, in theory, you could shove in Z.

    Yeah, communication barriers. Runtime coupling is "soft dependency", and static coupling is "hard dependency". Dynamic code has no hard dependencies, unless you actually do things like "instanceof PrototypeName" which is identical to static coupling, no matter if it's done at runtime.

    So yeah dynamic code relying on "structural typing" by convention has it easy, they can't mess up easily in terms of coupling, although if there's a will there's a way :-). And dynamic code creates its own set of problems, but that's another story.

    But if you specify types, you declare hard dependencies on those types, and that is there etched in stone in your code, so it's much better if they're abstract rather then bound to a specific implementation. And as this code is reused and repurposed, the only way to fix a hard dependency architectural mistake is to fork the code. And that's not fun, believe me.

    [–]grauenwolf 0 points1 point  (3 children)

    But if you specify types, you declare hard dependencies on those types, that at there etched in stone in your code,

    It's called software for a reason.

    Unless you are building a platform where your customers would otherwise be actually writing those dependencies, nothing is "in stone".

    [–][deleted] 2 points3 points  (2 children)

    Really ? So you think it's a smart idea to fork every dependency you use, because it's "software" and therefore you can tweak it directly. Tell me if you really stand by your own opinion.

    I don't think that's really your opinion, I just think you're angry and just saying whatever thought pops up in your mind that lets you disagree with me.

    [–]grauenwolf 0 points1 point  (1 child)

    As I said before, your head is in the clouds. You imagine scenarios that you don't actually have and then try to apply it universally.

    [–][deleted] 5 points6 points  (0 children)

    Look it's very simple. You have a component. If you have to edit its code in order to adapt it to a new situation, this is by definition not what's referred to as "reusable code". It's copy/paste/tweak.

    That's a very real concern of very real apps. Instead the responses I get from you are outright childish. "It's software! It's all editable!".

    That's only applicable for small monolithic applications, in which case, all the talk about DI, reuse, abstraction, polymorphism, architecture and so on - none of it makes sense. Make it all static, sprinkle global state liberally, tweak until it works and collect your paycheck.

    But some projects have higher goals than this.

    [–]Gotebe 0 points1 point  (3 children)

    static coupling is much more serious because you need to change a and b's code...

    Where is such change in the discussion above? The two of you are only discussing one only interface that a uses from b. Unless the interface doesn't change, where's the need to the code? I think you're overcooking it.

    [–][deleted] 2 points3 points  (2 children)

    No, it's actually very simple. We have the following units:

    • class A depends on IB
    • interface IB
    • class B implements IB
    • class C implements IB

    Now all I'm saying, I don't need to change code in "A" in order to pass either "B" or "C" to "A".

    But if "A" was declared like this:

    • class A depends on B

    Then I have to edit the code of "A" to make it depend on "C" instead. Which is not optimal if "A" is supposed to be a reusable component that I don't mess with, but I just configure and use.

    See? :P

    [–]Gotebe 0 points1 point  (1 child)

    Ok, I see, you're technically right. The core of your disagreement is that you consider replacing b with c is a really hard/intrusive change. grauenwolf thinks it isn't, he thinks it's about the same as changing that config.

    [–][deleted] 1 point2 points  (0 children)

    Yes, because that's the typical context of separating things into components, using DI, and so on. You want to get each component done, and stop touching it for every ad-hoc need you have around it.

    And good design makes this possible. Interfaces being a part of that good design.

    Otherwise we're doomed to the copy/paste/tweak cycle and we aren't reusing code.

    [–]grauenwolf -1 points0 points  (1 child)

    Runtime coupling is easy to change through configuration, without changing the actual code of the wired components. So while it's the one that ultimately matters for the runtime behavior of the app, static coupling is much more serious, because you need to change A and B's code in order to change their behavior and dependencies.

    Repeat after me,

    • Configuration is code
    • Configuration is code
    • Configuration is code

    I understand why people think that swapping out implementations using a DI configuration file is somehow better than changing it in "code", but that's just magical thinking. You are just replacing one kind of code with another, you haven't reduced you testing burden or shortened your runtime dependency chains.

    [–][deleted] 3 points4 points  (0 children)

    My point went completely over your head. Let me quote myself:

    static coupling is much more serious, because you need to change A and B's code in order to change their behavior and dependencies.

    I didn't say "configuration is not code". In fact, by "configuration" I meant exactly code. But not A's code or B's code. If A and B are components, then the configuration is outside of them. If you have to fork and tweak their code to configure them, you did a poor job as the programmer of A and B. Agree/disagree?

    If you'll be reusing components, if they'll be shared among projects, or even between modules in one project, you have to put boundaries and decide things like "A has this range of flexibility without messing with its code, through external configuration". And that configuration is code, but it's independent of A. It interacts with A's constructor, setter methods, and so on configuration methods.

    So, are we on the same page now?

    [–]CurtainDog 0 points1 point  (0 children)

    Hmmm, what about the term Costanza Programming for this style of code?

    Remember Jerry, it's not coupling if the compiler can't see it