← Back to context

Comment by nicklecompte

5 years ago

I don’t disagree with the overall message or choice of examples behind this post, but one paragraph stuck out to me:

> Martin says that it should be possible to read a single source file from top to bottom as narrative, with the level of abstraction in each function descending as we read on, each function calling out to others further down. This is far from universally relevant. Many source files, I would even say most source files, cannot be neatly hierarchised in this way.

The relevance is a fair criticism but most programs in most languages can in fact be hierarchized this way, with the small number of mutually interdependent code safely separated. Many functional languages actually enforce this.

As an F# developer it can be very painful to read C# programs even though I often find C# files very elegant and readable: it just seems like a book, presented out of order, and without page numbers. Whereas an .fsproj file provides a robust reading order.

> "with the level of abstraction in each function descending as we read on, each function calling out to others further down." ...

> Many functional languages actually enforce this.

Don't they enforce the opposite? In ML languages (I don't know F# but I thought it was an ML dialect), you can generally only call functions that were defined previously.

Of course, having a clear hierarchy is nice whether it goes from most to least abstract, or the other way around, but I think Martin is recommending the opposite from what you are used to.

  • Hmm, perhaps I am misreading this? Your understanding of ML languages is correct. I have always found “Uncle Bob” condescending and obnoxious so I can’t speak to the actual source material.

    I am putting more emphasis on the “reading top-to-bottom” aspect and less on the level of abstraction itself (might be why I’m misreading it). My understanding was that Bob sez a function shouldn’t call any “helper” functions until the helpers have been defined - if it did, you wouldn’t be able to “read” it. But with your comment, maybe he meant that you should define your lower-level functions as prototypes, implement the higher-level functions completely, then fill in the details for the lower functions at the bottom. Which is situationally useful but yeah, overkill as a hard rule.

    In ML and F# you can certainly call interfaces before providing an implementation, as long as you define the interface first. Whereas in C# you can define the interface last and call it all you want beforehand. This is what I find confusing, to the point of being bad practice in most cases.

    So even if I misread specifically what (the post said) Bob was saying, I think the overall idea is what Bob had in mind.

    • It seems your idea is precisely the opposite of Robert Martin's. What he is advocating for is starting out the file with the high-level abstractions first, without any of the messy details. So, at the top level you'd have a function that says `DoTheThing() { doFirstPart(); doSecondPart();}`,then reading along you'd find out what doFirstPart() and doSecondPart() mean (note that I've used imperative style names, but that was a random choice on my part).

      Personally I prefer this style, even though I dislike many other ideas in Clean Code.

      The requirement to define some function name before you can call it is specific to a few languages, typically older ones. I don't think it's very relevant in this context, and there are usually ways around it (such as putting all declarations in header files in C and C++, so that the actual source file technically begins with the declarations from the compiler's perspective, but doesn't actually from a programmer perspective (it just begins with #include "header").

    • > I am putting more emphasis on the “reading top-to-bottom” aspect and less on the level of abstraction itself (might be why I’m misreading it). My understanding was that Bob sez a function shouldn’t call any “helper” functions until the helpers have been defined - if it did, you wouldn’t be able to “read” it. But with your comment, maybe he meant that you should define your lower-level functions as prototypes, implement the higher-level functions completely, then fill in the details for the lower functions at the bottom.

      Neither really, he's saying the higher-level abstractions go at the top of the file, meaning the helpers go at the bottom and get used before they're defined. No mention of prototypes that I remember.

      Personally, I've never liked that advice either - I always put the helpers at the top to build up the grammar, then the work is actually done at the bottom.

  • > In ML languages, you can generally only call functions that were defined previously.

    Hum... At least not in Haskell.

    Starting with the mostly dependent code makes a large difference in readability. It's much better to open your file and see what are the overall functions. The alternative is browsing to find it, even when it's on the bottom. Since you read functions from the top to the bottom, locating the bottom of the function isn't much of a help to read it.

    1 - The dependency order does not imply on any ordering in abstraction. Both can change in opposite directions just as well as on the same.

  • ML languages are not functional ("functional" in the original sense of the word - pure functional). They are impure and thus don't enforce it.

    • As someone who almost exclusively uses functional languages: don’t do this. This kind of pedantic gatekeeping is not only obnoxious... it’s totally inaccurate! Which makes it 100x as obnoxious.

      “Functional” means “functions are first-class citizens in the language” and typically mean a lot of core language features designed around easily creasing android manipulating functions as ordinary objects (so C#, C++, and Python don’t really count, even with more recent bells-and-whistles). Typically there is a strong emphasis on recursive definitions. But trying to pretend “functional programming languages” are anything particularly specific is just a recipe for dumb arguments. And of course, outside of the entry point itself, it is quite possible (even desirable) to write side-effect-free idiomatic ISO C.

      The “original” functional language was LISP, which is impure as C and not even statically-typed - and for a long time certain Lisp/Scheme folks would gatekeep about how a language wasn’t a Real Functional Language if it wasn’t homoiconic and didn’t have fancy macros. (And what’s with those weird ivory tower Miranda programmers?) In fact, I think the “gate has swung,” so to speak, to the point that people downplay the certain advantages of Lisps over ML/Haskells/etc.

      2 replies →

We follow this approach closely - the problem is that people confuse helper services for first-order services and call them directly leading to confusion. I don't know how to avoid this without moving the "main" service to a separate project and having `internal` helper services. DI for class libraries in .NET Core is also hacky if you don't want to import every single service explicitly.

  • Is there a reason why private/internal qualifiers aren’t sufficient? Possibly within the same namespace / partial class if you want to break it up?

    As I type this out, I suppose “people don’t use access modifiers when they should” is a defensible reason.... I also think the InternalsVisibleTo attribute should be used more widely for testing.

    • You can't make a helper private if you move it out to a separate service. The risk of making it public is if people think the helper should be used directly, and not through the parent service.

      Internal REQUIRES that the service be moved out to a class library project, which seems like overkill in a lot of cases.