Hourglass logic: time's up!

There's one pattern that I've seen time and time again in codebases that have evolved over time, and I often seek to refactor it out to avoid its hidden pitfalls. If I told you that I called this "hourglass logic", would you be able to spot it in the following code?

function beginCheckout(checkoutProvider) {
  switch (checkoutProvider) {
    case 'stripe':
      // Suppose this code initiates a Stripe checkout
      stripe.doSomething();
      maybeSomeOtherLogic();
      break;

    case 'paypal':
      // Suppose this code initiates a PayPal checkout
      paypal.doSomethingElse();
      perhapsMoreLogicHereToo();
      break;

    case 'ethereum':
      // Suppose this code initiates a checkout using Ethereum
      metamask.doSomethingOther();
      finalizeTransaction();
      break;

    default:
      break;
  }
}

function CheckoutPage() {
  return (
    <div>
      <button
        onClick={() => beginCheckout('stripe')}
      >Check out with credit card</button>
      <button
        onClick={() => beginCheckout('paypal')}
      >Check out with PayPal</button>
      <button
        onClick={() => beginCheckout('ethereum')}
      >Check out with Ethereum</button>
    </div>
  );
}

This is a simple example, but intentionally so, because this problem can hide very easily in a large codebase -- especially when beginCheckout and CheckoutPage might live in different files. The "hourglass" comes from the fact that you have three buttons, both of which narrow control into the same beginCheckout function, which only differentiates between the buttons and then "fans out" again into three separate branches.

Based on my experience reading other people's code, here are a few reasons I find this problematic:

What's my favorite solution for this? Refactor the switch cases into separate functions, delete the hourglass function, and refer directly to the separate functions from the UI:

function beginStripeCheckout() {
  // Suppose this code initiates a Stripe checkout
  stripe.doSomething();
  maybeSomeOtherLogic();
}

function beginPayPalCheckout() {
  // Suppose this code initiates a PayPal checkout
  paypal.doSomethingElse();
  perhapsMoreLogicHereToo();
}

function beginEthereumCheckout() {
  // Suppose this code initiates a checkout using Ethereum
  metamask.doSomethingOther();
  finalizeTransaction();
}

function CheckoutPage() {
  return (
    <div>
      <button
        onClick={() => beginStripeCheckout()}
      >Check out with credit card</button>
      <button
        onClick={() => beginPayPalCheckout()}
      >Check out with PayPal</button>
      <button
        onClick={() => beginEthereumCheckout()}
      >Check out with Ethereum</button>
    </div>
  );
}

This has the added benefits of being shorter and easier to test!

Now, there may well be valid reasons for writing hourglass code. Perhaps data is being passed over the network or stored in a database, and needs to be processed later. As with any tool, use this particular refactoring skill wisely, and only when applicable. As the saying goes, "when all you have is a hammer, everything looks like a nail".