Card 07/ 08

ExerciseDifficulty: Intermediate1 min

Practice: Adding a Shipping Carrier Without Editing the Calculator

Two carriers in an if chain, and a third arriving next sprint.

Making code open for a new case starts with finding the code that every new case forces you to edit. This exercise gives you a shipping calculator with two carriers. A third, Evri, at 2.90 plus 1.00 per kilogram, arrives next sprint, and more are expected. Restructure the code so that adding Evri is a new class and nothing else.

py
def shipping_cost(carrier, weight_kg):
    if carrier == "royal-mail":
        return 3.50 + 0.80 * weight_kg
    elif carrier == "dpd":
        return 6.00 + 0.50 * weight_kg
    raise ValueError(f"unknown carrier {carrier}")


for carrier in ["royal-mail", "dpd"]:
    print(f"{carrier}: {shipping_cost(carrier, 4):.2f}")
OutputRan with python:3.12-slim (Python 3.12.13)
royal-mail: 6.70
dpd: 8.00
Every carrier is a branch in shipping_cost.

Answer two questions before you write anything. Which lines would you edit to add Evri today? What method would every carrier need, so that shipping_cost never has to ask which carrier it has?

Show one good answer

Today, adding Evri means editing shipping_cost itself: a new elif before the raise. The varying part is the cost calculation, so each carrier becomes a class with a cost(weight_kg) method, and shipping_cost calls that method on whatever carrier it is given.

py
class RoyalMail:
    def cost(self, weight_kg):
        return 3.50 + 0.80 * weight_kg


class Dpd:
    def cost(self, weight_kg):
        return 6.00 + 0.50 * weight_kg


class Evri:
    def cost(self, weight_kg):
        return 2.90 + 1.00 * weight_kg


def shipping_cost(carrier, weight_kg):
    return carrier.cost(weight_kg)


for carrier in [RoyalMail(), Dpd(), Evri()]:
    print(f"{type(carrier).__name__}: {shipping_cost(carrier, 4):.2f}")
OutputRan with python:3.12-slim (Python 3.12.13)
RoyalMail: 6.70
Dpd: 8.00
Evri: 6.90
Evri is a new class, and shipping_cost is unchanged by it.

Royal Mail and DPD print the same costs as before, 6.70 and 8.00 for 4 kilograms, which is the check that the refactoring changed nothing. Evri's 6.90 comes from a class added afterwards. Because Python is duck typed, shipping_cost needs no declared interface to work; in Java or TypeScript you would add a Carrier interface with cost.