GeistHaus
log in · sign up

Axol's Blog

Part of wordpress.com

I write about whatever comes to my mind

stories
How To Think Critically
Uncategorizedculturelogiclogical-fallaciesphilosophypoliticspsychologysociology
If you’ve been a long-time follower of my blog, you’ve probably wondered at some point: Axol, how are your takes so based? The answer is simple. I’ve mastered a skill that few others possess. A skill that allows me to navigate the realm of truth in a way that leads to the light. That skill […]
Show full content

If you’ve been a long-time follower of my blog, you’ve probably wondered at some point:

Axol, how are your takes so based?

The answer is simple.

I’ve mastered a skill that few others possess. A skill that allows me to navigate the realm of truth in a way that leads to the light. That skill is called:

Critical Thinking

A.K.A the ability to challenge a claim with rigor, regardless of source.

See, most people are capable of thinking critically about a claim that opposes their worldviews, but you’ve probably noticed that they don’t apply the same standards to views they feel positively or neutrally about.

I’m not like that.

In fact, I even tend to go overboard, and question things too much, to the point where I’m hesitant to take a hard stance on a topic unless I’m absolutely sure I’m right.

Better to be safe than sorry, though.

I often get frustrated when people make obvious reasoning errors where it counts the most, yet no one else points them out (not a flex, btw). And I know they’re smart enough to do it, but deep down, they’re afraid of the backlash.

There was this social experiment ran by Solomon Asch in the 50s.

A group was brought into a room with a board that had three lines drawn on it, each line clearly differing in length. The trick was that all but one group member were paid actors, who confidently gave wrong answers when asked by the coordinators which line was closest in length to a fourth sample line. The genuine participant was the last to answer, and they often answered in line with the majority opinion.

But they never believed it. They just went along out of fear.

This experiment is relevant now more than ever. It explains how otherwise smart people can be duped by peer pressure.

And this doesn’t just apply to factual claims; it also applies to moral ones as well.

So if you want to join the elite critical thinkers club (whose only member so far is me), strap in.

1. What Are Your Principles?

Your principles are a set of commandments you adhere to above all else.

For example, if you’re a theist, you probably adhere to Divine Command Theory.

But if you’re not, you should come with a set of principles and rank them based on which you prioritize (in case of clashes between them). For example:

  1. Advocate for people’s rights and freedoms
  2. Look out for your family/friends/community
  3. Value truth
  4. Enjoy life
  5. etc

You don’t have to have the same set of principles as others. But having principles allows you to make all kinds of judgments without having to wrestle with cognitive dissonance all the time. A good set of principles naturally leads to a balanced worldview, and a bad or incomplete one leads to flimsy rationalizations of strange ideas.

2. Examine Assumptions

Every conclusion is a function of premises. But when these premises aren’t explicitly stated, they’re called assumptions because they’re taken for granted.

I’m really good at mapping out other people’s beliefs and arguments, because I’m very attentive to detail. It’s this attentiveness that allows me to deconstruct and demolish people’s arguments.

But keep in mind that there are an innumerable amount of assumptions in many multifaceted arguments, so you have to be able to hone in on only the most relevant and most contentious ones. Otherwise, you’ll be stuck in analysis paralysis.

Being able to question your own assumptions is the ultimate skill. It allows your beliefs to be fluid enough that if you encounter evidence that challenges your assumptions, you can change your belief without taking a hit to your delicate ego.

For example, ask yourself the following:

  • How much freedom should be traded for safety?
  • How often is mainstream science wrong? And in what ways?
  • What should be the limits of free speech?
  • What kinds of problems benefit more from bottom-up (individualistic) solutions compared to top-down (systemic) ones? And vice versa?
  • etc

Don’t be afraid to question ideas considerered “objective” or “settled”. At worst, you’ll at least learn why they’re true, instead of taking them for granted.

3. Stop With The Identity Politics

If you’re passionate about an issue to the point that you openly and repeatedly advocate for it, it’s very tempting to tie so much of your identity to one side of the issue. And that’s especially the case when you have a following and a career built from said advocacy.

The problem with this is that you make it more difficult to change your perspective when you’re so entrenched and dependent on it. Labels are useful, but they should describe you, rather than you orienting yourself around them.

There’s so much diversity of opinion within even the most niche of ideologies, but identifying too much with a label makes you hesitant to voice an opinion that doesn’t align with the majority, which risks shunning and being cast out.

For example:

  • If your political party’s leader isn’t acting in your interests, it’s not a betrayal to vote for a different party.
  • You can criticize your own side while agreeing with most of its opinions.
  • You shouldn’t assume people who disagree with you are evil, stupid, or mentally ill. They may simply disagree on practicality, rather than on principle.
4. Leave Your Echo Chamber

You’re an independent person.

You don’t have to shelter yourself from opposing ideas.

Social media algorithms incentivize engagement, which means they’ll show you more of what you’re already consuming. This makes it really easy to go down a rabbit hole where you start to believe that the majority opinion is what you see.

This plays into our worst cognitive biases. In reality, there are many reasonable people on both sides of most issues, because the truth isn’t always easy to arrive at.

Reddit is a major culprit in this. Subreddits are ran by moderators who can’t be held accountable, and people who moderate political subreddits tend to be very partisan. This leads to trigger-happy mods who use their power to ban people who express even the slightest disagreement with the majority opinion.

It’s good for a community to limit discussion topics so as to preserve its purpose. However, this shouldn’t be the case with contentious political topics, because what you end up with is people preaching to the choir to reaffirm group status and earn social brownie points, rather than any substantive discussion.

Just like how incest eventually produces physical deformities due to reproduction with too similar genes, echo chambers produce ideological deformities by reproducing with too similar ideas. All this rots the critically thinking mind.

Unfortunately, there’s nothing we can really do about echo chambers. But what you can do is challenge yourself. What’s the harm in perusing ideas from other communities? It’s not a waste of time to learn how others think. And if you’re worried about self-doubt, what does that say about the strength of your ideas?

5. Value Consistency

Everyone has double standards. It’s nothing to be ashamed of.

However, if someone calls out your double standards, and you don’t have a good explanation as to why you apply different standards to similar situations, then do us all a favor and be ashamed.

Here are a couple examples:

  • When a capitalist state crumbles, a communist blames it on capitalism. But when a communist state crumbles, they claim that the communism just wasn’t done right.
  • A feminist claims that men and women should have equal representation in the workforce. However, when asked if this representation should extend to the less pleasant jobs such as sewage, construction, or military, she bites her tongue.
  • A diversity advocate claims that a lack of proportional representation of races in any field is evidence of systemic discrimination. However, when pointed to fields where minorities are over-represented, they jump through hoops to explain why it’s not actually discriminatory.

And here are examples of beliefs that might seem self-contradictory but aren’t:

  • You can be pro-“free speech” while also supporting people being cancelled for abhorrent ideas, as long as that cancellation isn’t due to government interference.
  • You can claim that one culture is superior to another if it values human rights more. Cultural relativism isn’t absolute.
  • You can believe that we as a society are responsible for taking care of our least fortunate, while not shaming individuals who choose not to do so. This is because collective responsibility doesn’t imply individual responsibility from everyone.

It’s tricky stuff, but as long as your justifications are based on sound and consistent principles, you’re good.

6. Spot Fallacies

Here’s a handy Wikipedia link to a list of all known fallacies.

But you don’t need to memorize all these, because the average person can sus out most of these by intuition alone.

Reductio ad absurdum is a technique that helps you spot fallacious reasoning. You take a train of logic and use it to argue for a claim you know is ridiculous, thus showing that the reasoning itself is incorrect.

With enough experience, you’ll be able to label these on the spot. Though when debating others, I’d advise not naming them out loud. You’re better off just explaining the reasoning error directly, rather than spouting off fallacies like a nerd with his encyclopedia. Most people won’t do the research into understanding the fallacy if you don’t explain it to them.

For example, instead of just saying, “false dichotomy,” say, “well actually there’s a third option that’s very likely too.”

Or instead of saying, “appeal to nature,” say, “just because it’s natural doesn’t mean it’s okay,” then list an example or two.

Eventually, you want to be able to spot these fallacies in your own thinking before a belief takes root. This is especially important if your personality predisposes you to impulsive and irrational beliefs based on feelings.

Here’s an example you might remember:

Women say they’d feel safer encountering a bear in the woods than a random man, because, statistically, women are more likely to be victimized at the hands of men than by bears.

Give it a thought and see if you can spot the fallacy.

Got it?

If you didn’t, that’s okay. This is one of those really tricky statistical fallacies.

The data they drew from to make their conclusion simply takes the number of women who’ve been victims of bear attacks in a given year, and divides that by the population of women in a given country. A similar collection is done for victims of men.

There’s nothing wrong with the data itself. The mistake these women made was neglecting to factor in conditional probabilities.

The probabilities they calculated from this data include all women in that country, even if they hadn’t gone anywhere near a forest that year. And obviously, people tend to live outside forests in most countries. In other words, this probability has too much noise, and doesn’t take situational factors into account.

Instead, they’d have to prove that the probability of being attacked GIVEN that they’ve ALREADY had an encounter in the forest is higher for men than bears.

Other common statistical fallacies include:

  • Citing a recent upward or downward trend in a graph using too short of a time-frame, and when zoomed out a little more, is just a small spike or dip in the grand scheme of things.
  • Using percentage changes on top of percentages. For example, claiming that people who eat food “A” are 50% more likely to experience heart disease than those who eat food “B”. But when you look at the data, it’s actually 1.5% for food “A”, and 1% for food “B”. The percentage was applied on top of what was already a percentage.
  • Citing averages, but neglecting medians and distributions when analyzing data.
  • Zooming in to a graph while hiding the scale on the y-axis, then drawing a big slope to exaggerate an effect when presenting it.
7. Evaluate Evidence; Don’t Just Cite It

Oh, if only I had a nickel for every time someone misrepresented a study in an argument.

Or quoted a sentence out of context from an academic paper to bolster their point.

Or even cited a news article after claiming that “studies show.”

What’s going on here?

Rhetorical question. Most people don’t really care about studies as a science. They’re used more as fodder in an argument rather than a stable base from which to draw conclusions.

If you’re gonna cite a study, READ IT. And I don’t mean read every little detail about the statistical analysis of the results (thought that’s a bonus). That stuff requires A LOT of study to fully understand. But at least get a good overview:

  • Title
  • Abstract & Context
  • Hypothesis
  • Methodology
  • Variables
  • Conclusion

and understand the strengths and limitations of different methodologies.

For example, surveys are good for soliciting information en masse, but you always have to consider that answers may be influenced by selection bias (i.e certain kinds of people being more or less likely to respond to the survey) and cognitive distortions (i.e questions about one’s own character, accounts of previous experiences, well-known memory blind spots, etc).

Correlational studies are good for analyzing trends, but it’s important to know which factors are being controlled for if you want to use them in an argument. The gender wage gap fiasco happened because people didn’t realize that the studies weren’t controlling for hours worked, experience, and how different women’s jobs were from men’s. Because of this ignorance, people automatically assumed it was due to pervasive discrimination.

Experimental studies are what many consider to be the gold standard for proving causative links, because they isolate the causes they’re testing. Having said that, you have to check for the following:

  • Is the sample size high enough?
  • How were the subjects sampled? Would the results generalize to the average person?
  • Who’s funding the study? Do the conductors have a financial incentive to report certain results?
  • Is the person who put forth the hypothesis the same one conducting the experiment? Could that lead to experimenter bias, where the conductor unwittingly introduces factors that may influence how subjects respond?
  • For health-related experiments, is there a placebo control group? And how long did the study run for? Do short-term improvements diminish or even regress in the long term?
  • Has the study been replicated by independent researchers?

I don’t blame researchers for how their studies are misused. I blame you. But I’ll forgive you if you change your ways.

8. Prepare Against The Best Counterarguments

Of all these tactics, this is the one that elevates my takes above most.

In game theory, there’s a concept called minimaxing.

This is when you calculate every response your opponent has to every one of your moves, then you pick the set of moves that guarantees victory.

Obviously, debate is different from a game in that there are an unlimited amount of moves (arguments) one can make, but the idea still applies.

Instead of anticipating every counterargument, you should anticipate the best ones. This means placing the weakest or most contentious parts of your beliefs under scrutiny.

If you make an honest effort and find objections to be too strong, you should consider changing your mind. Otherwise, you should feel more confident entering a debate and knowing that you’ve already pre-empted most reasonable objections.

You might’ve wondered how in-person debaters often counter their opponent’s objections so precisely and on the spot. It’s not all quick wit. A lot of it is anticipation and preparation.

For example, if you claim that your specific coding methodology is superior, and you can’t find hard evidence, then you should at least have a very well-reasoned argument based on how developers usually go about reading and writing code, rather than projecting your own ideals.

Or if you’re gonna use historical diagnosis rates to claim that mental illness has been on the rise since the 80s, you’d better have a counter-objection prepared as to why this can’t be explained by an increase in societal acceptance and mental health interventions, as opposed to being a literal increase in the rate of mental health problems.

Just one more.

If you’re gonna paint a certain group of people as violent extremists, then it’s of the utmost importance that you have damn solid justifications for that claim, and be prepared to defend them in a way that doesn’t lead to labelling obviously non-violent people as violent.

Conclusion

Just by reading this article, you’ve already exposed your mind to a higher form of cognition.

And it’s irreversible.

You’re welcome.

Thanks for reading, and question everything.

Except that. Everything but that.

http://theaxolot.wordpress.com/?p=5848
Extensions
12 Bad Faith Debate Tactics You Need
Uncategorizedlogical-fallaciesfallaciespoliticsdebatephilosophyArgumentation
I’m back at it again. Some of my first articles involved me expressing frustration at the common ways people conduct themselves in online debate. It’s a frustrating feeling when I know I’m close to exposing someone’s fundamental misunderstandings about a topic, but they manage to weasel their way out of conceding their wrongness. Or when […]
Show full content

I’m back at it again.

Some of my first articles involved me expressing frustration at the common ways people conduct themselves in online debate.

It’s a frustrating feeling when I know I’m close to exposing someone’s fundamental misunderstandings about a topic, but they manage to weasel their way out of conceding their wrongness.

Or when I go out of my way to be as charitable as possible, and only get misunderstanding in return.

But then I realized, I should try to see things from another perspective. Maybe I should put myself in the shoes of these people and get a taste of the forbidden debate powers they possess. After all, the best way to beat someone is to understand how they think.

So I gazed into the abyss, and now, I’ll teach you how to argue in bad faith. Here are twelve tactics I’ve learned.

1. Never Doubt Yourself

Your opponent has just made a good point.

So good, in fact, you almost find your mind changing. You’re terrified!

You were so arrogant and assertive, that any concession will return that to you tenfold.

You never bothered to prepare for the best counterarguments to your position. It’s a mystery how you were blindsided like this.

That’s okay. There’s a neat trick that fixes all this.

It’s called, “Rationalization.”

Rather than using facts and premises to lead the way to a conclusion, simply do the opposite.

Start with the conclusion you want, then stretch all evidence such that it could conceivably support your conclusion, even if it’s beyond reason.

“Occam’s Razor” can take a hike.

2. Concede Nothing

You are flawless in your thinking.

All your ideas as airtight.

No caveats.

No concessions.

No exceptions.

You can’t just let this one point go and admit your beliefs aren’t perfect.

Everything has to be correct.

3. Play Innocent

Go out of your way to misread the room when it suits you. For example, play devil’s advocate when it’s completely unnecessary to the debate.

Give ridiculous or extremist ideas far more courtesy than they deserve, and pretend like you’re being reasonable.

Be dismissive and uncharitable, then use your politeness as a shield to maintain the moral high ground.

Abuse “sealioning.” Pretend like you’re a child who knows nothing about the world, who’s lacking in logical faculties, who can’t do basic research, and who needs evidence for even the most obvious of claims.

Act like you’re just trying to understand your opposition’s point of view, when in reality, you have a preconceived notion of their beliefs that will continue to poison the debate.

Use “dog whistles” to attract people who agree with you while maintaining plausible deniability.

Your conduct will fool the audience into believing you mean well, and your opponent will look bad for getting reasonably frustrated and calling you out.

4. Conceal Your Fundamental Disagreements

Disguise your stance as a simple matter of “practicality” and “evidence”.

Write off people’s feelings as outside the realm of facts, even when the topic itself concerns people’s feelings.

Pretend like you’re unemotional, unbiased, and rational, such that only cogent arguments will sway you, when in reality, there are some things you’ll never budge on (though maybe you should).

5. Cherry-Pick Evidence

Never make clear what standard of evidence you’d require to change your mind, even when asked.

On that note, supporting evidence and sources are just placeholders. If someone debunks them, just replace them with new ones. Work backwards from your desired conclusion, because it must be right.

Lower the bar of evidence for your claims, but raise it impossibly high for those of your opponents.

“Burden of Proof?” Forget about it. Since you know you’re right, your stance is automatically the default, so it’s up to others to argue you out of it.

Cite sources without fully reading them to see if they claim what you say they claim. They’re close enough.

If you can’t find any, rely on anecdotal evidence with the assumption that your anecdotal evidence is more valid than that of your opponent. If they question it, that’s gaslighting.

6. Abuse Subtle Fallacies

You want to go on the offense, but deep down, you know your points are incorrect. You just can’t admit it to yourself.

No worries. Fallacies are the best tool for self-deception.

Take advantage of the limited human working memory, and try to convince the audience that your opponent claimed something incorrect earlier in the debate. In reality, they claimed something similar, but no one remembers it vividly enough to dispute.

Create false dichotomies; trichotomies, even. People love separating things into disparate categories, even when those separations are illusory and there’s plenty of overlap.

Redefine words if you have to to make your conclusion sound plausible. It doesn’t matter that the vast majority of people use certain words as intended. You must be correct, which means English must be wrong.

On that note, abuse the continuum fallacy. Asking someone to draw a hard line on concepts with unavoidable grey areas makes you sound reasonable.

Every word is a Sorites paradox waiting to be called out.

Every moral grey area immediately indicates moral relativism, which means moral nihilism.

7. Psychoanalyze & Stereotype

There are fewer pleasures in life than thinking you understand someone better than they understand themselves.

You must put your insightfulness on full display. Speak with the tone of an experienced therapist as you make claim after claim about the other person’s psyche.

And go deep. Don’t just stick to the obvious things you can glean from what they say and how they act.

Assume their personality type, political affiliation, religious status, age, career, life experience, etc.

If you can’t find anything, just make it up. Stereotypes that make your opponent look bad are an easy win.

8. Pivot

Control the discussion, but not in a way that leads to further progress.

If your point is about to be debunked, raise a completely unrelated point and continue in that direction. But do it subtly and with charisma. You want to make your opponent look weird for circling back around and trying to finish what you started.

Never answer a difficult question directly. Resort to comedy, one-liners, gotchas, and thought-terminating cliches.

When asked a hypothetical, pretend like it’s irrelevant to the discussion when it actually is. Hypotheticals reveal your true beliefs, and you don’t want that.

9. Retreat To The Abstract

The moment you get into concrete details of evidence and logic, you’ve entered a losing battle.

Your beliefs only sound good at a high level, so any time you sense the debate starting to deal with tangible concepts, immediately begin philosophizing and speaking in vague generalities. Go into epistemology if you have to.

10. Move The Goalposts

Your argument has been debunked.

Instead of accepting defeat, shift the argument to something that sounds related, but is ultimately tangential to the overall point. Then, make that your new argument your overall point hinges on.

Your opponent can never win because the criteria for you losing the argument is ever-shifting.

You have to be subtle, though, otherwise people will notice and stop talking to you.

11. Exhaust Your Opponent

The best way to win is to frustrate your opponent, even if you deserve it.

Interrupt your opponent before they can formulate a coherent point.

Deliberately misconstrue and strawman them so that they have to spend valuable time re-iterating.

Go in circles. The bigger the circle, the better.

Make subtle digs at your opponent. Get them off their game.

Nothing frustrates a good faith debater more than a bad faith one pretending to be good.

12. Force A Stalemate

It’s over.

You have no more arguments because your opponent has debunked all of them.

You can’t admit defeat, lest you appear humble and open-minded.

There’s only one thing to do.

Apply an “argument-ending phrase.”

As the name implies, these are phrases which conclude an argument in a way that makes you appear reasonable. Examples include:

  • Let’s agree to disagree
  • We each have our own opinion
  • It seems we’re at an impasse
  • Well, it is what it is

Normally, you’re supposed to apply these when the core of a disagreement can only be resolved subjectively.

But why restrict yourself? So what if the topic is based in fact and logic? So what if the truth actually exists outside of your own preconceived biases?

Your opponent is being absolutist for trying to make definitive claims about reality. You’re the reasonable one, remember?

Conclusion

Wow! I feel mischievous already! I can see how tempting it is to rely on these tactics to avoid losing a debate.

That said, I’ll pass. I don’t need to resort to such filth; I’m above that.

If you see someone using these, link them this article so they can learn.

Thank you, and have a good faith day.

http://theaxolot.wordpress.com/?p=5428
Extensions
Don’t Refactor Like Uncle Bob (Second Edition)
Uncategorizedcodingjavaprogrammingsoftware-developmenttechnology
When I heard a year ago that Uncle Bob was planning on writing a second edition of Clean Code, I got excited, which isn’t normal for me. I thought the first edition was alright, and I don’t read often. Maybe it was the thought of getting to roast its code examples again like I did […]
Show full content

When I heard a year ago that Uncle Bob was planning on writing a second edition of Clean Code, I got excited, which isn’t normal for me. I thought the first edition was alright, and I don’t read often.

Maybe it was the thought of getting to roast its code examples again like I did in my first ever article.

Maybe it was the promise of a modernization to the teachings from the first, kind of like the excitement you get from reading patch notes to a piece of software you use.

Or maybe, deep down, I was hoping to see someone revise his ideas after so long, and realize he has to change his outlook on “clean code”. After all, that’s been the most scathing criticism of the first edition since it was published over seventeen years ago.

As much as I am a cynic who likes to poke fun, I genuinely respect those who admit they’re wrong and change their minds. I feel a deep joy when my ideas reach people and change their outlooks on things they’re passionately wrong about (though I sometimes wonder if I’m sabotaging my efforts by being belligerent).

So imagine my disappointment when, after spending $60 on this eBook, I found that Bob has not only changed his tune about his most controversial practices, he’s actually doubled down.

Oh yeah.

But I’m getting ahead of myself.

Update: That sentence should’ve read “…I found that Bob has not only not changed his tune…” Whoops.

The Good

I find myself agreeing with the vast majority of his views at a high level.

He discusses how professionals have a duty to keep code from rotting by actively applying clean code principles, even when it slows you down in the short term.

I especially loved his hypothetical but all-too-painful anecdote of code getting so bad that a separate thread of work is dedicated to a complete rewrite. This of course leads to bugs slipping through, as well as a draining overhead of keeping the rewrite up to date with the legacy code in terms of new features and bug fixes.

He discusses the importance of clean code, not just for productivity, but for ethical reasons such as the harm that software errors can cause in our software-dependent society.

His passion for clean code is clearly born from harrowing experiences. The first chapter is a must-read for these principles alone.

His principles on architecture and design are SOLID (heh) as usual, and I’d recommend reading those sections too if you’re fascinated about the intricacies of what makes good architecture.

You can tell this edition was updated for modern times. There are discussions about LLMs and their place in software development, and Bob even uses Grok and Copilot to compare his refactorings to AI-generated ones.

What I found especially pleasant was his focus on using modern languages and constructs to present his ideas. For one, he doesn’t just stick to Java. He uses Golang, Python, and JavaScript as well. And even when he uses Java, he takes advantage of more modern constructs like lambdas, streams, records, and pattern-matching. He’s clearly embraced a lot of functional programming concepts, and it’s a delight to see.

He breaks down each refactoring into digestible intermediate steps, and thoroughly explains his thought process between each. It frustrates me when experts immediately jump to their final solution and justify it after-the-fact, so I’m glad Bob didn’t do this.

Every discussion of a potentially controversial belief of his addresses counterarguments that people have raised. A lot of the time, I’d think of a caveat or exception to one of his ideas, and right in the next paragraph, he’d bring it up. That’s a sign of someone who’s privy to technical discussions, and it’s a definite improvement from the first edition.

In fact, the last section of the book is a transcript of a famous discussion between him and John Ousterhout challenging his clean code principles, which was a nice treat.

Whatever you liked about the first edition, it’s here, and there’s more of it.

The Bad

But all the things you didn’t like are back. With a vengeance.

He reused some of the code examples from the first edition, namely the godawful GuessStatisticsMessage & PrimeGenerator classes, and still believes them to be even remotely clean enough to be presented in a book like this.

But rather then rehash old code, I’ll take a look at one of the newer samples. The following is the first major example in the book, and it’s from the second chapter, Clean That Code!. With the help of AI, Bob deliberately wrote this code uncleanly for demonstration purposes:

public class FromRoman {
    public static int convert(String roman) {
        if (roman.contains("VIV") ||
                roman.contains("IVI") ||
                roman.contains("IXI") ||
                roman.contains("LXL") ||
                roman.contains("XLX") ||
                roman.contains("XCX") ||
                roman.contains("DCD") ||
                roman.contains("CDC") ||
                roman.contains("MCM")) {
            throw new InvalidRomanNumeralException(roman);
        }
        roman = roman.replace("IV", "4");
        roman = roman.replace("IX", "9");
        roman = roman.replace("XL", "F");
        roman = roman.replace("XC", "N");
        roman = roman.replace("CD", "G");
        roman = roman.replace("CM", "O");
        if (roman.contains("IIII") ||
                roman.contains("VV") ||
                roman.contains("XXXX") ||
                roman.contains("LL") ||
                roman.contains("CCCC") ||
                roman.contains("DD") ||
                roman.contains("MMMM")) {
            throw new InvalidRomanNumeralException(roman);
        }
        int[] numbers = new int[roman.length()];
        int i = 0;
        for (char digit : roman.toCharArray()) {
            switch (digit) {
                case 'I' -> numbers[i] = 1;
                case 'V' -> numbers[i] = 5;
                case 'X' -> numbers[i] = 10;
                case 'L' -> numbers[i] = 50;
                case 'C' -> numbers[i] = 100;
                case 'D' -> numbers[i] = 500;
                case 'M' -> numbers[i] = 1000;
                case '4' -> numbers[i] = 4;
                case '9' -> numbers[i] = 9;
                case 'F' -> numbers[i] = 40;
                case 'N' -> numbers[i] = 90;
                case 'G' -> numbers[i] = 400;
                case 'O' -> numbers[i] = 900;
                default -> throw new InvalidRomanNumeralException(roman);
            }
            i++;
        }
        int lastDigit = 1000;
        for (int number : numbers) {
            if (number > lastDigit) {
                throw new InvalidRomanNumeralException(roman);
            }
            lastDigit = number;
        }
        return Arrays.stream(numbers).sum();
    }

    public static class InvalidRomanNumeralException extends RuntimeException {
        public InvalidRomanNumeralException(String roman) {
        }
    }

It works as follows:

  1. Validate the input string against impossible three-digit sequences.
  2. Replace two-digit characters involving subtractive notation by a custom single numeral character.
  3. Check for unnecessary repetitions of certain digits (“IIII” should be “IV”).
  4. Go through each character and convert it to the equivalent decimal number, taking into account the custom numerals from earlier, while placing them into an array.
  5. Make sure the numbers are in non-increasing order (to catch invalid numbers like “VX”).
  6. Add up all the numbers and return the final result.

Bob conducts his refactoring step-by-step throughout the rest of the chapter. I won’t show the steps; you can read the book for yourself if you’re interested.

Side Note: I want to mention that this is quite a tricky example. Unless you already know the best algorithm, it’s hard to determine whether to do a cleanup refactoring, or an algorithmic refactoring. The former is when you reduce duplication, make syntax more concise, extract functions or variables, etc. The latter is a revision of the logic itself in hopes of simplification or optimization.

Here’s Uncle Bob’s refactoring:

public class FromRoman2 {
    private String roman;
    private List<Integer> numbers = new ArrayList<>();
    private int charIx;
    private char nextChar;
    private Integer nextValue;
    private Integer value;
    private int nchars;
    Map<Character, Integer> values = Map.of(
            'I', 1,
            'V', 5,
            'X', 10,
            'L', 50,
            'C', 100,
            'D', 500,
            'M', 1000);

    public FromRoman2(String roman) {
        this.roman = roman;
    }

    public static int convert(String roman) {
        return new FromRoman2(roman).doConversion();
    }

    private int doConversion() {
        checkInitialSyntax();
        convertLettersToNumbers();
        checkNumbersInDecreasingOrder();
        return numbers.stream().reduce(0, Integer::sum);
    }

    private void checkInitialSyntax() {
        checkForIllegalPrefixCombinations();
        checkForImproperRepetitions();
    }

    private void checkForIllegalPrefixCombinations() {
        checkForIllegalPatterns(
                new String[]{"VIV", "IVI", "IXI", "IXV", "LXL", "XLX",
                        "XCX", "XCL", "DCD", "CDC", "CMC", "CMD"});
    }

    private void checkForImproperRepetitions() {
        checkForIllegalPatterns(
                new String[]{"IIII", "VV", "XXXX", "LL", "CCCC", "DD", "MMMM"});
    }

    private void checkForIllegalPatterns(String[] patterns) {
        for (String badString : patterns)
            if (roman.contains(badString)) throw new InvalidRomanNumeralException(roman);
    }

    private void convertLettersToNumbers() {
        char[] chars = roman.toCharArray();
        nchars = chars.length;
        for (charIx = 0; charIx < nchars; charIx++) {
            nextChar = isLastChar() ? 0 : chars[charIx + 1];
            nextValue = values.get(nextChar);
            char thisChar = chars[charIx];
            value = values.get(thisChar);
            switch (thisChar) {
                case 'I' -> addValueConsideringPrefix('V', 'X');
                case 'X' -> addValueConsideringPrefix('L', 'C');
                case 'C' -> addValueConsideringPrefix('D', 'M');
                case 'V', 'L', 'D', 'M' -> numbers.add(value);
                default -> throw new InvalidRomanNumeralException(roman);
            }
        }
    }

    private boolean isLastChar() {
        return charIx + 1 == nchars;
    }

    private void addValueConsideringPrefix(char p1, char p2) {
        if (nextChar == p1 || nextChar == p2) {
            numbers.add(nextValue - value);
            charIx++;
        } else
            numbers.add(value);
    }

    private void checkNumbersInDecreasingOrder() {
        for (int i = 0; i < numbers.size() - 1; i++)
            if (numbers.get(i) < numbers.get(i + 1))
                throw new InvalidRomanNumeralException(roman);
    }

    public static class InvalidRomanNumeralException extends RuntimeException {
        public InvalidRomanNumeralException(String roman) {
            super("Invalid Roman numeral: " + roman);
        }
    }
}

Nothing was learned, it seems. This works as follows:

  1. Check against invalid sequences of numerals (illegal prefixes and unnecessary repetitions)
  2. Loop through the roman string’s characters. In each iteration:
    • Get the current letter and the next (if there is a next)
    • If the current letter is ‘I’, ‘X’, or ‘C’, check if the next letter accepts it as a prefix. If so, subtract its value from the next letter’s value, add that to a list, and skip the next letter for the following iteration.
    • Otherwise, just add the value of that single letter to the list.
  3. Finally, check that no number is greater than the previous in the list.

First of all, he took a pure function and turned it into an instance method with attributes, instead of passing around arguments. He did this last time too, but this time he gives reasons, which I’ll discuss in a later section.

Second, and just like last time, his method decomposition is bad.

For example, the doConversion method calls out to three other methods, but these methods don’t reduce duplication, nor do they improve the comprehensibility of the overall method. Sure, it reads like a high-level checklist of steps, but unless your target audience is non-technical people, all this accomplishes is obfuscating HOW the conversion happens.

Once a reader enters the doConversion method, they’ve mentally accepted that they’re going to see some ugly details. The word “conversion” makes this clear, so arbitrarily abstracting them away into their own functions just wastes time. I have to go three methods down, each containing the word “convert”, so I can see how the conversion works. Why?

Code should be “blunt” when being “polite” results in being “opaque”.

I’ll admit, it’s not that bad in this example, because after I read each method, I found their names to be intuitive. But did you catch that?

After I read each method.

Since there are no arguments, and no promise of purity (as evidenced by the abundance of instance variables), I have to blindly trust that each method’s name adequately describes what the method does without any side effects.

If you have the hindsight bias that Uncle Bob has, this isn’t an issue. But I believe code should be as readable for first-time readers as it is for those familiar with it. And a major component of readability is the trust that each method does what it says it does. Because without that trust, readers will feel compelled to read those methods anyway, and the abstraction benefits that Bob purports are nullified, while the costs of indirection remain.

Obviously, pure functions aren’t automatically trustworthy, but it’s not a binary. Purity leans more toward determinism and self-containedness, which leans far more toward trustworthiness than the above.

This is what happens when you optimize for superficial readability (nice method names with hidden complexity), while allowing unpredictable behavior to proliferate (stateful entanglement).

I could go line-by-line and explain every instance of code that made me go “huh?” or “seriously?” (like why is nchars an instance variable when it’s only used in one method?) (it’s actually used in two methods, my bad). But it would probably be easier if I just presented my own refactoring, while maintaining Bob’s general logic.

public class FromRoman3 {
    private static final Map<Character, Integer> ROMAN_NUMERALS = Map.of(
            'I', 1,
            'V', 5,
            'X', 10,
            'L', 50,
            'C', 100,
            'D', 500,
            'M', 1000);
    private static final Map<Character, Character> NUMERAL_PREFIXES = Map.of(
            'V', 'I',
            'X', 'I',
            'L', 'X',
            'C', 'X',
            'D', 'C',
            'M', 'C'
    );
    private static final String[] ILLEGAL_PREFIX_COMBINATIONS = new String[]{
            "VIV", "IVI", "IXI", "IXV", "LXL", "XLX",
            "XCX", "XCL", "DCD", "CDC", "CMC", "CMD"
    };
    private static final String[] IMPROPER_REPETITIONS = new String[]{
            "IIII", "VV", "XXXX", "LL", "CCCC", "DD", "MMMM"
    };

    public static int convert(String roman) {
        if (containsIllegalPatterns(roman, ILLEGAL_PREFIX_COMBINATIONS) ||
                containsIllegalPatterns(roman, IMPROPER_REPETITIONS)) {
            throw new InvalidRomanNumeralException(roman);
        }
        List<Integer> numbers = new ArrayList<>();
        int i = 0;
        while (i < roman.length()) {
            char currentLetter = roman.charAt(i);
            char nextLetter = i == roman.length() - 1 ? 0 : roman.charAt(i + 1);
            if (!ROMAN_NUMERALS.containsKey(currentLetter)) {
                throw new InvalidRomanNumeralException(roman);
            } else if (NUMERAL_PREFIXES.getOrDefault(nextLetter, (char) 0) == currentLetter) {
                int num = ROMAN_NUMERALS.get(nextLetter) - ROMAN_NUMERALS.get(currentLetter);
                numbers.add(num);
                i += 2;
            } else {
                int num = ROMAN_NUMERALS.get(currentLetter);
                numbers.add(num);
                i += 1;
            }
        }
        if (containsIncreasingNumbers(numbers)) {
            throw new InvalidRomanNumeralException(roman);
        }
        return numbers.stream().mapToInt(Integer::intValue).sum();
    }

    private static boolean containsIllegalPatterns(String roman, String[] patterns) {
        for (String badString : patterns)
            if (roman.contains(badString)) return true;
        return false;
    }

    private static boolean containsIncreasingNumbers(List<Integer> numbers) {
        for (int i = 0; i < numbers.size() - 1; i++)
            if (numbers.get(i) < numbers.get(i + 1)) return true;
        return false;
    }

    public static class InvalidRomanNumeralException extends RuntimeException {
        public InvalidRomanNumeralException(String roman) {
            super("Invalid Roman numeral: " + roman);
        }
    }
}

First thing I did was replace all instance variables with local variables passed around through arguments. That alone massively increased readability.

Second, I extracted every explicit mention of roman numerals into constants. I did this partly for performance reasons. But really, I did it so I could label those numerals with constant names rather than rely on function names like Bob.

Third, I revised the function structure.

I made the convert function into the meatiest function of the whole class. You came all the way here to see how numeral conversion works, so here it is in all its glory.

I changed checkForIllegalPatterns to containsIllegalPatterns, and brought the exception throws into the main function. I felt this was more explicit. The word “contains”, along with the rest of the signature, clearly indicates what the function does. The word “check” doesn’t tell you what happens if the check fails.

I changed checkNumbersInDecreasingOrder to containsIncreasingNumbers and brought the exception out, similar to the pre-validation steps. But there’s no duplication here, so why did I keep the method? Two reasons:

  1. This method can be understood in isolation.
  2. This post-validation step isn’t the main purpose of the convert function.

The thing I struggled the most with was the conversion loop. Bob coded it in a way that would’ve caused a lot of duplication had I simply inlined addValueConsideringPrefix. I wanted to clean this up without changing his algorithm.

The first idea that came to mind was using a Map<Character, Character[]> to map each prefix letter to its potential next letter. But as I was implementing that idea, I realized I could just reverse the mapping so that each letter was mapped to its prefix. And since the reversed mapping was unique, I didn’t need Character[] as the value type.

After that, it was a simple matter to keep all the logic inside the loop (which I changed to a while loop to make the index incrementation more obvious).

Now of course, by doing this, I’ve lost the conciseness of the existing pattern-matching, but I think it was worth it to avoid indirection.

By the way, Bob was nice enough to provide a comprehensive test suite, which was much appreciated for a tricky algorithm like this. So I’m as confident in my code as he is with his.

import fromRoman.FromRoman.InvalidRomanNumeralException;
import org.junit.jupiter.api.Test;

import static fromRoman.FromRoman.convert;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class FromRomanTest {
    @Test
    public void valid() throws Exception {
        assertThat(convert(""), is(0));
        assertThat(convert("I"), is(1));
        assertThat(convert("II"), is(2));
        assertThat(convert("III"), is(3));
        assertThat(convert("IV"), is(4));
        assertThat(convert("V"), is(5));
        assertThat(convert("VI"), is(6));
        assertThat(convert("VII"), is(7));
        assertThat(convert("VIII"), is(8));
        assertThat(convert("IX"), is(9));
        assertThat(convert("X"), is(10));
        assertThat(convert("XI"), is(11));
        assertThat(convert("XII"), is(12));
        assertThat(convert("XIII"), is(13));
        assertThat(convert("XIV"), is(14));
        assertThat(convert("XV"), is(15));
        assertThat(convert("XVI"), is(16));
        assertThat(convert("XIX"), is(19));
        assertThat(convert("XX"), is(20));
        assertThat(convert("XXX"), is(30));
        assertThat(convert("XL"), is(40));
        assertThat(convert("L"), is(50));
        assertThat(convert("LX"), is(60));
        assertThat(convert("LXXIV"), is(74));
        assertThat(convert("XC"), is(90));
        assertThat(convert("C"), is(100));
        assertThat(convert("CXIV"), is(114));
        assertThat(convert("CXC"), is(190));
        assertThat(convert("CD"), is(400));
        assertThat(convert("D"), is(500));
        assertThat(convert("CDXLIV"), is(444));
        assertThat(convert("DCXCIV"), is(694));
        assertThat(convert("CM"), is(900));
        assertThat(convert("M"), is(1000));
        assertThat(convert("MCM"), is(1900));
        assertThat(convert("MCMXCIX"), is(1999));
        assertThat(convert("MMXXIV"), is(2024));
    }

    @Test
    public void invalid() throws Exception {
        assertInvalid("ABE"); // I added this one
        assertInvalid("IIII");
        assertInvalid("VV");
        assertInvalid("XXXX");
        assertInvalid("LL");
        assertInvalid("CCCC");
        assertInvalid("DD");
        assertInvalid("MMMM");
        assertInvalid("XIIII");
        assertInvalid("LXXXX");
        assertInvalid("DCCCC");
        assertInvalid("VIIII");
        assertInvalid("MCCCC");
        assertInvalid("VX");
        assertInvalid("IIV");
        assertInvalid("IVI");
        assertInvalid("IXI");
        assertInvalid("IXV");
        assertInvalid("VIV");
        assertInvalid("XVX");
        assertInvalid("XVV");
        assertInvalid("XIVI");
        assertInvalid("XIXI");
        assertInvalid("XVIV");
        assertInvalid("LXL");
        assertInvalid("XLX");
        assertInvalid("XCX");
        assertInvalid("XCL");
        assertInvalid("CDC");
        assertInvalid("DCD");
        assertInvalid("CMC");
        assertInvalid("CMD");
        assertInvalid("MCMC");
        assertInvalid("MCDM");
    }

    private void assertInvalid(String r) {
        assertThrows(InvalidRomanNumeralException.class, () -> convert(r));
    }
}

Bob claims his code is cleaner than the original. It’s not. I actually like the original function’s forthcomingness. It decomposes nothing, and yet, the fact that it barely has any branching makes it easy to comprehend. I even found the double-digit numeral to single-digit numeral conversion to be…elegant?

Seriously, it’s thanks to this trick that the conversion loop of numerals to decimals has practically zero logic. It might as well be a table. Bob points out that it doesn’t pass all the test cases, but with very minor changes, I was able to fix it.

This isn’t the only code example where Bob over-decomposes, but I’d be here all day if I went through each one.

The Butchering

After the above refactoring, he writes this:

You might be a functional programmer horrified that the functions are not “pure.” But, in fact, the static convert function is as pure as a function can be. The others are just little helpers that operate within a single invocation of that overarching pure function. Those instance variables are very convenient for allowing the individual methods to communicate without having to resort to passing arguments. This shows that one good use for an object is to allow the helper functions that operate within the execution of a pure function to easily communicate through the instance variables of the object.

And in Chapter 7: Clean Functions, he argues that the following three variations of a sigma function are equally impure:

public static double sigma(double… ns) {
    var mu = mean(ns);
    var deviations = Arrays.stream(ns)
      .map(x->(x-mu)*(x-mu))
      .boxed().mapToDouble(x->x);
    double variance = deviations.sum() / ns.length;
    return Math.sqrt(variance);
}
public static double sigma(double… ns) {
    double mu = mean(ns);
    double variance = 0;
    for (double n : ns) {
        var deviation = n - mu;
        variance += deviation * deviation;
    }
    variance /= ns.length;
    return Math.sqrt(variance);
}
public static double sigma(double… ns) {
    return new SigmaCalculator(ns).invoke();
}

private static class SigmaCalculator {
    private double[] ns;
    private double mu;
    private double variance = 0;
    private double deviation;

    public SigmaCalculator(double… ns) {
        this.ns = ns;
    }

    public double invoke() {
        mu = mean(ns);
        for (double n : ns) {
            deviation = n - mu;
            variance += deviation * deviation;
        }
        variance /= ns.length;
        return Math.sqrt(variance);
    }
}

And you might wonder how he could possibly believe this?

Simple. He misunderstands the definition of function purity. I mean, he gets it mostly right until he says this:

How do you create a pure function? Simple. Don’t change the value of any variables; or to paraphrase the famous line from Mommie Dearest: “No Assignment Statements Ever!” Or to say that in yet another way: Pure functions are immutable.

His source?

Functional Design, by Robert C. Martin.

Uncle Bob has conflated two different functional programming principles.

There’s the principle of purity, which means no side-effects, that is, functions modifying outside their scope.

Then, there’s the principle of immutability, which emphasizes the lack of variable reassignments and mutations of existing values.

You can adhere to the first without adhering to the second.

This doesn’t sound like a big deal, but it’s a HUGE part of his rationalization. Since assignment statements are “impure” in his mind, he believes the first two examples to be just as impure as the third.

Now we’ve got instance variables and all manner of variable manipulations. And yet, the sigma function is pure. None of those impure operations are visible outside the sigma function. The bottom line is that purity is an external characteristic of a function, not an internal characteristic. It does not matter how impure the internals of a function are—that function will be pure so long as all the impurity is hidden from all external observers (including other threads).

He butchers the definition of function purity to essentially apply only from the perspective of public methods.

I’m genuinely appalled.

The concept of purity is meant to apply to ALL functions, not just the outermost ones. It’s supposed to make code easy to reason about, and that includes implementation details.

Bob claims that passing around instance variables is less of an overhead than function arguments. It’s no surprise he thinks his, considering he believes his method’s names are so precise and descriptive that arguments aren’t necessary.

Let’s imagine someone decides to hop into a method to understand an implementation detail.

They see a method littered with references to instance variables, and then they wonder what values those variables had before.

Maybe this method depends on certain variables being initialized a certain way at certain times.

Maybe you can’t properly call this method without calling certain other methods first.

In other words, each method depends on shared state.

Does Bob expect people read through every method before the one they want to understand? Doesn’t that defeat the whole point of abstraction?

Bob has replaced the overhead of method arguments, with an even more problematic overhead of shared state. The fact that this state exists only within a specific instance of a class doesn’t make it acceptable.

Pure functions read like contracts (arguments are part of that contract). They take specific input, whatever state that input may be in, perform a certain set of operations on that input, and output the same result every time. This makes them easier to understand in isolation, which reduces mental burden. The only “shared” state, if you can even call it that, is what the higher-level function passes as arguments between function calls.

Stateful methods without arguments ask you to take their names at face value, as if knowingly trying to dissuade you from the ugliness underneath. Every time you step into a method, you have to add a node into your mental execution graph (which you do anyway), but then you have to factor in the state of each instance variable between function calls.

This might not be so bad if the shared state is only between two functions that are called consecutively. But what happens when this goes four levels deep in four different call hierarchies? Bob must have a crazy powerful working memory to be able to juggle all this.

You might argue that there’s no significant cognitive difference between local arguments passed around as arguments, and instance variables being referenced. But there is, and it involves scope. By keeping a variable’s scope as small as possible, you reduce the space across which readers have to keep it in mind to be able to reason about it.

If this weren’t the case, you could simply have EVERY variable be at class scope, and it wouldn’t matter. Even Bob wouldn’t write code like that.

So, either Bob isn’t aware of these costs, or he is aware, but SEVERELY underestimates them. I believe it’s the latter, but either way, it’s a damn shame.

Conclusion

My opinion is similar to last time.

Follow the advice at a very high level, but ignore the examples. If you were expecting improvements there, you won’t find it.

I will say, Bob comes across as less dogmatic about his code changes in this edition, which is nice. But I don’t think you can play the “let’s agree to disagree” card when the “improved” code is this bad. I know that’s harsh, but there’s no polite way to say this.

Anyway, thanks for reading, and have a nice day.

P.S

It’s been about five days since this book was published on Amazon, and I haven’t seen a single article or video about it since. It’s just been radio silence. Even Uncle Bob himself hasn’t been marketing it. I’m getting kinda creeped out here.

It’s lonely.

http://theaxolot.wordpress.com/?p=4590
Extensions
A Refreshing Philosophy of Software Design
Uncategorizedbook-reviewbooksprogrammingsoftware-developmenttechnology
I pride myself on being the kind of blogger who writes what he truly believes, even when it’s unpopular. That’s part of why I’m so brash and come across as condescending. It’s cathartic for me, so if being correct is condescending, I don’t want to be wrong. Having said that, it feels good when I […]
Show full content

I pride myself on being the kind of blogger who writes what he truly believes, even when it’s unpopular. That’s part of why I’m so brash and come across as condescending. It’s cathartic for me, so if being correct is condescending, I don’t want to be wrong.

Having said that, it feels good when I find others whose opinions mostly resonate with mine. I very recently finished reading A Philosophy of Software Design, by John Ousterhout (I know I’m super late on that), and I’ve come to the conclusion that it should be mandatory reading for every software engineer.

The Good

First of all, it’s packed with content that isn’t easily found elsewhere. Both junior and senior engineers can learn a lot.

He explains complexity as a combination of dependencies and obscurity (though some problems have inherent complexity), and how it slowly creeps through incremental changes to code.

The concepts of interface depth and information hiding are excellent ways to explain what modularity looks like at the program, API, service, class, and even function level.

His use of the text editor example to explain many of his concepts felt natural and not forced at all.

In chapter 19, he gives his takes on some popular software trends such as OOP, Agile, Testing, etc.

My favorite take of his is on Test-Driven Development, in which he proclaims he’s not a fan because it’s too incremental and distracts from high-level design. He only likes it when fixing bugs. That, my readers, is the correct take, and it alone puts him leagues above most.

And best of all, he isn’t dogmatic about in his presentation. In fact, each of his points is full of nuance and discussion of specific caveats that I haven’t seen anywhere else. Throughout every chapter, I just kept thinking, “This guy knows his stuff.”

The Bad

The bad doesn’t outweigh the good, but I still have to point out what bothered me.

First of all, the ratio of code examples to advice is lacking. A lot of it will resonate with experts in the abstract, because we have lived experiences of said scenarios. But the less experienced may struggle to internalize some lessons without concrete examples to anchor them to. Ousterhout admits this in the introduction, but still.

But my real issues are with the presentation of these examples. A lot of the time, Ousterhout will present an interface and point out the flaws that lead to brittleness and complexity. But then, he’ll instantly come up with another interface and explain why it’s better.

His explanations are correct. It’s just that the way he presents them gives the impression that you can design a good interface from requirements alone, but then doesn’t explain how he reasoned his way to it. It feels like post-hoc rationalization of his expertise rather than a process anyone can deduce their way through.

The biggest culprit of this is the text editor example. I’ve never designed one of these before, so I was looking forward to hearing his thought process throughout. I was able to follow along, but a more junior developer would certainly struggle.

Am I supposed to believe that Ousterhout is so brilliant that he comes up with modular interfaces right off the bat? Of course not. You implement, your understanding grows alongside said implementation, and then you build your design. There’s not a single intermediate design iteration in the book. It goes straight from bad to good.

There were some other things that bothered me, too.

Chapter 10: Define Errors Out Of Existence had some great points, though it was a bit overstated. I rarely encounter instances of unjustified exceptions, though the examples he mentions, namely key deletion and substring, do fit the bill. I don’t know if it really warranted an entire chapter, though.

Chapter 11: Design it Twice is also not worth a chapter. Do you really need to tell people to try out multiple designs?

In Chapter 12, he “debunks” common excuses that people use to forgo writing comments and documentation.

He claims that self-documenting code is a myth because there are often too many details in an interface to communicate through code alone. The example he uses to support his stance is the ambiguity of Java’s built-in substring method. Specifically, whether its end is inclusive, and what happens if the start index exceeds the end index. Without its documentation, you’d have to read the method anyway to know these.

Now I don’t know what Ousterhout has been through, but surely this is an exaggeration. Yes, there may be details of a method that are difficult to communicate through its signature alone. But if the details are so significant, then that indicates bad design (assuming the functionality itself isn’t just unintuitive by nature). Ousterhout himself points this out in Chapter 10, so I don’t know why he uses this example. Java libraries aren’t the gold standard for code. People don’t actually believe that, right?

Right?

But there’s another factor to consider. Libraries, services, and APIs are in a different realm to classes and methods. The more granular you get, the less valuable documentation becomes, because it’s very hard to document these parts without just restating the implementation. Ousterhout acknowledges this in Chapter 16.

I don’t see people claiming that high-level documentation should be omitted, or that we shouldn’t write comments for confusing code in methods. In fact, I often see the opposite in code reviews, which is good.

“Good code is self-documenting” doesn’t mean, “No comments are allowed.” It means, “Comments shouldn’t be the default; they should be the last resort after code is made as clear as possible, yet there’s still confusion.”

I can tell he understands this because in Chapter 18, he says mentions comments as a way to compensate when code is nonobvious. So why devote so much of the book toward best practices regarding comments, when the majority of readers would already agree with his stance, assuming they can even understand it? It’s fragmented across four chapters, and not even consecutive, mind you, so I had a hard time piecing it together to explain to you here.

In Chapter 13, he actually gives good examples of low-level comments, but I think he prematurely generalizes those examples to claim that low-level documentation is more necessary than people believe. But the reason it’s valuable in these specific examples is because he’s dealing with an inherently complex and uninituitive topic (extremely low-level memory manipulation for RPC functionality), and it’s in C++. Interestingly, it’s at this chapter that he starts using C++ in his examples. A bit fishy, I have to say.

In Chapter 15, he advocates using comments as a design tool. That is, using comments to plan your code and filling them in with the abstractions that follow. He doesn’t explicitly say this, but it’s clear this is meant to be an alternative to TDD (*shudder*).

He even uses similar talking points like, “If you wait until after your implementation to write comments, you may decide not to write them at all,” and, “Comments are a design tool for interfaces.” I use comments to keep myself on track and plan out my implementations, but not the way he describes it.

Why would I document a class, method, or even a variable before I write it?

Why don’t I just write it and let the interface arise naturally as the implementation grows?

Code reviews can point out the need for comments, so does it matter what order I write them in?

What if comments are used as crutches to explain badly designed code?

Chapter 18: Code Should Be Obvious is super lackluster.

There are so many ways to increase the clarity of your code, yet he only mentions superficial aspects, like using white space judiciously and comments.

For things that decrease clarity, he mentions:

Event-driven programming (a.k.a indirect calls). I guess, but are people defaulting to event-driven calls so much that this is worth mentioning?

Generic containers (like Java’s Pair class). Once again, suspiciously specific. What about languages where multiple values can be grouped together without having to name the resulting object?

Different types for declaration and allocation (like List<Integer> = new ArrayList<Integer>();). Okay, this is starting to get silly. Is polymorphism bad now?

Violating readers’ expectations. This one’s alright.

Chapter 20: Designing for Performance barely scratches the surface. There are so many more performance optimizations to consider, and much more common than the low-level example he uses. What about bad queries and unnecessary API calls? Caching?

More generally, he barely talks about functional programming, whose principles strongly push you toward modular design right out the gate.

What about databases and SQL? Are they not software?

The book feels very C++ and Java-centric, even though it was first published in 2018. I’m not saying Ousterhout has to only use modern languages, but he should at least acknowledge that object-oriented languages carry a lot of legacy baggage, and that it might cause this book, and even his philosophy, to become outdated soon.

The Spooky

In Chapter 9, Ousterhout talks about decomposition. Specifically, when to separate modules into smaller components or bring them together to reduce complexity. In chapter 9.7 & 9.8, he discusses how and when methods should be decomposed. Sound familiar?

This was the topic of my article last week, and it spooked me how similar my points were to his, despite my never having read this book before, and not seeing my ideas expressed elsewhere with such precision:

  • He calls out LoC as a dumb metric for function decomposition, though much more politely than I do.
  • He explains the main cost of decomposition, the main one being the spread of complexity rather than its reduction.
  • He says that you should favor decomposition when the subfunction can be understood in isolation, and the parent function can be understood without the implementation of the child function, which is almost exactly what I said in Lesson 4 of my article.
  • He advocates organizing code into independent blocks.

I could go on. He explicitly pushes back on Clean Code‘s ridiculous function length recommendations, which I have to admit is pretty bold. I’d recommend people read the book for this alone. Treat it as a rebuttal to Clean Code.

Oh, and I’ll just drop this here.

Conclusion

I know I spent a lot of time on the bad, but believe me when I tell you that it’s not that significant compared to the rest of the book. My issues were more with how much time was spent on things I felt didn’t warrant such attention, and things I wished were discussed, rather than things that were outright wrong. I’m just a thorough guy. Anything I didn’t mention in the above sections, I consider good, or just not worth mentioning.

Overall, you should read this book if you haven’t already. And if you think Clean Code is good, then you REALLY NEED to read this.

But read my articles first so you can see just how on point I am.

Thank you.

Read my articles!

http://theaxolot.wordpress.com/?p=3855
Extensions
LoC Is a Dumb Metric for Functions
Uncategorizedcodingprogrammingsoftware-developmenttechnology
What’s the quickest giveaway that someone’s an amateur developer? When they tell you to break out a function into smaller functions, and the first reason they cite is “it’s over X lines of code.” Yeah, you heard me. This article is targeted at people who are so easily intimidated by long functions that they fail […]
Show full content

What’s the quickest giveaway that someone’s an amateur developer?

When they tell you to break out a function into smaller functions, and the first reason they cite is “it’s over X lines of code.”

Yeah, you heard me.

This article is targeted at people who are so easily intimidated by long functions that they fail to see deeper issues with them.

It’s about people who take a linear function with no repetition or need for reusability, break it out into smaller functions, and think they accomplished something.

I’ve written one-liner functions (besides getters and setters). I’ve written one-hundred-liner functions. I’ve written everything in between, and I have no regrets. So let me teach you some things you should’ve been taught a long time ago.

Lesson 1: Stop using aesthetic arguments

I cringe when developers say “this function just doesn’t look clean” to justify refactoring in lieu of actual reasoning, as if code cleanliness is an aesthetic judgment that only a true connoisseur can determine.

Intuition is valuable, but it’s a hint, not the answer. There’s always a deeper reason. You just may not be articulate enough to express it using reasoned arguments. I can help, but…

Lesson 2: “Lines Of Code” is the last thing you should think about

Referring to LoC to determine function cleanliness is like judging a paragraph of a book by how long it is. If the exact same information can be communicated in fewer lines of code, and if all else is equal, then sure, conciseness is valuable.

But all else is rarely equal. While function length can exacerbate cleanliness problems, it’s rarely the root cause.

Lesson 3: Cognitive Complexity

A superior metric to LoC is CC. A good linter will warn you when your cognitive complexity reaches a certain amount. I try to keep my functions under a CC of 15, but it’s up to you and your team.

What matters is that you don’t treat it as gospel.

What matters even more is that you try to simplify your function’s logic BEFORE you even think about decomposing. Because if you do, you may fall below your CC threshold, and you won’t feel the need to decompose at all.

Lesson 4: There are costs to decomposition

Too many developers will downplay the cost of function decomposition and act like it’s only costly below a certain LoC.

That’s noob talk.

Locality is the design principle of grouping related things together. It’s a mental shortcut or heuristic that allows the mind to assume a higher degree of independence from the code block being read, easing the mental burden. Spreading code across your codebase diminishes locality.

Linearity is the idea of having instructions be read one after the other, from top to bottom, just like how code is executed. Having to jump back and forth from function call to function call reduces linearity.

Context Overhead is how much context from the calling function you need to understand a called function, and vice versa. Ideally, this would be zero; the calling function treats the called function as a black box, and the called function can be understood in isolation.

Too often, people will decompose functions by line count, yet the details of each sub-function are HEAVILY intertwined with the overall purpose of the main function, such that there’s no real separation to ease the mental burden.

Functions that modify passed-in variables (like appending to a list), or worse, globals.

Functions that require a LOT of local variables to be passed in.

Functions that have a LOT of return values that aren’t necessarily related to each other.

And worst of all, function signatures that do a bad job of summarizing what the function is supposed to do.

You’ll notice this when the function name uses very simple yet vague language (i.e getDetails, setupInfrastructure, etc), but the body is so much more involved than the name implies.

The authors had a difficult time coming up with a good name because it’s impossible. There doesn’t exist a high-level description for the code in this function that isn’t just a restatement of the implementation.

You’ll have to read these functions anyway to understand what they do, so you haven’t actually reduced complexity, you just moved it.

Coupling is inevitable, but when you introduce a function that other parts of the codebase will call, they are now coupled to this function. Sometimes this coupling is desirable, but in the future, you may find that each caller acquires a slightly different twist to the same function. Now you have to either inline the function to some of the callers, make separate functions for each caller, or modify the function to handle the unique concerns and paths of the callers.

I’ll talk more about this in the next section.

Lesson 5: Reusability and deduplication are king

The very first thing you should think of when considering decomposition is whether this same functionality will be called elsewhere, the goal being to reduce code duplication.

Just make sure that the duplication you’re removing isn’t coincidental. “Coincidental duplication” is two or more pieces of code that just happen to be similar now, but are likely to diverge in the future.

Consider the principle of a Single Source of Truth. In the context of functions, this is when you have business logic that you want to standardize across your modules.

Also consider the Rule of Three. A single duplication is often a coincidence, but another one points to a pattern.

I say all this because what’ll often happen is that people will prematurely create functions because duplication is ugly. But when unique cases arise from the callers, a lot of people find it hard, psychologically speaking, to let go of said functions. Instead, they’ll Frankenstein-stitch on specific cases to account for each caller, leading to multiple unintuitive code paths (*shudder*).

Lesson 6: Testability, Dependency Injection, and Purity

Sometimes, the code you’re extracting is so involved and so tricky that you want to test it on its own, rather than exercising it through your current interface. In that case, you should consider extracting into a public method, whether it’s in the same class or a different one. But this should be a last resort, and the function you’re extracting shouldn’t mess with the internals of your class. It should be as pure as possible.

On a similar note, you may find that specific blocks of code work better as wrappers over external dependencies (DBs, files, third-party APIs, etc), and you want to be able to mock these dependencies in your tests. That’s another good candidate for extraction.

Lesson 7: Maybe the calling function is just doing too much

I’ve been talking about splitting blocks of code into separate functions, but what if the overall function itself needs refactoring?

Maybe it’s trying to do too many things.

Maybe it itself needs to be split into separate functions.

Maybe it takes so many arguments, causing a combinatorial explosion of scenarios that makes it hard to reason about and test, where more smaller functions would be easier.

Zeroing in on cleaning up the function in front of you can blind you to the bigger picture that maybe the function itself is bad.

Lesson 8: Self-containment and naming

Okay, let’s say you’ve dealt with the above scenarios, but your function is still painful to read. At this point, I’m assuming that your temptation to extract is solely because of complexity, and that duplication reduction isn’t a concern. You may have one of three issues:

  1. You have code that complicates the function to the point that it distracts from the function’s overall purpose, such as comprehensive validation logic before the real meat of the function, a tricky algorithm, or involved loop logic that the reader really doesn’t need to see right now.
  2. Your function is still a mish-mash of code that’s hard to hold in your head.
  3. Your business logic is just that complicated.

For (1), I’m inclined to agree. Sometimes you have logic that, while vital, just distracts you from what the function’s really about at a higher-level. For such scenarios, I would actually advise extracting to a separate function, even if it’s just private.

But consider Lesson 4, especially Context Overhead, to see if it’s worth it, and don’t be a chicken. If it’s just a few simple lines of code, it’s likely not worth extracting. Don’t let the “ugliness” scare you.

(2) I mentioned earlier that code that can be reasoned about in isolation is important, because once you understand it, you can use that understanding when reading the calling function.

However, there are some tricks you can use to better present your code without the costs of extracting to a separate function, if you find the costs to be high.

Just like how books are broken into paragraphs, you can break your code into blocks to hint to the reader that those specific lines of code are more related to each other than they are to others. This is a psychology concept called “chunking”.

Variable naming is also often overlooked. Too many developers use functions as a crutch to reduce complexity, when in reality they just have mediocre variable names. Get off it. Good names prime readers to infer functionality without having to read every single line.

For the messier blocks (i.e the ones filled with local context), you can place a comment at the top of each block to summarize if that helps. For these kinds of code blocks, in-place comments improve comprehension more than extracted functions (remember Lesson 4).

“Nooo! Comments lie!” And you think function names don’t? At least comments lie to your face, whereas functions hide the lies elsewhere.

In other words, if you feel the need to extract to a function, try blocking your code, using comments for the messy parts, and improving your naming first. That fixes most readability issues for those who haven’t yet drank the Clean Code Kool-Aid.

(3) I can’t help you with this. You might need to revisit your requirements or suck it up.

An Example

I was inspired to write this after reading through the first example of refactoring in Martin Fowler’s book, Refactoring: Improving the Design of Existing Code (highly recommend, btw).

The initial version of the code can be found here: Theatrical-Players-Refactoring-Kata/javascript/src/statement.js at main · emilybache/Theatrical-Players-Refactoring-Kata · GitHub

And the refactored code can be found here: GitHub – kanpou0108/refactoring-2nd-martinfowler: Refactoring: Improving the Design of Existing Code by Martin Fowler, Second Edition

To be clear, Fowler took the initial statement.js script, and split it into statement.js & createStatementData.js. The main idea was to separate the String summarization of the statement from the creation of the statement.

It’s a great refactor! And he acknowledges that while splitting these incurs a performance penalty, the readability and maintainability benefits far outweigh that cost.

But there also some weird choices that irked me in that they reek of the fear of “dirty code” I described above.

Starting with my biggest issue: Inheritance.

plays are either comedies or tragedies, and each play type has its own way of calculating an amount (i.e a price given the size of the audience), and volumeCredits (i.e customer loyalty points given the size of the audience).

In my opinion, Fowler doesn’t do a good job of justifying the use of polymorphism this early. There are only two types, and each type only has two methods. And neither are used outside this module.

Also, he uses JavaScript which doesn’t enforce method implementation in subclasses at compile-time like Java interfaces do, so even that benefit isn’t recognized here.

It would’ve been better to simply have a function to handle pricing, and another one for loyalty points, switching on the play type.

Oh wait, he actually did this in an earlier iteration before replacing it with polymorphism.

function amountFor(aPerformance) {
    let result = 0;
    switch (aPerformance.play.type) {
    case "tragedy":
      result = 40000;
      if (aPerformance.audience > 30) {
        result += 1000 * (aPerformance.audience - 30);
      }
      break;
    case "comedy":
      result = 30000;
      if (aPerformance.audience > 20) {
        result += 10000 + 500 * (aPerformance.audience - 20);
      }
      result += 300 * aPerformance.audience;
      break;
    default:
        throw new Error('unknown type: ${aPerformance.play.type}')
    }
    return result;
}

function volumeCreditsFor(aPerformance) {
    let result = 0;
    result += Math.max(aPerformance.audience - 30, 0);
    if ("comedy" === aPerformance.play.type) result += Math.floor(aPerformance.audience / 5);
    return result;
}

Extracting this simple functionality to separate classes is so unnecessary because it doesn’t reduce complexity, it just spreads it out. If you feel these functions are doing too much (especially amountFor), let me try this.

function calculatePrice(performance) {
    let price = 0;
    switch (performance.play.type) {
    case "tragedy":
      price = 40000;
      if (performance.audience > 30) {
        price += 1000 * (performance.audience - 30);
      }
      break;
    case "comedy":
      price = 30000;
      if (performance.audience > 20) {
        price += 10000 + 500 * (performance.audience - 20);
      }
      price += 300 * performance.audience;
      break;
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
    return price;
}

Wow, it’s suddenly super easy to read! What magic did I just do?

I just did some renaming:

  • amountFor was an awful name to begin with, so I changed it to calculatePrice.
  • result was vague, so I changed it to price.
  • I removed the unnecessary “a” from aPerformance.

Does your mind feel lighter reading this?

How about this?

function calculatePrice(performance) {
    switch (performance.play.type) {
    case "tragedy":
      let price = 40000;
      if (performance.audience > 30) {
        price += 1000 * (performance.audience - 30);
      }
      return price;
    case "comedy":
      let price = 30000;
      if (performance.audience > 20) {
        price += 10000 + 500 * (performance.audience - 20);
      }
      price += 300 * performance.audience;
      return price;
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
}

All I did was move the outer-level price variable declaration into each block and return early. Now, your mind can reason about each block independently, without having to hop over the default block to see what happens with price after the switch statement.

I guess I introduced some duplication by declaring price twice, but surely you can see it’s worth it. Let’s touch up volumeCreditsFor, first with some nice renaming:

function calculateCustomerCredits(performance) {
    let credits = 0;
    credits += Math.max(performance.audience - 30, 0);
    if ("comedy" === performance.play.type) credits += Math.floor(performance.audience / 5);
    return credits;
}

Now, I’m gonna do something unexpected:

function calculateCustomerCredits(performance) {
    switch(performance.play.type) {
    case "tragedy":
      return Math.max(performance.audience - 30, 0);
    case "comedy":
      return Math.max(performance.audience - 30, 0) +
        Math.floor(performance.audience / 5);
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
}

I introduced switching logic to the credit calculation function, even though by doing so, I’ve actually increased its complexity. Why?

First, it’s good practice to handle the default scenario, even though it would never be reached in calculateCustomerCredits, since calculatePrice is called first, and would’ve thrown an exception if the play type was unknown.

But the real reason I did this is because I’m not naive. I know that there’s a good chance that polymorphism will be introduced in the future as new play types are added.

Even though I’ve duplicated the base amount calculation in both blocks, this’ll make it trivially easy to move these implementations into subclass methods. Because if you noticed in Fowler’s refactored code, get volumeCredits has a super implementation, whose only purpose is to calculate this base amount.

Remember earlier when I mentioned “coincidental duplication?” This is what happens when you’re too averse to it. You end up with unnecessary coupling.

Now, let’s zoom out a bit and refactor createStatementData. Here’s how Fowler’s version looks:

export default function createStatementData(invoice, plays) {
  const result = {};
  result.customer = invoice.customer;
  result.performances = invoice.performances.map(enrichPerformance);
  result.totalAmount = totalAmount(result);
  result.totalVolumeCredits = totalVolumeCredits(result);
  return result;

  function enrichPerformance(aPerformance) {
    const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance));
    const result = Object.assign({}, aPerformance);
    result.play = calculator.play;
    result.amount = calculator.amount;
    result.volumeCredits = calculator.volumeCredits;
    return result;
  }
  function playFor(aPerformance) {
    return plays[aPerformance.playID];
  }
  function totalAmount(data) {
    return data.performances
      .reduce((total, p) => total + p.amount, 0);
  }
  function totalVolumeCredits(data) {
    return data.performances
      .reduce((total, p) => total + p.volumeCredits, 0);
  }
}

function createPerformanceCalculator(aPerformance, aPlay) {
    switch(aPlay.type) {
    case "tragedy": return new TragedyCalculator(aPerformance, aPlay);
    case "comedy" : return new ComedyCalculator(aPerformance, aPlay);
    default:
        throw new Error(`unknown type: ${aPlay.type}`);
    }
}
class PerformanceCalculator {
  constructor(aPerformance, aPlay) {
    this.performance = aPerformance;
    this.play = aPlay;
  }
  get amount() {
    throw new Error('subclass responsibility');}
  get volumeCredits() {
    return Math.max(this.performance.audience - 30, 0);
  }
}
class TragedyCalculator extends PerformanceCalculator {
  get amount() {
    let result = 40000;
    if (this.performance.audience > 30) {
      result += 1000 * (this.performance.audience - 30);
    }
    return result;
  }
}
class ComedyCalculator extends PerformanceCalculator {
  get amount() {
    let result = 30000;
    if (this.performance.audience > 20) {
      result += 10000 + 500 * (this.performance.audience - 20);
    }
    result += 300 * this.performance.audience;
    return result;
  }
  get volumeCredits() {
    return super.volumeCredits + Math.floor(this.performance.audience / 5);
  }
}

First thing I’m gonna do is remove the PerformanceCalculator class hierachy and replace it with the straightforward procedural functions from above.

export default function createStatementData(invoice, plays) {
  const result = {};
  result.customer = invoice.customer;
  result.performances = invoice.performances.map(enrichPerformance);
  result.totalAmount = totalAmount(result);
  result.totalVolumeCredits = totalVolumeCredits(result);
  return result;

  function enrichPerformance(aPerformance) {
    const result = Object.assign({}, aPerformance);
    result.play = playFor(aPerformance);
    result.amount = calculatePrice(result);
    result.volumeCredits = calculateCustomerCredits(result);
    return result;
  }
  function playFor(aPerformance) {
    return plays[aPerformance.playID];
  }
  function totalAmount(data) {
    return data.performances
      .reduce((total, p) => total + p.amount, 0);
  }
  function totalVolumeCredits(data) {
    return data.performances
      .reduce((total, p) => total + p.volumeCredits, 0);
  }

  function calculatePrice(performance) {
    switch (performance.play.type) {
    case "tragedy":
      let price = 40000;
      if (performance.audience > 30) {
        price += 1000 * (performance.audience - 30);
      }
      return price;
    case "comedy":
      let price = 30000;
      if (performance.audience > 20) {
        price += 10000 + 500 * (performance.audience - 20);
      }
      price += 300 * performance.audience;
      return price;
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
  }

  function calculateCustomerCredits(performance) {
    switch(performance.play.type) {
    case "tragedy":
      return Math.max(performance.audience - 30, 0);
    case "comedy":
      return Math.max(performance.audience - 30, 0) +
        Math.floor(performance.audience / 5);
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
  }
}

Now I’m gonna inline and delete the playFor, totalAmount, and totalVolumeCredits functions, because why have one-line functions that are only used in one place? They take focus away from enrichPerformance, calculatePrice, and calculateVolumeCredits, which is where the good stuff is.

export default function createStatementData(invoice, plays) {
  const result = {};
  result.customer = invoice.customer;
  result.performances = invoice.performances.map(enrichPerformance);
  result.totalAmount = result.performances
      .reduce((total, p) => total + p.amount, 0);
  result.totalVolumeCredits = result.performances
      .reduce((total, p) => total + p.volumeCredits, 0);
  return result;

  function enrichPerformance(aPerformance) {
    const result = Object.assign({}, aPerformance);
    result.play = plays[aPerformance.playID];
    result.amount = calculatePrice(result);
    result.volumeCredits = calculateCustomerCredits(result);
    return result;
  }

  function calculatePrice(performance) {
    switch (performance.play.type) {
    case "tragedy":
      let price = 40000;
      if (performance.audience > 30) {
        price += 1000 * (performance.audience - 30);
      }
      return price;
    case "comedy":
      let price = 30000;
      if (performance.audience > 20) {
        price += 10000 + 500 * (performance.audience - 20);
      }
      price += 300 * performance.audience;
      return price;
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
  }

  function calculateCustomerCredits(performance) {
    switch(performance.play.type) {
    case "tragedy":
      return Math.max(performance.audience - 30, 0);
    case "comedy":
      return Math.max(performance.audience - 30, 0) +
        Math.floor(performance.audience / 5);
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
  }
}

Finally, let’s improve the naming:

export default function createStatementData(invoice, plays) {
  const statement = {};
  statement.customer = invoice.customer;
  statement.performances = invoice.performances.map(addPlayInfo);
  statement.totalPrice = statement.performances
      .reduce((total, perf) => total + perf.price, 0);
  statement.totalCustomerCredits = statement.performances
      .reduce((total, perf) => total + perf.customerCredits, 0);
  return statement;

  function addPlayInfo(performance) {
    const performanceClone = Object.assign({}, performance);
    performanceClone.play = plays[performance.playID];
    performanceClone.price = calculatePrice(performanceClone);
    performanceClone.customerCredits = calculateCustomerCredits(performanceClone);
    return performanceClone;
  }

  function calculatePrice(performance) {
    switch (performance.play.type) {
    case "tragedy":
      let price = 40000;
      if (performance.audience > 30) {
        price += 1000 * (performance.audience - 30);
      }
      return price;
    case "comedy":
      let price = 30000;
      if (performance.audience > 20) {
        price += 10000 + 500 * (performance.audience - 20);
      }
      price += 300 * performance.audience;
      return price;
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
  }

  function calculateCustomerCredits(performance) {
    switch(performance.play.type) {
    case "tragedy":
      return Math.max(performance.audience - 30, 0);
    case "comedy":
      return Math.max(performance.audience - 30, 0) +
        Math.floor(performance.audience / 5);
    default:
        throw new Error('unknown type: ${performance.play.type}')
    }
  }
}

And we have a lovely looking function with subfunctions that don’t overstate their importance, procedural calculations that aren’t coupled to class hierarchies but can easily be made to if needed, and naming that doesn’t mislead readers into prioritizing structural refactoring over in-place refactoring.

I still kept three subfunctions, though.

addPlayInfo(performance) is quite involved, but it’s also easier to understand in a self-contained fashion. Though I wouldn’t really object if it was also inlined.

calculatePrice and calculateCustomerCredits are heavily numerical, contain branches, and are also easy to reason about independently.

That said, I do want to point to an advantage with the polymorphic approach that my procedural approach lacks. My code checks the play types twice, once in calculatePrice, and once in calculateCustomerCredits. That’s a bit of duplication that could prove annoying with more play types. I can keep that in mind, while still holding off on abstraction until later.

I tried to keep the overall logic of the program the same as Fowler’s to make my point.

It takes true skill to look at a function and accurately assess whether or not it’s carrying its weight, that is, whether its simplification of the problem space is enough to justify its distraction and indirection.

Learn it.

So in summary, you should lean toward extraction when:

  • The function would be easy to reason about in isolation
  • The signature does a good job of implying how the function transforms inputs into outputs.
  • Duplication can be significantly reduced.
  • You want to standardize certain business logic.
  • You want to test said functionality on its own.
  • When it works better as an external dependency.
  • When you’ve done your due diligence in cleaning up your current function, but it’s still too complicated.
  • Certain details significantly distract from the overall function, and you want readers to get a high-level view before they worry about these details.

and lean away when:

  • You’re doing it just to abide by some arbitrary LoC metric.
  • You can’t think of a straightforward signature for the function, which hints at bad design or context overhead.
  • You’re unsure of whether your duplication is coincidental or not.
  • You haven’t tried to improve the readability of your overall function first.
  • The benefits outweigh the costs from Lesson 4.

Use your judgment, and don’t be bullied by people who prescribe specific line numbers.

http://theaxolot.wordpress.com/?p=3402
Extensions
Test Driven Development: Bad Example
Uncategorizedprogrammingsoftware-developmenttechnologyTest-Driven Development
If you’ve read my previous articles on TDD (here and here), you’d know I have complicated feelings about it. If you’ve read my review of a critically acclaimed novel, you’d know I have major trust issues when it comes to book recommendations. And if you’ve read my first ever article, you’d know that big names […]
Show full content

If you’ve read my previous articles on TDD (here and here), you’d know I have complicated feelings about it.

If you’ve read my review of a critically acclaimed novel, you’d know I have major trust issues when it comes to book recommendations.

And if you’ve read my first ever article, you’d know that big names can still have terrible ideas.

TDD isn’t a well-defined practice in the industry. Whenever someone claims their team does TDD, it can mean any of the following:

  • We write automated tests (in no particular order)
  • We write all our tests upfront, then implement them
  • We follow TDD exactly as described in Kent Beck’s book (red-green-refactor)
  • We do TDD, but only for bug fixes and/or small features.

This makes it hard to have discussions about it because most people aren’t talking about the same thing to begin with.

I don’t care that much about this phenomenon. Words change over time. If TDD evolves to mean a practice that the industry has developed on its own (without perfectly adhering to the theory), then that’s what it is. When it comes to who’s using the correct terminology, it’s majority rules.

Instead, I want to talk about something very specific, that being the source of it all; Kent Beck’s 2003 book, Test Driven Development : By Example. A step-by-step guide on how to apply TDD, using actual examples and not just theory. Here’s an excerpt from the introduction:

This book follows two TDD projects from start to finish, illustrating techniques programmers can use to easily and dramatically increase the quality of their work. The examples are followed by references to the featured TDD patterns and refactorings. With its emphasis on agile methods and fast development strategies, Test-Driven Development is sure to inspire readers to embrace these under-utilized but powerful techniques.

Beck’s goal with this book isn’t just to demonstrate TDD, but to inspire people to use it by showing how much better it is. Here were my expectations going in:

  • A thorough and coherent explanation of the process
  • Theory that is well-supported and stands up to scrutiny
  • Examples that illustrate its strengths over traditional development
  • An honest discussion of its drawbacks
  • Well-presented and communicated material

I think that’s reasonable, don’t you?

The Theory

Kent Beck’s TDD process works as follows (paraphrasing):

  1. Jot down expected behavior in a list
  2. Pick a behavior and write a test for it in code with your imagined perfect interface (public methods), and watch it fail because you haven’t implemented it.
  3. Write just enough production code to make that one test pass
  4. Refactor your code to remove duplication
  5. Add more behaviors in your list as you discover more about the problem through implementation.
  6. Repeat.

This is called the “Red-Green-Refactor” loop, and it’s the backbone of the whole philosophy.

Even though tests play a large role in the process, the goal isn’t actually the tests themselves, but the design (though the tests are a bonus). The purported benefits are:

  • Better interfaces
  • Coupling reduction between components
  • Reduction in defects
  • Increased test coverage
  • Higher quality tests that don’t assume implementation details

His evidence is…anecdotal. Though programming studies are notorious for their methodological flaws, and he admits as much, so I won’t fault him for that.

It’s really hard to tell which design improvements he purports are exclusive to test-first, as opposed to testing at all, because they can all be practiced independently of TDD, and they often are.

But that’s not all, because the supposed benefits are also psychological:

  1. It gives you immediate feedback on your interface design early on
  2. It gives programmers confidence when they refactor their code (thanks to a test suite that catches regressions as they happen).
  3. It breaks down each change into small, manageable steps. (Red means your test isn’t a false positive, Green lets you focus only on passing the test, and Refactor lets you focus only on improving implementation)
  4. It increases trust within and between teams (because of the extra emphasis on testing)
  5. It increases motivation during development (because of seeing the test bars go from red to green and reducing monotony)

Let’s break each of these down and show why they’re either incorrect, misleading, or don’t require TDD.

(1) This is probably the most commonly cited benefit of TDD.

The idea is that, without TDD, developers are far more likely to dig themselves a hole with an interface that’s hard to properly test, likely because of logic that’s difficult to isolate. Then when it comes time to test, they encounter difficulties, and rather than improving their code, they slack on testing and push their code through.

This is also why Beck says tests should be written one at a time. Because if you write all your tests at once, then later on have to make major changes to the interface you’re testing, all those tests have to be modified, and the same resistance to change rears its head.

Where do I start?

This all sounds good in theory, but it’s just that; theory.

First of all, there’s the assumption that testing your interface early will expose design flaws more quickly. But in order for this to be effective, the developer has to already know how to write good tests. Otherwise, how would they know their design is hard to test?

But if they already know that, then they very likely have a good intuition on how to write their code such that it’s easy to test. In which case, TDD is just a bottleneck.

Second, interface design is decided by the consumers, not you, the developer/tester.

For APIs and GUIs, that would be the needs of internal or external customers.

And for lower-level components, that’s decided by the implementations of the higher-level components.

I think he believes that by writing tests one at a time, that refactors will be smaller because the developer does it bit-by-bit with each test, rather than making sweeping changes at once.

This is idealistic. I wish it was like this. But any expert developer will tell you that even a single line of code can open up a whole can of worms, requiring a major refactoring.

If you have a suite of tests you’ve accumulated with TDD, then discover something during implementation that requires a major refactor, or your requirements change (which happens often), then guess what? Beck misses this entirely, which is that YOU’LL STILL HAVE REWRITE YOUR TESTS ANYWAY.

(2) This isn’t exclusive to TDD. Tests written later can give the same confidence during a refactor. And I’m not even just talking about tests written after the whole implementation is complete (test-first vs test-last is a false dichotomy). Some people write tests after certain implementation milestones are reached to defend against regressions.

(3) Also not exclusive to TDD. Beck himself has a TODO list separate from his test suite. Manual testing as a measure of progress is just as valid. Then tests can be written later to handle edge cases once the interface has crystallized.

(4) Once again, not exclusive to TDD. High-quality tests don’t require TDD. If TDD is adopted voluntarily by a team, they likely also deeply care about test quality to begin with. It’s self-selection, not a consequence of TDD.

If management tries to enforce TDD, they also have to adopt something like pair programming so that everyone’s work is watched by someone else. I’d hardly call that “trust.” But even so, forcing people to use TDD will just result in half-baked tests. After all, if you have to write test code before production code when you don’t want to/don’t know how to, you’ll write fewer or worse tests.

(5) This is so subjective and Beck provides no evidence. Many would find it irritating and distracting to constantly switch between production and test code with such granular changes in between.

If you’re gonna claim (or even strongly imply) that TDD is superior without hard evidence, while not leaving any room for whether TDD’s success is situational or dependent on the developer, I’ll call you out. The only drawback Beck cites for TDD is that it won’t help you if you don’t care about writing good code. Gimme a break!

Many developers are able to comfortably work in a traditional development workflow. If you claim TDD is superior without qualification, then you must believe that it hasn’t caught on simply because of fear, laziness, or misunderstanding. Unless you have strong evidence, that’s an extremely arrogant stance to take (which tracks).

The Examples

Enough about the theory. Let’s look at how Beck applies TDD to actual problems. There are two examples he uses in the book, but I want to focus on the first one because it’s the most concrete and the easiest to explain.

InstrumentSharesPriceTotalIBM100025 USD25000 USDNovartis400150 CHF60000 CHFTotal65000 USD
FromToRateCHFUSD1.5

Beck presents a hypothetical bond portfolio system in USD. A new requirement has come in: the system needs to support multi-currency portfolios, with an exchange rate and a total in a desired currency.

Beck starts with this test, and demonstrates TDD from there using Java:

public void testMultiplication() {
    Dollar five = new Dollar(5);
    five.times(2);
    assertEquals(10, five.amount);
}

This is a test for multiplying a dollar amount by a scalar value to get a new dollar amount. This is meant to handle getting the total value of a specific bond (price per share * # shares).

I’m not gonna walk through every single TDD step he does, because that would take ages. He continually revises the objects and adds more tests as the solution evolves, so critiquing an early draft wouldn’t make sense. I’m just showing this for context.

But already, something’s wrong. Why are we starting our tests from the lowest-level objects? This Dollar object doesn’t exist, which implies that either:

  • This is a greenfield system (not just a new requirement in an existing system)
  • There’s some other object in the system that holds dollars (like a Money object)
  • That all money operations in the system until now have been operating with raw Java numbers (with the assumption that it’s all in USD)

This is already confusing because we, the readers, have no idea how the higher-level systems expect money to be represented or to behave at a code-level. Beck doesn’t explain this at all, so I can only infer from the scant details he’s described and the implementation itself (which is completely backwards). Presentation is just as important as information.

You can read the 16-chapter long TDD process yourself. But I can tell you that it doesn’t make TDD look good at all. The amount of revisions Beck makes is staggering. If anyone actually developed like this, I’d worry.

Beck also doesn’t provide a final, authoritative version of the code in the book at the end of the section, so rather than piece it together from each step, I found it online. Once again, presentation could use improvement.

Here’s a link to the final code. Take a look and keep in mind the following:

  • This repo uses modern Java, but the underlying meaning is the same
  • The repo organizes classes into folders, but the book does no such thing
  • testArrayEquals, testFrancMultiplication & testDifferentClassEquality are older tests that were created then deleted later.
  • This book was written in 2003, before constructs like Enums were introduced to Java.
  • Money is supposed to be an abstract class, not a concrete one

Update: Here’s an even better repo for the code. It removes remnants of the previous TDD steps. Also, the Money class was actually refactored to be concrete, with the Dollar and Franc classes being removed in Chapter 10. Apologies for not making this clear.

In the spirit of TDD, let’s look at the tests first. He has tests for the following scenarios:

  • Money equality and inequality, with same and different currencies
  • Scalar Money multiplication
  • Same-currency and different-currency addition
  • Sum with another sum
  • Sum with times

Good stuff.

But as I looked at the code more closely, I saw so many problems.

What if bank.reduce is called with currencies that aren’t in its hashtable? The Bank class doesn’t handle this scenario

Are negative amounts allowed? There’s no guard for this in the code

Why is money represented by integers? What about cents?

These aren’t just nitpicks. It pokes holes in the whole concept that these scenarios weren’t considered, yet TDD is presented as the solution to bad testing.

But here’s the big one. Why are we using INHERITANCE to represent summations of money? Are you kidding me? I thought this was a joke when I first saw it.

Beck’s inspiration for this came from math, like how “(2 + 3) * 5” is evaluated as:

(2 + 3) * 5

= 5 * 5

= 25

where the operations in the parentheses are evaluated first.

You have a list of shares, the value of each share, the number of each share, and a bank with conversion rates. That’s all we know, so just do this:

public Money getTotalPortfolioValue(List<Money> shareTotals,
 Bank bank, String desiredCurrency) {
    Money portfolioValue = Money.zero();
    for (Money shareTotal : shareTotals) {
        Money convertedShareTotal = shareTotal.reduce(bank, desiredCurrency);
        portfolioValue = portFolioValue.plus(convertedShareTotal);
    }
    return portfolioValue;
}

No need for a separate object. Just do the work and only complicate when requirements demand it.

Beck has arbitrarily decided to bundle sums of money into an object because…it’s elegant like math? My most charitable guess is that he wanted to bundle the amounts into an expression so that he could delay evaluation until he decided which currency to reduce to.

He doesn’t say this. I’m giving him the benefit of the doubt and reading between the lines.

But this doesn’t accomplish anything that my above solution can’t either. If you can pass around a Sum object, you can pass around a List<Money> too. There’s no need to encode summation in an object. A function does the trick just fine.

It’s not more performant either, because Sum.reduce is recursively defined, so Money.reduce has to be called for each Money in the sum anyway, just like my function.

It’s interesting that there isn’t also a Prod expression for scalar multiplications. If you’re gonna represent math, don’t stop at summations (that’s a joke btw).

It’s because of this attempt at elegance that Bank.reduce has a clunky execution flow.

Take a simple conversion like bank.reduce(Money.dollar(5), "CHF"):

  • bank.reduce(Money.dollar(5), "CHF") ->
  • return Money.dollar(5).reduce(this, "CHF") ->
  • int rate = bank.rate(“USD”, "CHF"); ->
    return new Money(5 / rate, "CHF");
  • if ("USD".equals("CHF")) return 1;
    Integer rate = (Integer) rates.get(new Pair("USD", "CHF"));
    return rate.intValue();

Notice how we hop into the Bank class, then it PASSES ITSELF into an Expression object (Money in this case), then we hop into the Money object, then BACK to the Bank class to get the rate? What’s going on here!?

I’ll tell you what’s going on. Beck didn’t want the money-related objects to be responsible for their own conversions, probably because of some rigid idea of the object-oriented responsibilities of each class.

But Expression.reduce already takes a Bank object, so why the unnecessary pass-through?

Phew. That’s enough.

I know there’s gonna be that guy who’s like, “B-b-but it’s just a toy example. The point isn’t the correctness or elegance, it’s just to demonstrate TDD”.

If you’re trying to showcase the strengths of TDD and claim it will increase the productivity and quality of your work, but your process is really cumbersome, badly presented, and results in clunky design, what am I supposed to think as a reader?

There’s another severe flaw that applies to both examples, and it’s something that every zealot does to pontificate their paradigm’s strengths while ignoring weaknesses.

Any paradigm can look good when applied to silly, simple examples. But the true measure of any process is how adaptable it is. What about DB calls, third-party APIs, file operations, GUI, other side effects, etc. Surely these were relevant concepts even in 2003, so why’s this book considered the ultimate guide to TDD?

There are other relatively minor flaws with the book, but I think I’ve said enough. Even if you like this specific practice of TDD,, surely you can admit this book leaves a lot to be desired.

God, I’m tired. But the work is done, and at least I learned the word, “augend,” today.

This wouldn’t be a fair review if I didn’t also talk about the positives.

First, I respect Beck for sticking to his guns and presenting TDD in all its glory; both the beautiful and ugly parts. Many advocates will shy away from discussing the parts that feel like a slog to get through, but Beck is disciplined, and I don’t believe he’s a grifter trying to profit off industry buzzwords.

I also want to commend Beck for something we take for granted, but is impressive nonetheless. When performing operations on an object’s attributes (like Money.times earlier), he favors returning a new resulting object over mutating the original, because he understands the dangers of side effects.

I like that he switches between Java and Python to demonstrate that TDD principles are language-agnostic.

There’s some good advice in Part III about how to set up testing in certain tricky scenarios, and general refactoring advice. I’d recommend people read that, if nothing else.

I think it takes courage to pour so much time into a piece of work you care deeply about, and share it with the public in hopes of influencing them. Even if that opens you up to criticism. I’m sure Beck and many’s experiences have made them feel that TDD was the best solution for their problems, and that’s valid.

I don’t know how much of Beck’s beliefs have changed since publishing, and honestly, I don’t care that much. This isn’t personal at all. I like to criticize works on their own merits.

Anyway, thanks for reading my longest article so far, and have a nice day.

http://theaxolot.wordpress.com/?p=2631
Extensions
Be An Agnostic Programmer
Uncategorizedailogicprogrammingsoftware-developmenttechnologytestingwriting
If civil engineering is a mature field, then software development is a baby. An ugly baby, but perhaps a late bloomer. The so-called “best practices” of our industry are a chaotic nebula of differing, often incompatible, perspectives. “If you don’t follow TDD, you’re not a professional.” “If you think OOP leads to overengineering, you’re doing […]
Show full content

If civil engineering is a mature field, then software development is a baby. An ugly baby, but perhaps a late bloomer. The so-called “best practices” of our industry are a chaotic nebula of differing, often incompatible, perspectives.

“If you don’t follow TDD, you’re not a professional.”

“If you think OOP leads to overengineering, you’re doing it wrong.”

100% code coverage is the gold standard.”

But I’m not here to discuss individual practices in detail. Instead, I want to zoom out and look at how we got here in the first place. Strap in, because this is gonna get philosophical.

Margaret Hamilton coined the term “software engineering” to lend the discipline a legitimacy when the moon landing was in the works. It seems to have worked. Although nowadays, the term is so broad that a simple React web app also qualifies.

When you include the word “engineering” in your title, people think your discipline is this scientific, rigorous, methodological process that yields the best answer based on collective historical experience. And to the layman, it might seem so. After all, software runs on machines, and machines fall under “engineering.”

But let’s not kid ourselves.

Software development isn’t a science.

It’s not an art, either. It’s a mix of both, and that’s why I love it.

The science is in the logic of your program and the architecture of your system. It’s in how well you can prove the correctness of your code (invariants, assertions, tests, etc). And it’s in how you investigate and deduce your way to the root causes of issues.

But there’s a human element, too.

With every sizable new system or feature, you explore while your design isn’t yet crystallized, and that gives room to creativity. You don’t always know how your interfaces (APIs, classes, namespaces, modules, etc) will end up until you have a good amount implementation in front of you (part of my dislike of by-the-book TDD).

And just like a UI/UX designer, you need empathy and foresight to ensure a good experience for future maintainers, even if you have to break conventions sometimes. If that’s not art, then what is?

Know the rules well, so you can break them effectively

But you can’t break the rules without a diverse set of tools in your toolbox. Whenever I see someone proselytize a specific programming paradigm as a one-size fits all “best practice”, it’s because they don’t get this.

Programming attracts people who are enamored with logic in action. Unfortunately, it also attracts people who can be obsessive about it, to the point where they fallaciously believe that programming is a discipline with hard rules like other kinds of engineering. And when that happens, you end up exclusively subscribing to a paradigm your were taught as “the correct way,” and it becomes difficult to think outside of that box.

The classic example is Object-Oriented Programming. Many developers had their first exposure to programming through this paradigm, and were taught little else. So when they went into the workforce, they constantly ran into problems that didn’t map neatly onto OOP principles. So what did they do? They forced them to, like a square peg when all you have are round holes.

I was lucky in that I was exposed to many programming styles during my early education, such that I don’t feel constrained to adhering to a single one. Does that make me unprincipled? No, it makes me versatile. Git gud.

Some endure a hard journey of unlearning these rigid beliefs. But others double down and become dogmatists, unable to concede to the drawbacks of their favorite paradigm lest they invalidate their whole identity as developers. And some, though far fewer, become grifters who profit off the air of authority that being “principled” gives them.

Programming books are a major culprit, because they hold the ultimate air of authority. Honest authors will:

  • Acknowledge when something is more their opinion than fact
  • Highlight the drawbacks of their suggested methodologies as well as their benefits
  • Give leeway for breaking of their rules in service of the ultimate goal of code: to be maintained by humans (until AI takes over, I mean)

But dishonest authors will:

  • Intersperse personal opinions with facts to subtly convert readers
  • Treat their preferred paradigm like a product to be sold (e.g downplaying the flaws)
  • Assert objective superiority of their methodologies in all scenarios
  • Cherry-pick or misrepresent studies that confirm what they’re pushing and ignore those that contradict

(Note: Isn’t it interesting how we rarely cite studies when debating the best programming guidelines? But rather we always appeal to principles and/or psychology?).

But the biggest culprit of all, in my opinion, is a lack of confidence.

Every discipline has beginners that ask questions like, “What’s the best way to do X?” or “Is it okay if I don’t do X?”. In fields involving creativity, the most common answer is, “it depends.”

People still give general recommendations, but the experts are confident enough in their skills to pick and choose which guidelines to follow, yet humble enough to acknowledge they can’t push hard rules.

Expert software developers, in particular, discuss trade-offs instead of asserting best practices with snappy soundbites. Because every situation is unique. That’s what makes the job so difficult, yet so rewarding.

But single-minded adherents are terrified of tapping into their creative side, unless they’re heavily constrained. They’re terrified of improvising and not knowing exactly what to do in every situation. But most of all, they’re terrified that there isn’t an objective way to get a correct answer to their problems.

“Everything has to adhere to a principle, or else how do I know I’m doing the right thing?”

Experience, intuition, and being comfortable with uncertainty.

P.S

I’m not saying there aren’t any best practices at all. What I’m saying is that the more high-level a practice is, the more leeway you need to exercise with how rigidly you adhere to it. Simply because that’s the nature of creative fields. If I had to roughly rank the following kinds of software development guidelines from 1 to 5, where 1 is the most low-level, and 5 is most high-level, it would look like this:

  • (1) Code style
  • (2) Module dependency structure
  • (3) Development paradigm (TDD, OOP)
  • (4) System architecture practices
  • (5) Work-planning (Agile, Scrum, etc)

Notice how the higher you go, the less agreement you’ll find on what best practices are. Here’s another ranking with creative writing:

  • (1) Spelling & grammar
  • (2) Sentence variation and prose
  • (3) Chapter/Scene structure
  • (4) Plot beats
  • (5) Overall story and theme

Obviously unlike stories, software is never finished, but you get my point. The higher-level you go, the more complicating factors and cases there are to consider. There’s actually a similar kind of debate in the writing world, where you have “plotters” who plan their story before writing, and “pantsers” who go by the seat of their pants and explore their plot as they write, but that’s a whole other topic.

Anyway, thanks for reading.

http://theaxolot.wordpress.com/?p=2302
Extensions
When it’s Okay to Interrupt People in a Debate
UncategorizedfallaciespoliticsdebatecommunicationArgumentation
Oh imagine a land It’s a faraway place Where the good faith debates are held Where the winner is the best Where each point gets addressed And the bad faith ones are quelled Where the activists from the left And the hosts from the center And the pundits that lean to the right You are […]
Show full content

Oh imagine a land

It’s a faraway place

Where the good faith debates are held

Where the winner is the best

Where each point gets addressed

And the bad faith ones are quelled

Where the activists from the left

And the hosts from the center

And the pundits that lean to the right

You are lost in the sauce

As your rudeness comes across

But sometimes you need it in a fight

I’ve been listening to some debates recently, most notably involving Jordan Peterson and Destiny (God, do those two annoy me).

But anyway, what I’ve noticed is that there’s always an exchange where someone is making their point, then the opposition interrupts to make a counterpoint, and then they get interrupted with the phrase:

“Can you let me finish my point, please? Don’t interrupt me.”

When I was naive about debates, like most of you, I used to believe that interruption was a sign of low confidence and bad faith. That it automatically made your side weaker because you had to resort to cheap tactics to get your point across.

But as I listened more and more, I often found myself wanting a speaker to be interrupted, even though there isn’t a debater out there that’s more good faith than I am. So what’s going on?

“Bad faith debate” is when a debater prioritizes something other than the truth when everyone listening is expecting the truth. It’s an act of deception with ulterior motives that doesn’t respect the implicit social agreements that everyone goes into the debate with.

Most people don’t go into a debate open to change their minds, but most do genuinely believe what they say, even if they’re utterly wrong. So if someone is being genuine, it doesn’t make sense to me to call them “bad faith” unless there’s strong evidence for it, like:

  • Being a known grifter
  • Being a troll
  • Being a contrarian
  • Clearly lacking in maturity and self-control
  • Having a financial incentive to profess said beliefs
  • Actions showing they want to “win” debates (i.e showing up the opposition with “gotchas”) over reaching the truth
  • And so on

So why would someone who’s likely arguing in good faith interrupt their opposition? Popular answers include “They want to control the discussion”, or “They feel cornered because they know they’re wrong.”

But those are just thought-terminating cliches that give people a false moral high ground. What happens when your idol does the same thing? Are you just gonna excuse them every time?

One very good reason to interrupt the opposition is if they’re monopolizing the conversation. After the opening statements and all that jazz, the conversation flows freely from there.

Ideally, every side should get equal participation. But whether intentionally or not, one side can often get carried away with their ramblings and pontificating. Not every debate has a moderator, so the only option is to interrupt when you feel you’re not being heard. It’s rude, but necessary.

The second reason is similar, and it’s in response to a “gish-gallop”, which is when you overwhelm the opposition with so many arguments that they don’t have the time to respond to them all. The “gish-gallop” is an effective rhetorical tactic because it makes you come across as assertive and gives the opposition no time to respond to all of them.

Destiny often interrupts this tactic when it’s used against him, and rightly so. But then he proceeds to do it to them and then some because of how quickly he talks. Annoying.

The third is to defend against rabbit holes made from strawmen. Often, a debater will start a counterpoint to a strawman of your position, and continue to elaborate on it. That’s a huge waste of time and could severely damage perception of your positions. There’s no choice but to interrupt.

Finally, you might have to interrupt to clarify some terms before the discussion continues. It doesn’t make sense to wait until the end of a long, multifaceted argument to clarify a definition, because otherwise you won’t be able to follow along and will have to ask for a repetition.

But don’t do this if your opponent is making a simple point with a simple sentence. And definitely don’t pull a JP and be like, “What do you mean by ‘simple’?”.

That’s pretty much it for this piece. Adherence to rigid rules with little consideration for context is the kind of thing that keeps people stuck at the “amateur” level, and that goes for any field.

http://theaxolot.wordpress.com/?p=2282
Extensions
Caleb Tries Legacy Coding (Part 6: The Finale)
Uncategorizedfictionprogrammingsatireshort-storiesshort-storysoftware-developmenttechnologywriting
Caleb racked his brain trying to come up with the right dynamic programming recurrence for this medium LeetCode problem. He’d never used DP at work. In fact, he didn’t believe anyone had. So why did companies keep asking them? It wasn’t like it mattered anyway, since he couldn’t even get past the resume screening. It […]
Show full content

Caleb racked his brain trying to come up with the right dynamic programming recurrence for this medium LeetCode problem. He’d never used DP at work. In fact, he didn’t believe anyone had. So why did companies keep asking them? It wasn’t like it mattered anyway, since he couldn’t even get past the resume screening.

It had been a month since he’d gotten fired.

Out of the blue, his manager had sent him the dreaded e-mail. And not only had he CC’ed HR, he’d also CC’ed Greg and Todd to rub salt in the gaping wound. The e-mail cited his “lackluster performance”, even though he’d been doing more work than Greg and Todd combined. Before he’d had a chance to process his unemployment, he’d been promptly deactivated, just like Steve.

He scoured the internet to get in touch with Steve. He tried LinkedIn, Twitter, Github, and many other social media sites, but to no avail. At this point, he wasn’t even sure that Steve was his real name.

But Caleb himself was an open book online. If Steve had wanted to reach out, he would’ve already. But it had been radio silence for the past month.

He gave up and looked at others’ solutions on the problem, only to find they had nothing to do with DP, but rather used an obscure algorithm he’d never heard of. Any other solution would exceed the time limit. That was enough LeetCode for today.

He was about to get up for a drink when an e-mail notification popped up from his sidebar. Its title simply read, “Please read…”, and it was from his old company. He wanted to dismiss it as a taunt from his manager, but self-assurance wasn’t his strong suit.

He opened it up and read the first sentence,

Dear Caleb. We are pleased to offer you the title of “Senior Developer” at our company.

Huh!? This had to be a mistake. Maybe they’d meant to send it someone else. But his name was in the writing. He continued reading,

We’ve recently had a leadership change, and after review, we’ve reassessed the value of your contributions during your time with us. Your experience with us makes you a highly valuable candidate for this position.

Where was this coming from all of a sudden? And what did a “leadership change” have to do with it?

Attached is the contract for this position. Please review it and sign if you’d like your old role back.

Regards,

Hiring Team

He hurriedly opened the contract, though still expecting it to be some sick joke.

But it wasn’t. It was for real. And it offered forty-thousand more dollars a year than his previous salary.

It was almost too good to be true, and he had to restrain himself from signing immediately.

He grabbed a drink from his mini-fridge to ease his mind and processed the news. On paper, the offer was everything he’d wanted in his career. But his idiot, former manager could very well still ruin everything. But if the company really needed him, his manager couldn’t do squat, right? Was the pay increase really worth losing his peace of mind?

His phone vibrated in his pocket, and he pulled it out to see a call from an unknown number. He never ignored spam calls because they’d fill up his voicemail anyway.

He swiped and put the phone to his ear. “Hello,” he said casually.

“Hello, Caleb.” The voice was unmistakable.

Caleb sat up in his seat. “S-Steve!? H-How did you get my number!?”

“Ahh…irrelevant inquiries. You have not changed.”

Clearly, Steve’s attitude hadn’t changed either. Caleb unloaded, “Where the hell have you been!? You promise me job security, then ghost me after we both get fired!? Why would you do that!?”

“As expected, your emotion clouds your intellect. But rest assured, all has transpired according to my plan. You have your offer.”

“How do you know about that?” He set his drink aside.

“Recall that I have ties with upper management.”

“Right, but what’s going on?”

“Upper management had grown callous. Rather than seek to expand, they desired to reduce costs. You were targeted because your manager feared your contributions would outshine his, and so he spoke little of them. I, however, resigned of my own volition.”

“Wait. Why would you do that? I thought you liked the power and security you had.”

“‘Had?‘ You misunderstand. I am more powerful now than ever.”

Caleb sat in silence, completely confused.

“Allow me to elucidate. I could not prevent your imminent termination. Therefore, I enacted a plan wherein the company would be compelled to recognize your value afterwards. After your termination and my corresponding resignation, I impersonated a customer. I sent the company a scathing complaint, claiming I’d encountered a bug regarding the feature you coded.”

“What was the bug?”

“There were not any. I fabricated the complaint. Regardless, no other developer had the time nor the patience to investigate your code for the source of the bug.”

“I see.”

Steve continued, “The company pleaded for my return, as they believed I was to be their savior regarding this complaint. I accepted, but on conditions with which they happily obliged. Firstly, I demanded a consultancy role instead of a full-time position, with a sizable salary increase and working hours of my choosing.”

“And that worked?” Caleb never ceased to be amazed by Steve’s manipulation skills.

“Very much so.”

“What else did you ask for?”

“I asked that they re-acquire your employment, citing your expertise in the area and your overall skills.”

“Wow…okay. I don’t what to say.” Caleb pondered a bit. “But I don’t know how I feel about working with my old manager again.” He took a sip of his drink.

“I understand. That is why I demanded your manager be terminated and replaced.”

Caleb choked on his drink and spit it out. “No way! Even you don’t have that much power!”

“You continue to underestimate me. I informed them that he had been downplaying your work, among a myriad of other fraudulent activities that the company believed had been concealed from the public.”

“So you blackmailed them?”

‘Blackmail’ is an unpleasant word. I consider it, ‘Restoring Karmic Equilibrium.’ Regardless, it is done.”

He was really gone; the man who’d made Caleb’s job into a living nightmare. That was what the hiring team had meant by a “leadership change.”

“Thank you, Steve. I mean it.”

“You can thank me by signing that offer and enacting my teachings.”

“But why didn’t you tell me all this? You had a whole month.”

“You are not experienced enough to deserve such comfort. You must build resilience.”

Caleb smiled and electronically signed the contract with a few mouse clicks. All he had to do was reply to the e-mail.

“There’s just one thing I don’t understand.”

“What is it?”

“Why would you risk your job for me?”

And for the first time since they’d met, Steve laughed. It was a deep, hefty chuckle that unsettled Caleb to his core. It lasted ten awkward seconds before stopping abruptly.

“Were you under the impression that I only worked one job?'”

THE END

P.S

Thanks for reading, everyone! I didn’t expect this story to last six chapters, but I felt I had to keep it going until it could end naturally and satisfyingly. I’m not a professional writer, especially in the satire genre, but I’m proud of my work here.

http://theaxolot.wordpress.com/?p=2034
Extensions
Caleb Tries Legacy Coding (Part 5)
Uncategorizedfictionprogrammingsatireshort-storysoftware-developmenttechnologywriting
Caleb published his pull request. It had been Steve’s idea to wait until the last day of the sprint before doing so. As he put it, “Such pressure causes standards to buckle.” In truth, he had finished a week in advance thanks to the AI he used to write most of the code. “So yeah…I […]
Show full content

Caleb published his pull request. It had been Steve’s idea to wait until the last day of the sprint before doing so. As he put it, “Such pressure causes standards to buckle.” In truth, he had finished a week in advance thanks to the AI he used to write most of the code.

“So yeah…I just published my pull request for the new API. If you guys can take a look, that’d be great,” Caleb gave his standup.

Neither Greg nor Todd said a word, their silence evidence for their cluelessness and disinterest; exactly what Caleb wanted.

His manager spoke up, “Excellent! Let’s get this approved before lunch and deploy it immediately.”

He silently stared at his PR for the next two hours, not daring to be caught off guard doing anything else. Steve had coached him on how to respond to PR comments in a way that made him appear understanding while denying the need for significant changes. He also taught him how to artificially increase the size of the diff.

The first of which was through unnecessary formatting changes. A simple keyboard shortcut painted half the project’s files with red and green, including the files he’d modified. It was a clever tactic to disguise logic changes behind formatting changes.

The second was the unnecessary renaming of methods, especially public ones. Such changes would span many of the calling files, diluting the focus of the PR. And since his names were better, no one could really object.

Finally, his magnum opus, writing overly complicated logic. A skilled and motivated engineer would spot the complexities and propose simplifications. Caleb’s goal was to covertly gaslight his coworkers into believing that his changes were as simple as they could be, and that their lack of understanding wasn’t a reflection of the code’s quality.

A comment came from Todd, “Do we need so many empty lines between these two methods?” Ah! One of his “bikeshedding” ducks. An intentional flaw that wasn’t significant enough to warrant skepticism of the overall project, but obvious enough to make a rookie feel like they were meaningfully contributing.

Caleb quickly removed the extra lines and replied, “Good catch! Don’t know how that slipped in. It’s fixed now.” A few minutes later, Todd gave his PR a thumbs-up. It was obvious he’d barely understood anything, but he was pacified, and that was all that mattered.

Fifteen minutes to go. The ball was in Greg’s court now. Greg wouldn’t ask any significant changes with such little time remaining. All Caleb had to do was not invalidate his feedback.

His comment came, “I don’t understand what this regular expression is supposed to do. Could we implement this functionality without one?”

The moment of truth. Obviously, Greg had a point, but he either wasn’t sure, or he didn’t care enough to assert it. He had no idea what the database looked like. No one did, except for Caleb and Steve. So Caleb replied, “It searches for a user’s permission record in the database. It would be very cumbersome to implement it without a regex.”

That should placate him, Caleb thought. But another message came, “Really? That hasn’t usually been my experience.”

Now for his finishing move. “Trust me, it’s simpler this way. If you have a free hour, I can explain it to you.” Even if there was enough time, Caleb knew Greg wouldn’t go out of his way to learn the “depth” behind his implementation. Not when he had a life outside of work.

“Alright.” Another thumbs-up appeared right on time. He wasted not a single second and pushed his PR. The deployment pipeline picked up the changes, built the repo with no issues, and deployed it for public use. The tension dissipated from his body, and he laughed a hearty laugh of relief.

The next four hours were the most serene of his life, like that feeling when you return from a long trip and see your home in a new light. Steve had saved his job. And though Caleb had been forced to abandon his programming principles, he didn’t feel the slightest twinge of guilt anymore. The promise of power was all he needed.

His team hadn’t received any complaints yet about the new API. Feeling certain his changes were satisfactory, at least for the time being, he reached out to Steve to brag about his success. But where he would’ve normally seen a profile picture with “Steve Miller” written above, he saw a black void and the words, “Unknown User.”

He’d been deactivated.

Not a second later, Caleb received an e-mail with the subject line, “Regarding your employment…”

TO BE CONTINUED

http://theaxolot.wordpress.com/?p=1906
Extensions