DEV Community

Discussion on: In Defense of Clean Code: 100+ pieces of timeless advice from Uncle Bob

 
peerreynders profile image
peerreynders

His last point is great though about abstraction being hard to get right and that getting the interface right the first time is important.

See also in your article

Choosing good names is hard.

It's been my observation that people will usually find ways to avoid doing hard things.

  • lots of JavaScript code is full of anonymous inline functions (of any size). Among other things it avoids having to name the function, effectively forcing any future reader of the code having to fully mentally parse the function body to divine the function's intended purpose without even a hint from a function name.

  • similarly if people are convinced that it is "hard to get the right abstraction on the first try" they often aren't going to invest any effort in an attempt.

So he's really arguing against doing abstraction badly.

Perhaps focusing on abstraction is a misdirection.

Wrapping third-party APIs in a thin layer of abstraction makes it easier to swap out one library for another in the future.
Wrapping third-party APIs in a thin layer of abstraction makes it easier to mock the library during testing.

Chapter 7 Error Handling was written by Michael Feathers, Author of Working Effectively with Legacy Code

All he was suggesting was to "map" the errors/exceptions that are specific to the API to application specific exceptions that the application knows how to handle.

That being said I put forward the perspective that (the right) boundaries are more important than "abstractions". And ultimately even the "right" boundaries are subject to which objectives are perceived to have the highest value.

Data-Oriented Design: What's wrong? — Mapping the problem

Object-oriented development is good at providing a human oriented representation of the problem in the source code, but bad at providing a machine representation of the solution. It is bad at providing a framework for creating an optimal solution, …

Robert C. Martin is clearly in the "good at providing a human oriented representation of the problem in source code" camp and TDD is a key part of his platform.

People often pursue "testing to attempt to show correctness.”

But Michael Feathers makes the observation:

Unit testing does not improve quality just by catching errors at the unit level. And, integration testing does not improve quality just by catching errors at the integration level. The truth is more subtle than that. Quality is a function of thought and reflection - precise thought and reflection.

That thought and reflection also iteratively revises the boundaries towards optimal testability.

This seems to be in direct contradiction to DHH's Test-induced design damage. As a framework designer he's a proponent of "Rails is your application". He's primarily interested in speed of initial implementation and is perfectly content to rely on integrated tests to just verify product correctness — but not challenge the quality of design (the design of Rails doesn't change and comes with pre-imposed boundaries).

Robert C. Martin's view is informed by the Hexagonal architecture were:

  • the application exists at the core
  • the application dictates the interfaces the "adapters" have to implement (not to be confused with the Adapter Pattern).
  • the "adapters" can be replaced with test doubles to accurately and quickly test "the application".

So going back to the out-of-context over-generalization:

Wrapping third-party APIs in a thin layer of abstraction

  1. Define the client facing contract/protocol "the application" actually needs first.
  2. Select a third-party API capable of fulfilling those needs.
  3. Implement the contract(s) on top of the chosen third-party API

If the semantics between the contract and the third-party API are sufficiently different expect to implement an anti-corruption layer.

When it comes to external APIs it can be worth looking into contract tests.

  • contract tests document the understanding of the contract/protocol as used by "the application".
  • contract tests are run infrequently against the "adapters" and the actual API to verify that all assumptions about the API still hold.
  • contract tests are used to guide and verify the expected behaviour of the test doubles of the various "adapters" used for testing the application core.

Is it easy to identify optimal boundaries?

Of course not.

The design process is an iterative one

Andy Kinslow, Software Engineering 1968, p.21

Suddenly one week it dawned on us what was wrong. Our model tied together the Facility and Loan shares in a way that was not appropriate to the business. This revelation had wide repercussions. With the business experts nodding, enthusiastically helping—and, I dare say, wondering what took us so long—we hashed out a new model on a whiteboard.

Eric Evans, Domain Driven Design 2004, p.196

Startups don’t have stable business rules - so boundaries are constantly in flux
Adam Ralph, Finding your service boundaries 2019

i.e. it's rare to get it right off the bat.

Thread Thread
 
mindplay profile image
Rasmus Schultz

Both of those ideas aren't criticisms of abstraction though, those are criticisms of doing abstraction badly.

I don't agree. The criticism is that of abstracting the wrong things. The third-party library you're using is already an abstraction of something - and there could be reasons to further abstract that, but often there isn't, and just avoiding coupling, in my opinion is definitely not the right motivation.

To solve the first problem, just... don't add the third-party library's implementation details into your abstractions's interface. Leaking the implementation details defeats the whole purpose of abstraction.

Even if you don't leak implementation details, you're going to "leak concepts" - ideas from the underlying library are likely going to bleed into your domain, even if things like types and interfaces do not.

Some reasons I might choose to abstract would be:

  1. The library is really complex and does a lot more than I need - in that case, I can avoid direct coupling to a complex library by hiding it behind a simple interface.

  2. The library doesn't quite do everything I need - in that case, I can build an abstraction that adds in the missing details, and again avoid direct coupling to something that wasn't quite what I needed in the first place.

On the other hand, why would I abstract something if it's already (more or less) exactly what the project needs? If I put my own very similar units in front of some library units, any ideas of being decoupled is really just an idea - if anything changes, it's practically guaranteed to break my abstraction.

I think that's the case he's talking about in the video.

Every line of code, whether that's your code or library code, adds complexity: every line of code is a liability, so every line of code needs to have a specific, measured reason to exist.

In my experience, the most successful projects are always the ones with less complexity.

So it has to be a conscious, case-by-case decision, in my opinion.

Thread Thread
 
peerreynders profile image
peerreynders

If I put my own very similar units in front of some library units, any ideas of being decoupled is really just an idea.

This is assuming a one-to-one distinct abstraction to concrete library relationship. That type of alignment isn't necessarily the best way to move forward.

In my experience, the most successful projects are always the ones with less complexity.

I think it's more important to evaluate if complexity is managed appropriately. In my view OOD invariably adds complexity in order to manage complexity — it can work but it often isn't a slam dunk.

So it has to be a conscious, case-by-case decision, in my opinion.

That's pretty much a given. Guidelines tend to be a starting point, not some absolute truth.

What ultimately devalued the video for me was the example - why would there be a need for a concrete messaging abstraction? The actual goal is to have the application logic be "ignorant" of the messaging solution that is being used to handle messages. This idea is similar to Persistence Ignorance (PI):

Well, PI means clean, ordinary classes where you focus on the business problem at hand without adding stuff for infrastructure-related reasons.
Jimmy Nilsson, Applying Domain-Driven Design and Patterns 2006, p. 183

"The Application" will only need to send a finite number of message types, and receive a finite number of message types. Worst case each send-type has its own function into the infrastructure and the application exposes a separate function for each receive-type. The application is only interested in providing the data for outgoing messages and extracting the data from the incoming messages. The application really doesn't care what happens on the other side of the application boundary.

Quote

… and then you find out that there are messaging libraries that abstract the underlying transport whether it be Azure Service Bus or Rabbit MQ, etc. …

Rabbit MQ has no business being inside the application boundary.

To use J. B. Rainsberger's terminology:

  • Rabbit MQ has to live in the Horrible Outside World (HOW)
  • The application exists in the Happy Zone (HZ).
  • The HOW and HZ are separated by the DeMilitarized Zone (DMZ; where the "adapters" live; narrowing API, pattern of usage API)
  • The HZ can depend on the HZ
  • The DMZ can depend on the DMZ
  • The DMZ can depend on the HZ
  • The HOW is permitted to depend on the HZ
  • The DMZ may depend on the HOW
  • The HZ cannot depend on the DMZ
  • The DMZ is allowed to talk to the HZ by implementing an HZ interface.
  • The HZ cannot depend on the HOW (… and yet this is exactly what many frameworks encourage you to do — I call it the creepy hug problem — it's nice to be hugged unless the hug is a little bit too long and a little bit too tight by somebody you don't know that well)
  • The HOW is allowed to talk to the HZ by implementing an HZ interface.

FYI: Mock Roles, not Objects

Quote

Now again the reason you're abstracting these things is because you want to isolate them. And you might want to isolate them because you don't want to depend on them directly. Again most people say so I can swap it out.

The "swap out" argument is specious but isolation is the prize.

Isolation is the pre-requisite to being able to leverage fast micro-tests — "tests to detect change", i.e. establish rapid feedback as we refactor the code to reduce the volatility in the marginal cost of adding the next feature.

Doesn't this HOW/DMZ/HZ stuff slow us down?

In the beginning perhaps but again J.B. Rainsberger explains The Scam:

The cost of the first few features is actually quite a bit higher than it is doing it the "not so careful way" … eventually you reach the point where the cost of getting out of the blocks quickly and not worrying about the design is about the same as the cost of being careful from the beginning … and after this being careful is nothing but profit.

He acknowledges that "the scam" is initially incredibly seductive but eventually there comes the point where the cost of continuing is higher than the cost of starting again.

So the initial investment is aimed at going well for long enough, so you'll beat fast all the time.