Don't Refactor Like Uncle Bob
theaxolot.wordpress.com
theaxolot.wordpress.com
If anything, I would go in the opposite direction. There's no need to use parameterized variables at all. Just write out the message 3 times:
private String generateGuessSentence(char candidate, int count) {
if (count === 0) {
return "There are no " + candidate + "s";
} else if (count === 1) {
return "There is 1 " + candidate;
} else {
return "There are " + count + " " + candidate + "s";
}
}
Maybe this isn't "clean" (I'd claim it is) but it's certainly the easiest to understand.In a chapter, talking about different ways you could write a class that generates verses for the song 99 bottles of beer, we end up at:
https://sandimetz.com/99bottles-sample-ruby#_shameless_green
The Shameless Green solution is disturbing because, although the code is easy to understand, it makes no provision for change. In this particular case, the song is so unlikely to change that betting that the code is "good enough" should pay off. However, if you pretend that this problem is a proxy for a real, production application, the proper course of action is not so clear.
When you DRY out duplication or create a method to name a bit of code, you add levels of indirection that make it more abstract. In theory these abstractions make code easier to understand and change, but in practice they often achieve the opposite. One of the biggest challenges of design is knowing when to stop, and deciding well requires making judgments about code. private String generateGuessSentence(char candidate, int count) {
if (count === 0) return "There are no " + candidate + "s";
if (count === 1) return "There is 1 " + candidate;
return "There are " + count + " " + candidate + "s";
}It just needs one tired / inexperienced / somethingelse coder to "quickly add" an extra call to the topmost if and then you'll have interesting issues.
I'll take the "ugly" braces every day compared to having risky structures like that in the code at all.
But easier maintainability I also favor braces everywhere.
defmodule Guess do
def guess_message(candidate, _count=0) do
"There are no #{candidate}s"
end
def guess_message(candidate, _count=1) do
"There is 1 #{candidate}"
end
def guess_message(candidate, count) do
"There are #{count} #{candidate}s"
end
end return "There are no " + candidate + "s";
I always hated overloading + for both addition and concatenation. Hence D uses ~ for concatenation. "10" + 1
does that mean 11 or "101" ? // this may be a mistake! English language is unorthogonal in the extreme!
const char * Add_es( int count ) { return 1 == count ? "" : "es"; }
const char * Add_s( int count ) { return 1 == count ? "" : "s" ; }
(some English words pluralize with "es" suffix, most with "s"). It has proven surprisingly useful: Msg( "%d file%s updated when you switched back", updates, Add_s( updates ) );
Only difference vs example is mine prints "0" vs "no".The requirements changed and it now has to handle a new case? Now you can refactor it. What refactoring to choose depends on the new case you have to support - it's going to look a lot different if you need to accept a new "language" parameter than if you need to special-case another number.
That being said, the first time I had to edit this (even just do change the base string) I’d make the change.
For example, many languages have grammatical gender and so may also change how the word looks like. Bonus points, when the word body changes. In this example it wouldn't change radically, but the word for a file in German is "die Datei" and the plural is "die Dateien" (although apparently "das File" can be used.)
Okay, this isn't that big of a deal, one might say. You just pack the word's singular and plural with the possible gender, and you're set. But no, because this isn't even the worst part! The simplest complicated case might be that you have a singular, dual, and plural. In some sense this makes sense, since "two" is pretty special in terms of amount of stuff.
But then you get stufi like the following, as documented in [0]:
> Three forms, special cases for numbers ending in 1 and 2, 3, 4, except those ending in 1[1-4] > > [...] > > Languages with this property include: > > Slavic family > > Russian, Ukrainian, Belarusian, Serbian, Croatian
Or the six form pluralisation found in Arabic.
So yeah, this sort of "complicated" pluralisation isn't just an issue if you're trying to support some comparatively very niche languages, but this is a thing one needs to think about when supporting many major world languages.
And of course, how you inflect the noun you're pluralising depends on the context of the sentence and the relevant grammar. For example, the form might change if you're at a place that expects a noun in its nominative versus accusative form. And of course, there's the order of words, and so muth more, that could be mentioned.
So yeah, natural language is difficult. Not quite Turing complete, but good localisation takes a lot of effort.
[0]: https://www.gnu.org/software/gettext/manual/html_node/Plural...
I don't think this is quite on target. Sure, he needs to go where the people are in order to communicate to the most people, but that doesn't mean he doesn't speak out against fads if it's appropriate.
This talk was really formative for me: https://www.youtube.com/watch?v=Nsjsiz2A9mg If I remember correctly, he rails against just sticking the database in the middle of your system and calling that your architecture.
If I'm remembering https://www.youtube.com/watch?v=QHnLmvDxGTY correctly, he says OOP is no big deal, it's just encapsulation-polymorphism-inheritance, and then goes on to say things like C had better encapsulation than OO languages (you can keep your .c file to yourself, and just give someone the .h to program against.). And c-style bytes with stdin/stdout was designed with perfect forward-compatibility - programs like your terminal don't need to be recompiled even though they interact with your program (which was written after).
And for the last 5+ years he's been pushing FP pretty hard, especially closure. I don't see him as pushing OO on people at the expense of anything else.
Where I disagree with Uncle Bob is in his proposed solution. Define a plugin interface for every side-effecting part of your code, and pass in safe plugins when you want to test it. He's explicit about his preferences: the core of your application should be pure objects, exposing only methods and no data. And since you want those to be encapsulated, you can't let them leak out, you have to pass the actions in, so that you can do them in your core. This is a bonkers, topsy-turvy world where you can't just have a pure data processing routine. If you could, you would just write it, test it, use it in your side-effecting procedures, and go about your day. No plugin architecture required. Infinitely more testable, because your data processing routines don't have side effects. Uncle Bob's design forces you to deal with what happens when your side-effects go wrong somewhere in your core logic. This kind of thing is the reason we have a "law of leaky abstractions." Side effects can't be encapsulated. We try anyway, and they leak across the barrier.
I also went and looked at some of his blog posts about functional programming, and as far as I can tell, he's mostly repackaged his traditional advice for Clojure. I think a much better source there would be Eric Normand.
I think we're on the same page so far
> the core of your application should be pure objects, exposing only methods and no data.
I'll put my thumb on the scales here and say that classes should be all-behaviour or all-data. In that context, he's talking about structuring the behaviour classes in your application. Take the following (I don't recommend it because it violates DIP, but that's not the point I want to make yet):
UserService -> UserStore { User getUser(uid) {..} }
Since getUser is exposed as a method, you could swap out UserStore for PostgresUserDatabase or anything else that exposes getUser(). But if you reached directly into UserStore's HashMap (exposed data), then you're stuck with HashMap storage and can't replace it with a database. As for User itself (all-data) I don't think it's a big deal if you read its data directly (e.g. user.name.firstName). Some folks insist on a getter there, but I say if you're doing the architecture well, it should be no problem to change it to a getter later on if that's what you actually need. I think that is all that is meant by "exposing only methods and no data".> you have to pass the actions in
I think I get where you're going with this, but first
> This is a bonkers, topsy-turvy world
Topsy-turvy synonyms: reversed ... upside-down ... inverted? Yes, inverted! I, for one, vote that we rename DIP to BTTDWP, the "Bonkers topsy-turvy dependency world principle."
I suspect that Uncle Bob used a particular convention for his arrows and didn't define them in this talk, which is why you find it bonkers. The arrows are dependencies in the "who-knows-about-whom" sense, not the "who-invokes-whom" sense. The call-chain still goes in the expected ('forward') direction, but it's the knows-about arrows that are flipped in BTTDWP.
Here's an (incorrect) 3-part system, prefixed with (W)eb, (B)usiness and (D)atabase, for a typical getUser() flow.
WUserController -> BUserService -> DUserStore
There's no risk here of BUserService accidentally coupling to Web stuff (unless BUserService decides to return a WUser instead of a BUser.) But there is every risk that BUserService will treat DUserStore like a concrete database. BUserService will start doing things like opening Hikari connection pools and beginning/ending SQL transactions, and then you'll never be able to test it in isolation.All BTTDWP means here is to flip the 'knows-about' arrows so that the concrete, outside bits (which Uncle Bob keeps calling plugins) know about the B classes, instead of the other way around:
WUserController -> BUserService <- DUserStore
The call chain still flows in the original direction! This is done with interfaces in mainstream OO: WUserController -> BUserService -> IUserStore
<- DUserStore
I think that's how I'd push back on "you have to pass the actions in".> If you could, you would just write {pure data processing routine}, test it, use it in your side-effecting procedures, and go about your day.
Yes this is right. Your side-effecting procedures are typically the Database or other plugins, and your data processing is typically your business logic. Calling into the business logic from the plugin means that BTTDWP is already satisfied and you don't have to reverse any arrows.
> And since you want those to be encapsulated, you can't let them leak out, you have to pass the actions in, so that you can do them in your core.
I don't think leakage is a problem in the above W->B<-D setup. If you're standing in W, and you call B for something that's stored in D, the usual W->B->(I)D call-chain happens, but because B doesn't know about D (hidden behind I), it can't possibly leak D-specific stuff out to W. It's completely up to B as to what it wants to return/leak back to W.
FWIW, I believe Uncle Bob slightly misspoke from 52:20:
"I don't want your application to have outgoing dependencies" <- Correct
"Your application is composed of business rules, and those business rules are the family jewels." <- Correct
"You keep those jewels in a nice little bag and you don't let people look in." <- Misleading
"You keep them isolated; you don't let the frameworks touch them." <- Possibly misleading / needs context
"Looking in" is fine. He's just spent 30 minutes saying that all the (know-about) arrows point in, right!?!? And you certainly don't do the reverse: the family jewels aren't allowed to "look out" at the database or the web.I think he clarified it slightly in the next sentence, though: "you don't let the frameworks touch them". I think he meant that you do not put framework code into your business logic classes - you shouldn't see any Spring or Postgres or Hikari in the imports sections of your PayrollCalculator ("PayrollCalculator shouldn't wear the Spring wedding ring"). But Spring can ask your PayrollCalculator for the numbers it needs to return back out to the web client, which I would consider somewhat "looking in".
It would be out of character for me to not plug Haskell at this point - so I will say just this: That pesky IO monad thing that everyone keeps complaining about? That's just another incarnation of this, but it's less forgiving to newcomers because it results in a compile-time error (rather than untestable code that gets monkey-patched with shudder Mockito.) IO can call pure, but pure can't call IO. That's all it is! It just happens to be more important in a lazy setting where the compiler will lift and lower expressions in and out of each other and only evaluate expressions insofar as a caller is demanding results.
String getCandidateCountString(char candidate, int count) {
if (count == 0 ) {
return String.format("There are no %ss", candidate);
} else if (count == 1) {
return String.format("There is 1 %ss", candidate);
} else {
return String.format("There are %s %ss", count, candidate);
}
}
There are 3 as. lolBeginner: I'll just create an if statement with 3 clauses.
Middle: No! that's clearly a ton of duplication - it's bad code!
Expert: I'll just create an if statement with 3 clauses.
And I can see how people get to the middle position. People often tell beginners to eschew duplication. They learn to see any trio of similar-looking lines as a code smell and jump to refactor. It takes a few years of doing that and getting burnt by it before you can really differentiate "this is duplicative and would benefit from refactoring" from "this only looks duplicative, and is actually cleanest with the repetition in place."
question = "Is";
object = "this";
adjective = "easy";
String.format("%s %s %s");
vs String.format("Is this easy");
The second is much easier to understand than the first, so one should be careful when pulling out too many variables.Seems like someone should write a book called "Clean Code Done Right" that has actual good examples of code. Maybe Rich Hickey? The high level advice is good, code should be written as simply as it can be. Functions should have a small clear mission and it should be easy to argue that the function is correct locally. That is to say: a functions correctness shouldn't depend in a complicated way on other functions, just a reasonable expectation on the behavior of the functions it calls, and a reasonable simple description of its mission. Easy to state this objective often difficult to achieve in practice.
Maybe a book showing elegantly coded solutions to actually difficult programming problems would be good, e.g. a system with maybe the complexity of high performance sharded fault tolerant Redis. There would be a lot of interesting topics there.
Weird. My impression was that hating on uncle bob had made for low hanging blog fruit for a good 5 years already.
Some of CC is alright (I have a feeling he popularised negative gates and early returns?), some of it didn't age as well. As is often the case it's hard to see it in the context of its time.
So I started to refer to Clean Code when I needed an authority argument: if it’s in a book, you need a stronger argument against it, and unless you have a sound argument, your code is unclean.
I believe Clean Code mostly gets referenced by people who look for someone to agree with them on anything specific. The weaker opinions and examples become “junk DNA” for a selective quoter.
Back then, I often didn’t find a section where Clean Code agreed (or disagreed) with me. Mainly because I got influenced by functional programming, which the book does not address.
Once I decided to read the whole book, because I wanted to collect the good parts. I found myself only disagreeing in a small number of sections. But at the same time, I was completely unable to use his examples for anything.
I think this book serves a purpose. But I don’t think it addresses enough of what I have learned in the last ten years.
A simple example is naming things: the book does a fine job at giving good and bad examples. But I find that once you find the right vocabulary from your problem domain, and you want to choose among words that are all appropriate, thoughts like “is this at the right level of abstraction for this area of the code?” and “will this name also be meaningful in 6 months?” don’t get addressed. It’s the scenario where all your choices are okay, which name is better?
The book played a big role on my early career before I gained an authority and an experience of my own.
Usually when this happens, people are dismissing a toy example, (and they can make the toy even simpler.) I think that is the case here.
String-concatenation in a word game is not worth this abstraction overhead. But I think it's supposed to teach people not to be afraid of usingLongFunctionNamesToTellAStory().
It would probably work better on code like:
..
// Otherwise we gotta make a sibling now
if ( newNode (&siblingId, a) == NEWNODE_FAIL ) {
return ADD_FAIL;
}
Node *sibling = getNode (a, siblingId);
// And a new place for it to point
NodeIdx destId = 0U;
if ( newNode (&destId, a) == NEWNODE_FAIL ) {
return ADD_FAIL;
}
..
could take on more of a style like: if (needSiblingNode()) {
return ADD_FAIL;
}
..
pointToNewAddress()Whenever I voice criticism of 'Clean Code', a lengthy, awkward and difficult conversation follow, where I must clarify that I don't dislike writing code that is 'clean', I dislike writing code according to the whims of some dude who wrote a bunch of random practices down in a book he titled 'Clean Code'.
It never goes over well, and my chances of success are inversely proportional to how technical the conversation partner is, and how much of an agenda has.
When a non-technical manager 'helpfully' suggest I adopt best practices from this book to improve code quality, a special kind of pain arises in my soul.
I know I'm not alone in this struggle, the ways of defeating this menace has escaped a generation of programmers.
Same goes for Agile.
So if the OOP class is confusing, it has not met its goal. One could also argue that functions "do one thing" to reduce complexity, and if that one thing is to return a string, your function has succeeded.
Ousterhout also writes about "deep" functions versus "shallow" functions. "Deep" referring to a function that does a lot, without the caller doing a complex series of functions to get the end result. So subjectively, a single function with a lot of logic would be "more correct" than a class with many shallow methods.
Uncle Bob has caused more wasted time and effort than any other “thought leader” except perhaps Martin Fowler.
His examples are absolutely terrible and really make me question how anyone can read more than a few chapters into this book and still think Uncle Bob is an expert.
It’s subjective. It’s easy to obfuscate code, really hard to make it “clean.”
Personal anecdote: our cto is hung up on making super tiny methods bc of this book. You can’t follow logic because you’re constantly hopping between tiny bits of code all over the project. It’s like trying to read a Wikipedia article that’s been condensed to one run on sentence with every word hyperlinked.
*Terms popularized by consultants selling books and consulting.
Yet it is very conforting and comfortable if you are inside its echo chamber.
So all the output of those thought leaders are not to be applied to the letter, but should be part of a whole educational process that also evaluates all the limits of that thinking.
But that takes time to think. Which our modern society has almost outlawed, in its rush for "fastfood for thought" that enables selling more ads and hype
We use Clean Code in one of our classes here at the university. What I tell my students is that they should take every piece of programming advice, and form their own rationalized opinion about that advice. And no matter what you think the right thing is, half the internet is probably going to disagree with you.
What's important, is that you assess the situation, draw upon your knowledge to code up the "best" solution, and have an opinionated rationale for why you did it that way.
Five years from now, you're going to look back at all your college code and be amazed at how crap it is. But what's important is that by having these opinionated rationales for the work you do, you are continuously learning how to make your code better.
And you're doing it in a flexible way. Sometimes rules should be broken for the greater good.
```ts
const pluralizePhrase = (noun: string, count: number) =>
count == 1 ? `There is one ${noun}` : `There are ${count || "no"} ${noun}s`;
``` private String generateGuessSentence(char candidate, int count) {
return switch(count){
case 0 -> "There are no " + candidate +"s";
case 1 -> "There is one " + candidate;
default -> "There are " + count + " " + candidate + "s";
};
}
And it would look even better with string templates...hopefully soon! private static String generateGuessSentence(char candidate, int count) {
return String.format(switch(count){
case 0 -> "There are no %1$ss";
case 1 -> "There is one %1$s";
default -> "There are %2$s %1$ss";
}, candidate, count);
}I learned go this way. I don’t mean I learned the syntax I mean I learned how to structure the code. It’s just really well thought out and extremely pragmatic.
It’s a master class in “clean code”.
I could probably point you to some things he has said that might change that...
Compound booleans are super useful for cleaning up huge chunks of imperative code. I do it all the time, and it makes the function far easier to read (although the tradeoff is in debugging).
Internet: "But some of his minor ideas are wrong."
Me: "So you think it's wise to ignore his brilliant ideas because of a few nits?"
Internet: "He once invaded Poland."
Me: ???
(though part of me wonders if the "Uncle Bob hate" is actually a clever PT Barnum style marketing campaign for Clean Code)
I have 211+ programming books in my library going back to 1998. I don't consider any books from 16 years ago to be valuable or relevant today.
This is healthy.
Applies to all advices, wisdoms and mottos.
I prefer the 'after' (not that I don't have complaints about it).
When I read 'before', I can't see any structure. I scan from top-to-bottom, get half-way down, think "wait, what?" and my eyes jump back up without me even consciously deciding to.
> The first thing you’ll notice is that Martin has taken a single, mostly PURE function (shout out to all the functional bros), and made a class out of it.
That doesn't make a good purity argument. 'Before' has a print. There is no way to test it apart from reading stdout (or god forbid, having your test harness somehow intercept/mock the print call). `After` has no such IO, it returns the String itself to make unit testing possible.
I despise mutability, but neither before nor after does any re-assignment, (only assignment.) So really, those fields should be marked 'final' and the 'private void' methods should be constructors or something... I'm not really sure about the 2008 version of whatever language he's using.
print(new GuessStatisticsMessage().make(candidate, count));
There are cases where using a class as a bag of pseudo-global variables that get mutated during a long web of internal method calls — a recursive-descent parser for a non-trival language is a prime example, — is fine but this, I'd argue, is not one of those cases. You need to (very straightforwardly) map two input values to three output values and return them glued together — one function with a switch in it is quite enough for that.P.S. And what the hell does "thereAreNoLetters" even mean? Oh, right, it makes sense since this is a letter-guessing game, and this class is used to tell you that no, there are no letters like the one you tried to guess. Bleh.
It's trivial to change if you care about that part. DI - pass in a ConsoleWriter. It also solves the test without mocking.