Recent Posts (page 17 / 67)

by Leon Rosenshein

Kinds of Costs

WIP

Costs are costs, right? You pays your money and you takes your choice. Simple, right? Not really. There are all kinds of costs. Up front, back end, hidden, and delayed for example. All of those are relevant to development. Besides those, there are two major ones to be aware of because they often turn into biases

  • Opportunity Cost: The time & money you spend on X means you can’t do Y. So choose wisely.
  • Sunk Cost: The time money already spent on X is gone. Is Y cheaper than continuing X?

Opportunity cost bias often gets expressed as one of 2 ways. The first is fear of making the wrong choice. Instead of thinking about what you should do and devoting enough resources on X to actually get it done, choose not to decide and peanut butter the resources around. You spread them thinly over multiple choices, and instead of finishing something you end up not finishing anything. Just like you want to [minimize work in progress] /posts/2021/09/09/). You have the time to get things done. You want to focus your resources.

The second is assuming facts not in evidence. Things like assuming you know exactly what the result of the different choices would be. That choice X will have a 100% return, while choice Y will have a 1000% return. It might happen, but what are the odds of each. You can’t use the potential upside without considering the probabilities. Not just ROI, but things like time, or how others will perceive it. You might assume X will take 2 days and Y 2 weeks, but that doesn’t mean it will happen. You might like bright colors and choose to paint a salmon-pink accent wall in your kitchen, but that doesn’t mean your prospective buyers will like it. There’s a reason why your real estate agent stages your house without doing anything unusual.

Sunk cost bias also come from fear. Fear that you’ve wasted your time/money/resources. No one wants to waste things, so you get that feeling you have that you’re almost done. That solving one more little problem will make everything work. After all, you’ve already invested (sunk) a bunch of resources into it. A little more to make it work makes sense. And if it’s a little more then you’re right. But if you let the fear (or pride of ownership) take control then you underestimate the cost of continuing and overestimate the cost of trying something new. Otherwise known as throwing good money after bad.

The thing is, both costs are real. You need to take them into account. The trick is to make a good decision. One based on facts, not biases.

by Leon Rosenshein

Cupid

In honor of Valentine’s Day, a new acronym. SOLID has been around for a while, and there are lots of good things about SOLID. Applying the SOLID principles (almost) always leads to better code. But, as @tastapod wrote, SOLID is about principles, and with principles, like rules, you’re either in compliance with the principles or not. Now you just have to decide how compliant you want to be. The answer to that of course, is it depends. Strict, absolute compliance can lead to over-abstracted code that might work well but is effectively opaque to someone new to the codebase. Ignoring SOLID can lead to a big ball of mud.

Enter CUPID. A set of properties for your code, not a set of rules to follow. The more your code exhibits these properties, the more readable, the more understandable, to another reader it is. So what is CUPID?

Composable: Each function/library/package has clear boundaries and works well with other functions/libraries/packages

Unix Philosophy: Do one thing and do it well

Predictable: Does what you expect. The principle of least surprise

Idiomatic: Feels natural. Don’t be too clever

Domain based: The solution domain models the problem domain in language and structure

CUPID is a set of properties that your code should have, not a set of principles to write it by. And the choice of those 5 properties was driven by a set of meta-properties. Each of the CUPID properties needed to be practical, human, and layered.

It’s those 3 meta-properties that really differentiate CUPID from SOLID. Instead of being about the code itself, CUPID is about how the code feels to the people who have to write, read, and maintain it. But don’t take my word for it. Read what Dan has to say about it when he introduced it.

Because, as Martin Fowler said,

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

by Leon Rosenshein

More Perspective

​​I’ve talked about perspective before. Your point of view is a huge component of how you look at a situation. Let’s say you’re a fish. And let’s say you needed to navigate from Portland, Maine to Portland, Oregon. A map would be a good thing. It would really help you get from Portland to Portland. But for a fish the map wouldn’t look like most of the maps you’re used to seeing. It would look a lot more like this:

A map of the land as seen by a fish. Lots of water and only coastlines

Lots of ocean and a bit of coastline. (Yes, there’s too much land showing in Africa and major rivers aren’t shown, but you get the idea). Because that’s what’s important to fish. They don’t know, or care, what’s happening in Kansas or the central Asia. As far as they’re concerned those places might as well be on the moon. They have an impact on the world, but when getting from Portland to Portland they just don’t matter.

Working on software can be a lot like that. As the developer you care a lot about the internals. How the pieces fit together. How they communicate. How a change in one place impacts another. Your customers, on the other hand, couldn’t care less. If it’s an application they care about how many button clicks it takes them to do their task. If it’s a library they care about how good the documentation is and how consistent the API is. In general, customers want you to do the thing that makes their lives easier. The only time they care about making your life easier is when it helps them get what they want.

There are lots of ways that this impacts us, but one of the most important is how we communicate with our customers. When communicating with customers the key is to speak in their language. To talk about the benefits they get. How their life will be easier/better. Not that run time is ½ or that memory required is lower, but that they’ll be able to run twice as many things in the same time and it will cost them less. That’s customer benefit. Not “We’ve re-built the system as a set of microservices to reduce hard coupling”, but “Our new microservice architecture lets makes the system more resilient to network and hardware issues and lets us respond to customer needs faster”. Which one do you think customers want more?

So next time you need to explain something to someone and you draw a picture, make sure it’s from their perspective.

by Leon Rosenshein

Beware Of Just

This comes up in lots of different contexts

The problems with saying 'Just'

but it boils down to “I know you’re busy, but here’s a simple and obvious (according to my cocktail party understanding) solution to the problem I’m having. I need it working tomorrow.” 

After all, how hard can it be to write a script to automate it? You already know how to do it. Response times for your web query are slow. Just add a cache. It’s simple.

It’s simple on the surface, but when you dig into it there are a lot of details. Lots of fiddly bits that need to be just right, or you end up in worse shape than you were before. The first implementation of the script hides error messages, or worse, ignores errors and continues on, doing the wrong thing. The cache you just turned on gives inconsistent results depending on when you ask and where you ask from. All of these things are correctable, and when you get it working you are in better shape, but it’s not as simple as “Just cache it”.

I can be guilty of this myself. I used to say to people, "While you’re in there, can you make this other change?" While it might be the right time, it’s never as simple as that. Neither I, nor anyone else should expect it to be, yet we keep doing it.

So Just Remember, when you hear someone say “Just ….”, it’s never as easy as it sounds.

by Leon Rosenshein

Perspective 2

Consider this

 

"Everyone has this problem so clearly we can tolerate it"

Vs

"Everyone has this problem so clearly we should solve it"


Start with the exact same situation and come up with the opposite result. How is that possible? Is one right and one wrong?

As usual, my answer is, It Depends. It depends on what the problem is. It depends on the impact of the problem. It depends on your goal. It depends on your capabilities. It depends on what you mean by tolerate and solve. But most of all, it depends on the perspective you look at it from.

The problem, whatever it is, is how the current state differs from a desired state. Your perspective is what determines how you see it. Do you see the problem as an impediment to everyone’s progress that you should just minimize and move on, or do you see it as an opportunity to improve things? An opportunity to give yourself a competitive advantage by removing the impediment.

Solving the problem is better. So, we should be solving every problem we see, immediately, right? Unfortunately, no. You can’t solve every problem. You really can’t solve every problem at once. And partial solutions are often worse than the original problem, so you don’t want to start something you’re not going to finish. Which means you need to take the time to understand the problem and make an informed decision about which problem(s) to solve and which to tolerate (for now).

Which brings us back to what it depends on. Does the problem take 5 minutes, once a week, or 10 seconds 25 times a day for 100 people? That’s big difference to its impact. Will it take a few hours to fix or a few months? The ratio of impact to cost is a big input. Is the problem (and its impact) on the critical path to your strategic goals? If it is, the importance goes up. Do you have capability to solve the problem? The knowledge, the time, and the resources? Since those aren’t unlimited, what isn’t going to get done if you solve this problem? What’s the impact of not solving that problem? The costs and benefits aren’t always obvious, and the trade-offs are even less obvious.

The key though is the perspective you see it from. Is it an impediment or an opportunity?

by Leon Rosenshein

Write Your Unit Tests FIRST

There are at least a couple of ways to take that. There’s the temporal definition of first, meaning you should write your unit tests before you write the code that makes the tests pass. That’s pretty straightforward. Write a set of tests that verify the functionality you’re about to build. Write some more tests that verify failure modes. When all your tests pass you’re (almost) done. That’s classic test driven development, or as I like to call it, design by example. It’s not perfect, but when you’re done you have working code, proof that it works, and some examples of how it’s expected to be used. If that’s the only way you interpret “Write your unit tests FIRST” you’ve gotten a lot of benefit.

But you can get even more by writing your test FIRST. That’s FIRST, the acronym. It’s not a TLA, but it’s close.

  • Fast : Unit tests need to be fast. You’re going to be running them hundreds (thousands) of times. Even more if you’re using Continuous Testing. The longer they take the more intrusive they are, or worse, they start to get skipped, which eliminates much of the benefit
  • Isolated : Each test should stand alone. It shouldn’t require any other test runs first, or set the stage for the next test. Ideally all the tests can run in parallel with no change to the results. They should be fully isolated from external systems, and if some kind of resource is needed it should be created by the test before it runs. Finally, the test should test one and only one thing. Test the happy path. Test a specific failure mode. Test initialization. In the limit there’s only one assert on the result. Not always possible, but a good goal.
  • Repeatable : Each test should give the exact same result every time. Partly that comes from being isolated, but also includes handling of threads, race conditions, setup/cleanup, and (pseudo) random numbers. All flaky tests do is waste time. First you waste time figuring out it’s a flaky test, then you’re wasting time running tests that don’t mean anything.
  • Self-Verifying : Unit tests either pass or fail. And they pass or fail as a group. They can’t require someone to look at the results and decide if it’s pass or fail. If there’s something important to the result in the console or log output then capture it and look at it. No asking a human to decide. There’s no such thing as needing 80% or better passing to call it success. No, "test A or B can fail, but not both". If that’s really the way it is then write your tests such that a single test passes or fails based on that criteria.
  • Timely : Tests are written in a timely manner. There’s really two benefits here. You get the design by example from above, but you also make sure your code is testable. Sure, you can always refactor the code to make it testable, but why? If you do it that way the first time you don’t have to go back and redo it. There’s going to be a lot of re-work as you learn more anyway, so why add to the burden?

So before you go diving into some code, either to fix a bug or implement something, write the tests FIRST.

by Leon Rosenshein

Sooner, not Faster

What’s better, faster or sooner? More to the point, what’s the difference between them, and why should you care?

Let’s start with some definitions:

Fast:

  1. Moving or able to move quickly

    She’s a very fast runner.
    a fast car

  2. Happening quickly : taking a short amount of time

    a fast race
    We’re off to a fast start.

  3. Operating quickly a fast computer

Soon:

  1. Without undue time lapse : BEFORE LONG

    soon after sunrise

  2. In a prompt manner : SPEEDILY

    as soon as possible the sooner the better
    no sooner said than done

At its core, fast is about speed. It’s about movement. Speed is distance divided by time, so more speed is either more distance or less time.

Sooner, on the other hand, is temporal. It’s about time. It’s about the time between starting and finishing. The only way to be sooner is to take less time.

So faster is a function of distance and time, while sooner is just a function of time. Sooner is faster, but faster isn’t always sooner. That’s the key difference, and why you should care.

To know which is better though, first you need to define better. In this situation I define better as maximizing value added. That means value as defined by the customer/user, not as defined by the set of features provided. Which means getting feedback and acting on it.

Combined, that means sooner is better than faster. Sooner means whatever value is being delivered gets delivered before it otherwise would have. Getting feedback earlier and being able to act on it means you deliver what the customer wants/needs before you might have.

And that’s where being agile comes in. Working software. Customer collaboration, Responding to change. That helps you deliver sooner, not faster.

by Leon Rosenshein

Decisions 2 (if ... elseif ... elseif ... elseif ...)

Speaking of the difference between a choice and a decision, the number of choices you have to pick from can deeply influence the decisions you make about the code you write. Because the number of choices you have can sometimes drive the implementation of the decision of how to do something far from where you actually make the decision.

You could do it up-front, with a set of micro-decisions, but you better know what the options are and make all the right choices then. If you do it that way though you don’t have much flexibility, because by the time the decision starts to impact what happens you’re far from the decision and it’s often too late to adjust. A better choice is to push the decision up the stack and abstract away the implementation down the stack to where more (context and detail) is known and the impact is clearer.

Another way to look at it is to think about some simple non-branching code. It proceeds from statement to statement doing whatever it’s told. All the choices were made by the compiler a long time ago. At runtime there are no decisions to be made.

Now add a simple if statement. Everything runs along smoothly, but somewhere in the middle there’s a decision and something different happens. If there’s only one of them then it’s a special case and (almost) everyone knows about it and expects it.

Of course, as time goes on more special cases show up, and the simple if turns into an if ... elseif ... elseif .... They look clean. But pretty soon new requirements show up. Special case upon special case, with sub-cases and caveats, to the point where you build a truth table, hope you get it right, then implement it. Eventually no-one is sure what happens, or why. Look at this code I wrote a year ago. It got so bad I put in a comment explaining what was supposed to happen because it wasn’t obvious to me, let alone someone else, from reading the code.

var filteredEntries []userstate.StateEntry
initialTargetLen := len(targets)
for _, entry := range entries {
    req := entry.Value.(authutils.GetCredentialsRequest)
    // This is getting complicated, so breaking it down. We EXCLUDE a profile if ALL of these conditions are false
    // 1) The user specified a list of profiles and the current profile is one of them
    // 2) The user did not specify a list of profiles AND specified the --all flag OR the --aws flag
    // 3) The user did not specify a list of profiles, the current profile is not an admin profile,
    //       and the user did NOT specify the --admin flag
    // 4) The user did not specify a list of profiles, the current profile is an admin profile,
    //        and the user specified the --admin flag

    if !((initialTargetLen > 0 && targets[entry.Key]) ||
        (initialTargetLen == 0 && (refreshParams.Flags.All || refreshParams.Flags.Aws)) ||
        (initialTargetLen == 0 && roleIsAdmin(req.AWSRole) == refreshParams.Flags.Admin)) {
            continue
        }
    filteredEntries = append(filteredEntries, entry)
    delete(targets, entry.Key)
}

Try adding a new parameter that interacts with that logic. Combinatorial explosion. And it’s possible (likely) that some of the flags are mutually exclusive. It needs to be fixed, and one day I’ll get to it. Probably the next time I need to add something. At that point I’ll figure out how to extract the logic in some flag/parameter abstract way.

The problem is that the decision (what the user wants to do) is made far away temporally from when the code needs to decide. And on a completely different timeline new requirements get added. And this code makes that hard.

Making that kind of change easy is an architectural decision. There are lots of ways to do it. Everything from changing the requirements to objects, abstractions, inheritance, and interfaces. And they should all be considered before a decision is made.

by Leon Rosenshein

Is That a Choice or a Decision

I’ve talked about evaluating your decision based on what you knew at the time instead of the outcome of the decision before. That’s as true now as it ever was. Unfortunately, that’s a retrospective approach to understanding the quality of your decision. It doesn’t help much while you’re trying to make the decision.

As much as it would make my job easier, I don’t have a time machine that will let me evaluate my decisions based on what I find out later. But there are some rules of thumb that can help to ensure that you’re making a good decision, in the moment, regardless of what ends up happening.

The simplest, and most important, is to make sure that you’re actually making an informed decision. Sticking with the status quo, or taking the first option you come up with isn’t a decision. It’s a choice, but it’s not a decision. To make a decision you need to make a choice between options. If there’s only one option it’s rule by fiat or declaration.

That means you need at least two options to make a decision. Do or Do Not (there is no try). Do A or B. At least you’re making a choice now. Arguably a decision. But it’s a very limited one. Those kinds of choices are often polar opposites, and the world is rarely that simple.

Which means you need to avoid the tyranny of or. Sure, some choices are truly binary, but most aren’t. It’s not this or that, but more often, a little bit of this and some more of that. It’s conjunction junction. Hooking up tasks and options and choices into a coherent whole that solves the problem. Shades of grey.

So next time you need to make a decision, remember, if you have:

  • 1 choice you’re making a decision by fiat or declaration
  • 2 choices you’re probably choosing between polar opposites. Avoid the tyranny of OR
  • 3+ choices and you’re making a more informed decision based on knowledge of the situation.
by Leon Rosenshein

Flow

You’ve probably heard of flow. That perfect balance of concentration, energy, attention, and understanding that leads to progress. You’ve probably been in a flow state a few times. It seems magical. You get more done than you expected and when you’re done the result feels right. Everything seems to click and the code is something you’re proud of.

And it’s by no means restricted to development, or just creative endeavors, but also in repetitive, assembly line, types as well. One of my early jobs was at a concrete block factory. Everything from running the molding machine to loading trucks, and pretty much everything in between. 

There was a lot of automation, but we also did a bunch of semi-custom work that added manual steps to the process. Particularly the part of putting the finished blocks into cubes on pallets so they could be stored in the yard or loaded for shipment. Some of the patterns we needed to use so the blocks interlocked and didn’t fall off when the truck went around a corner were complicated.

Even worse, some of them were rough split, so we had to keep pairs together so they fit well. Some days running the cuber was a struggle. Getting things in just the right place as more blocks kept flying down the line was hard. It felt like Lucy in the candy factory. Nothing seemed to go smoothly and eventually we’d either have to get more people at the cuber or hit the pause button and get caught up. Other days it, with the exact same kind of blocks, and the machine running at the exact same speed, things would just flow.

More relevant to us though is flow as a developer. I think this is an urban legend, but the XKCD page on the Ballmer Peak is real. And there’s certainly truth to the idea of reducing inhibitions to increase creativity. Pretty sure blood-alcohol-content isn’t the best way to get that increase, and stress and anxiety make things worse. So what does go into enabling flow?

The most common way of looking at it is to compare how your skill level at a task compares to the challenge of the task. Seems reasonable. With the caveat that your skill level at a task has an inverse, but non-linear, relationship to how much of a challenge it is. For any given task, the better at it you are, the less of a challenge it will be. The current model looks a lot like

Complex definition of flow

To me that just looks like a color wheel though. It’s visually round, which seems odd, and oddly specific. The sharp boundaries between states just doesn’t feel right.

What does feel right, to me at least, is a much older version of that image.

Simple explanation of flow

It’s the same author, but 40+ years older. It could probably do with a little more detail on the areas outside of flow, but for visualizing the difference between flow and not flow, I think it captures the real driver, which is a proper balance of skill and challenge. It might be hard to do, but you know you can do it, or it might be easy, and you can do it very easily. Either way, there’s a balance.

That’s why we like flow. Not because the task is easy, but because the mental state is easy. There’s no anxiety, boredom, worry, or apathy to deal with. All of your mental energy can go into accomplishing the task. Which also explains why so much gets done.

But here’s the catch. Flow is productive, but it’s often not growth. At all but the highest challenge/skill levels, you’re doing things that you know how to do. There will be incremental improvement, but to really make a step change to your skill/ability, you need to challenge yourself to go beyond your skill level. And there’s no flow there.