0

I was reading articles about TDD and I found the following one. https://blog.cleancoder.com/uncle-bob/2017/03/03/TDD-Harms-Architecture.html

In order to answer this question I think someone should read the full article, but in any case I'll quote the part I'm not able to grasp or to picture in my head.

enter image description here

The right side [...] decouples the users from the services by using an API. What’s more, the services implement the API using inheritance, or some other form of polymorphism [...]

...Okay I guess

I want you to make a simple substitution. Look at that diagram, and substitute the word “TEST” for the word “USER” – and then think. Yes. That’s right. Tests need to be designed. Principles of design apply to tests just as much as they apply to regular code. Tests are part of the system; and they must be maintained to the same standards as any other part of the system.

Here is the part I don't understand. I just don't see an API reducing all those dependencies, for me an API just provides endpoints to be called from outside the application... How come that all that coupling is going to automatically dissapear if I change my backend to be connected within an API?

I'm just too blind to see how it would work... I'd love to understand it because I've been using TDD for two years now and I have suffered that pain he talks about whenever I need to refactor my tests just because we perform a clean up in the backend.

Can anybody come up with a snippet showing how it would work or which concept should I focus on to understand it better?

Rauuuñ
  • 17
  • 1
  • 1
    You're overthinking this. The API is a single reference point. That's all there is to this. – Robert Harvey Jun 10 '21 at 11:52
  • I see what you mean – Rauuuñ Jun 10 '21 at 12:07
  • 1
    "for me an API just provides endpoints to be called from outside the application" - I think your confusion stems from a misunderstanding of what the term API means; today, when someone uses the word API, they usually mean a *web API* - which is a thing that has "endpoints to be called from outside the application". However, that's just one kind of an API. – Filip Milovanović Jun 10 '21 at 13:21
  • 2
    The term API is more general than that - it denotes an Application Programming *Interface* - in other words, it's any set of methods and rules that code needs to adhere to in order to access some component (or a subsystem, or a system). E.g. functions exposed by a library you're using form an API (to that library) - along with the types of parameters, return values, and all the rules describing how to use them. Public methods of a class are an API (to the class). Uncle Bob is using the term in that more general sense. – Filip Milovanović Jun 10 '21 at 13:22
  • You decouple by carefully designing those "entry points" and the rules. Since tests will use your components through that API you designed (which may be an interface, or some small number of of classes forming an "outer shell" of your component), they serve as a stand-in, or a test bed, for the real production code that will eventually use your component through that same API, in the same way. That's his point – Filip Milovanović Jun 10 '21 at 13:24
  • Basically, don't test internals (they are private!), and write small classes (whatever you imagined as small when you read this - probably smaller than that). – Filip Milovanović Jun 10 '21 at 13:29
  • @FilipMilovanović +1 to everything!, but... "You decouple by carefully designing those "entry points"" Imagine I have a class method that depends on two services to develop a functionallity. Problem arises when unit testing: if I mock/stub the services response, I might find myself having to change such unit test in the future if the service structure changes. Does that mean I am doing something wrong? Cause I can put effort on decoupling but on big methods such as the ones that prepare everything to render a view, I just have a strong coupling there that I can't get rid of – Rauuuñ Jun 10 '21 at 14:22
  • 1
    In this particular case when Bob Martin says API he's thinking of something like a Java Interface or Abstract Class: "the services implement the API using inheritance, or some other form of polymorphism". Not something that at run-time goes in between the users and the services. – bdsl Jun 10 '21 at 14:46
  • If you just pass the response of the service directly, and write your client code and tests against that data structure (which may be fine to get you started), then when the API response of the service changes, you'll have to reimplement your code and your tests. However, don't forget, your class method has (potentially) input parameters and an output type - that's an "API" to the method. If the changes in the service are largely cosmetic, you can "translate" the response to the old format; and really, you only need to include/worry about the properties your app actually uses for something 1/2 – Filip Milovanović Jun 10 '21 at 16:34
  • You can also do similar things with objects and functions, it doesn't have to be data. If the changes are more conceptually misaligned , you might need to do something more involved. But, actively look for things like that: *concepts/ideas/processes* that persists across changes, and find a way to represent them in code - then write other code, and tests, in terms of those. That's where you get your interfaces and abstractions from. Of course, there's no silver bullet, but the goal is to get your client code and your tests to be less brittle. 2/2 – Filip Milovanović Jun 10 '21 at 16:35
  • P.S. You might also be interested in Working Effectively with Legacy Code by Michael Feathers - I'm thinking largely because it details how to break various forms of coupling, by finding and making use of what he calls "seams" in the code. Do try to find the book, but here's a [summary of key points](https://understandlegacycode.com/blog/key-points-of-working-effectively-with-legacy-code/). – Filip Milovanović Jun 10 '21 at 16:35
  • @FilipMilovanović Thanks a lot, you made my day! – Rauuuñ Jun 10 '21 at 18:20

4 Answers4

4

enter image description here

Change breaks things. Hiding change limits how far that breaking goes and makes changing easier.

If you want to look at this in the most abstract way: this is simply indirection.

A famous aphorism of Butler Lampson goes: "All problems in computer science can be solved by another level of indirection" (the "fundamental theorem of software engineering"). This is often deliberately mis-quoted with "abstraction layer" substituted for "level of indirection". An often cited corollary to this is, "...except for the problem of too many layers of indirection."

wikipedia.org - Indirection

Which is simply a way to say, sure you can do this. But it aint free. It comes with a cost.

When you say:

Here is the part I don't understand. I just don't see an API reducing all those dependencies, for me an API just provides endpoints to be called from outside the application... How come that all that coupling is going to automatically disappear if I change my backend to be connected within an API?

You are 100% right. Not a single dependency has been eliminated. The users have exactly the same needs that they had before.

What's different now is the expression of those needs (what you'd look at to learn what they are) is no longer mixed together with service implementation details and scattered over many disparate services. Separating the implementation and the interface isn't simply to make testing or polymorphism easier. It also makes reading code easier. A reviewer of your code now has a tidy place to go to learn exactly what user needs you're offering to satisfy.

And from the user perspective there is less coupling simply because they don't know what is satisfying their needs.

I'm just too blind to see how it would work... I'd love to understand it because I've been using TDD for two years now and I have suffered that pain he talks about whenever I need to refactor my tests just because we perform a clean up in the backend.

TDD causes early pain. Technical debt causes late pain. So long as the early pain is cheaper than the late pain you got a bargain.

But that doesn't mean there isn't some learning to do. TDD can be done in very messed up ways. From locking down things to where change is actually harder with TDD (test against interfaces not implementation details to avoid most of this) to testing requirements that either don't exist anymore or never should have existed in the first place (map tests to requirements if names don't make it obvious).

However, just because you have to refactor tests doesn't mean you did something wrong. Sometimes it means a requirement changed. Sometimes it means you've restructured the code. TDD never promised that tests never change.

In fact, there is a rigorous methodology for refactoring tests. Michael Feathers gave us refactoring against the red bar. Apparently he got it from Elizabeth Keogh. I've talked about it before. It walks you through steps to change a unit test so that the code under test can't sneak away from you.

Which is nice, but again, it isn't free. Change comes at a cost.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
1

I just don't see an API reducing all those dependencies

Because it doesn't. Uncle Bob is wrong there. Let me list some reasons for this:

The very first thing is, Uncle Bob doesn't seem to think in terms of design trade-offs. It seems to be always in absolutes, like the UI must always be separated from the business, database and all other technologies must all be separated from each other, etc. Just take a look at Uncle Bob's code to see how unmaintainable that is.

Architecture is heavily dependent on the requirements. That means you can't make a trade-off "in general" without even looking at what that application is for.

Second, introducing an additional abstraction is not free. Putting a box in a diagram might be easy, but you're bringing a lot of complexity on board. This is also not acknowledged.

Third, it doesn't even decouple anything necessarily. Uncle Bob leaves out all the details about that one additional box. Like, is that API a data-oriented API, like Uncle Bob usually defines them? Because if it is, both sides have to know what that data is and how it works. That would mean that if the "services" change, the "users" have to change anyway, because of the changed semantics and/or protocol. What if the workflow changes, the meaning of some data elements, etc.?

He might mean, that you don't exactly need to know which service you need to call, because you're calling a central thing. That might help you in some situations, but that's a pretty small part of the whole thing.

In general though, I agree with the conclusions. TDD will not get you a good design automatically, it is not a design practice. It is still all up to us.

Robert Bräutigam
  • 11,473
  • 1
  • 17
  • 36
  • 1
    "Putting a box in a diagram might be easy, but you're bringing a lot of complexity on board" Indeed!! Okay I get the same thoughts as you! I agree with everything in the post except this API black magic solving every problem. I try to use as many tools as I can to decouple my tests from code and try to just not "write unit tests" but think a good design for my tests and then write them. But sometimes I use one tool and sometimes I use another – Rauuuñ Jun 10 '21 at 12:19
  • 1
    "Because if it is, both sides have to know what that data is and how it works." - well, of course, the point of an abstraction is that both sides depend on it, rather then one side depending on the other. "That would mean that if the 'services' change, the "users" have to change anyway" - not if the change is internal to the services; if the API is stable (as an abstraction should be), the service can "translate" to it, and users are not affected. "because of the changed semantics and/or protocol" - that's a change in the API, not in the service; you're talking about an early/unstable API. – Filip Milovanović Jun 10 '21 at 13:41
  • I've downvoted as this answer feels more like hate against Robert C. Martin than reasonable answer to OP's question. R.C. Martin's ideas might be wrong. But that doesn't mean you should attack the person with the ideas. – Euphoric Jun 10 '21 at 13:50
  • @FilipMilovanović If the change is internal to the service then you wouldn't have problems changing it without an additional abstraction either. Also if you expect a "stable API", then Uncle Bob's suggestion is even more unreasonable. Stable APIs are really hard and a lot of overhead. You can't just suggest one on a whim. – Robert Bräutigam Jun 10 '21 at 14:07
  • @Euphoric I don't know where you get all that. I attacked specific ideas in the post the OP has referenced. Which was written by Uncle Bob. I even wrote that I agree with the overall conclusions. I think you have it backwards. – Robert Bräutigam Jun 10 '21 at 14:17
  • I don't think this answer deserves a down-vote, but I do think it misses what "API" means in this context. Something that @bdsl mentioned in a comment on the question itself. – Greg Burghardt Jun 10 '21 at 16:13
  • "If the change is internal to the service then you wouldn't have problems changing it without an additional abstraction either" - Well, if you don't have an abstraction (or, rather, if your interface to the service just sort of emerged on it's own - 'cause you always have one, it's just a matter of if it was consciously designed or not), then changes that *ought* to have little impact on client code (i.e. changes that could be made internal with some careful redesign) will propagate back to clients. The problem is that the coupling is not consciously controlled, but has "just happened". – Filip Milovanović Jun 10 '21 at 16:32
  • "Stable APIs are really hard" - yes, but trying to wrestle highly coupled code into shape is harder and more overhead; moreover, abstractions have to be more stable then the things that depend on them, *otherwise they are useless*. I agree, though, you can't just come up with a stable API, but you start with some best-guess abstractions, while keeping things small and simple (YAGNI-style), and approach stability over time. – Filip Milovanović Jun 10 '21 at 16:32
  • @GregBurghardt Thanks :) I'm actually old enough that API means programmatic API by default. – Robert Bräutigam Jun 10 '21 at 16:32
  • @FilipMilovanović I posit that any interface that only has one implementation will not be stable. Also, if you control both sides (like a boundary inside an application) that API will not be stable either. You can't just will an abstraction into being, let alone a stable one, it has to come from the problem domain. And that only lasts until the requirements change, which they do (at least in my projects) daily. So what you say makes sense in theory, but nearly never actually happens in practice outside of "official" standards. – Robert Bräutigam Jun 10 '21 at 16:45
  • I used the word "interface" in a general sense (so, the methods you call and types you use), I'm not talking about C#/Java interfaces. And by "abstraction" I don't necessarily mean an interface or an abstract class (could also be a function, a facade, things like aggregate roots, mini languages, DSLs, data structures, conventions, an anticorruption layer, a whole microservice). Also, by "stable" here I mean relatively stable compared to things that depend on it; it's not necessarily the level of stability of widely accepted protocols. – Filip Milovanović Jun 10 '21 at 16:58
1

The decoupling happens because you put interface between the services and users. It is the same rule as for interfaces in java. If you take a java List for example. List is an interface that has some methods. You expect certain behavior from this interface. Let's just say that you have something like this:

public List<String> appendComma(List<String> strings) {
  strings.append(",");
  return strings;
}

Now my question to you is: Is the List an ArrayList or LinkedList?

The answer is simple: You don't care. Which means that you have decoupled implementation from the behavior. If you do TDD this means that you only have to define the behavior of the interface, which means that you can use the same tests for ArrayList and LinkedList or you can make your own implementation of the List if you wanted to, as long as the behavior will be the same.

It is the same if you are talking about some remote APIs. Http API exposes /users/:id - which returns a user with a specified id.

Again my question is: Which service returns the user? Again, You don't care. Implementation of that feature (behavior) is left completely to the provider of that API. It could be implemented with a monolith, as a microservice, users could be stored in SQL database or NoSql database. The point is that you don't need to know that, which decouples your application from the implementation of the API. In fact, the application of the API provider might have been a monolith in the beginning, but he broke it down into microservices as time went on, but you didn't need to change your code.

If I come back to unit tests: You need to write the tests for the interface and not implementation. When you do TDD spend more time to define the interface so it won't be fragile and it won't change often. When that is done, create a class that implements it. After that you can do whatever you want to that implementation as long as it is passing the tests.

Blaž Mrak
  • 460
  • 3
  • 8
0

I offer you a (hopefully) better illustration:

diagram

You are right to be confused, because the original example is somewhat inconsistent, and the explanation that follows it is cursory and depends too much on the context outside of the article.

The one point I want to highlight is the fact that the example is supposed to demonstrate decoupling, but it also includes consolidation — an unrelated design change. This is distracting, because the consolidation is very apparent and draws too much attention. The author should've either demonstrated decoupling without introducing unrelated design changes or made it clear why/how that design change was important.

But, you're also wrong in the assumption that API is necessarily an endpoint. In the context of web development, the API could be the same layer that you plug into a web framework / reverse proxy. Which means that tests may consume it directly, without spinning up a server.

With all that established, I hope the connection with tests is clear now: if the tests are designed to go through the same indirection the user goes, then the tests will benefit from the indirection the same way the user does: rearranging the implementation should have no impact on the tests.

This is another way of saying that high-level test (that go after the public interface) must be preferred to low-level tests (that are concerned with implementation details).

Shadows In Rain
  • 669
  • 5
  • 13