Facebook Pixel

DRY, KISS, YAGNI & Law of Demeter

SOLID governs the shape of classes and their relationships. Four other heuristics govern the moment-to-moment decisions inside and around those classes: DRY, KISS, YAGNI, and the Law of Demeter. Each is a rule of thumb rather than a formal principle, addressing a category of decisions that accumulates into maintenance cost when handled poorly: knowledge duplication, unnecessary complexity, speculative features, and excessive coupling to internal structure.

DRY — Don't Repeat Yourself

DRY, coined by Andrew Hunt and David Thomas in The Pragmatic Programmer, states that every piece of knowledge should have a single, authoritative representation in the system. The failure mode is duplicated intent, not only repeated lines. When the same business rule appears in two places, a change to the rule requires a change in two places, and the second place will eventually be missed.

A checkout system validating order amounts is a clear example. If the rule "total must be greater than zero and less than $10,000" is written twice — once in the REST controller and once in the service layer — the controller gets updated when the cap rises to $50,000 but the service is forgotten. Orders that slip past the controller will then pass through the service unchecked.

1# Bad: the same validation rule duplicated in two places
2class OrderController:
3    def create_order(self, payload: dict) -> None:
4        if payload["total"] <= 0 or payload["total"] > 10_000:
5            raise ValueError("Invalid order total")
6        # ... hand off to service
7
8class OrderService:
9    def place_order(self, order: "Order") -> None:
10        if order.total <= 0 or order.total > 10_000:  # same rule, same numbers
11            raise ValueError("Invalid order total")
12        # ... save order
1# Good: the rule lives in one place; both caller and service delegate to it
2MAX_ORDER_TOTAL = 10_000
3
4def validate_order_total(total: float) -> None:
5    if total <= 0 or total > MAX_ORDER_TOTAL:
6        raise ValueError(f"Order total must be between 0 and {MAX_ORDER_TOTAL}")
7
8class OrderController:
9    def create_order(self, payload: dict) -> None:
10        validate_order_total(payload["total"])
11        # ...
12
13class OrderService:
14    def place_order(self, order: "Order") -> None:
15        validate_order_total(order.total)
16        # ...

The diagram below shows how a duplicated rule creates two update sites; the DRY fix collapses them to one authoritative location both callers delegate to.

DRY is sometimes misread as "never write similar-looking code twice." That is too strong. Two functions that happen to share a loop structure but encode different business rules are not duplicates — unifying them creates a false dependency. The question is whether two pieces of code change for the same reason. If yes, they should be one piece.

KISS — Keep It Simple

KISS (Keep It Simple, Stupid) is a reminder to prefer the simplest correct implementation. Code is read far more often than it is written, and a solution that requires careful parsing to understand adds cognitive overhead each time it is touched. The simpler version is generally preferable unless a measurable constraint — performance, conciseness of a well-understood idiom — justifies the added complexity.

A common violation is the over-clever one-liner. The following function builds a summary of product inventory. The first version is compact; the second is readable.

1# Bad: a one-liner that requires careful mental parsing
2from typing import Sequence
3
4def low_stock_skus(inventory: dict[str, int], threshold: int) -> list[str]:
5    return sorted(k for k, v in inventory.items() if v < threshold)
6
7# Good: same logic, explicit loop — intent is clear at a glance
8def low_stock_skus(inventory: dict[str, int], threshold: int) -> list[str]:
9    low_stock = []
10    for sku, quantity in inventory.items():
11        if quantity < threshold:
12            low_stock.append(sku)
13    low_stock.sort()
14    return low_stock

Neither version is incorrect. The explicit loop makes the intent readable to any reader without requiring familiarity with the functional idiom in use.

YAGNI — You Aren't Gonna Need It

YAGNI, from Extreme Programming, is a constraint on speculation. Do not build features, configuration options, or extension points that no current requirement demands. Speculative code also carries an ongoing cost: maintaining, documenting, and working around it, often for a requirement that never actually arrives.

A common form is building a pluggable architecture before any second plugin exists. An inventory alert system with a hard requirement of "email alerts" does not need a registry of notification channels configured via YAML.

1# Bad: pluggable channel registry nobody asked for yet
2from abc import ABC, abstractmethod
3
4class AlertChannel(ABC):
5    @abstractmethod
6    def send(self, message: str) -> None: ...
7
8class AlertService:
9    def __init__(self) -> None:
10        self._channels: list[AlertChannel] = []
11
12    def register(self, channel: AlertChannel) -> None:
13        self._channels.append(channel)
14
15    def alert(self, message: str) -> None:
16        for channel in self._channels:
17            channel.send(message)
18
19# Good: the requirement is email alerts — write exactly that
20import smtplib
21
22class AlertService:
23    def __init__(self, recipient: str) -> None:
24        self._recipient = recipient
25
26    def alert(self, message: str) -> None:
27        smtp = smtplib.SMTP("mail.internal")
28        smtp.sendmail("alerts@inventory.com", self._recipient, message)

YAGNI is not a ban on abstractions. It is a ban on premature abstractions. When a second notification channel is actually requested, refactor then — with concrete requirements in hand, the right interface boundary is obvious. Invented too early, it is usually wrong.

Law of Demeter

The Law of Demeter (also called the Principle of Least Knowledge) states that a method should only call methods on its direct collaborators — not on objects returned by those collaborators. The violation is the "train wreck": a chain of method calls that navigates deep into an object graph. Each dot in the chain is a dependency on internal structure.

A shipping module that calculates tax based on the destination city is a common example.

1# Bad: ShippingService reaches three levels deep into Order's object graph
2class ShippingService:
3    def calculate_tax(self, order: "Order") -> float:
4        city = order.get_customer().get_address().get_city()
5        return TAX_RATES.get(city, 0.0) * order.total
6
7# Good: Order tells the service what it needs — the service doesn't navigate
8class Order:
9    def destination_city(self) -> str:
10        return self._customer.address.city
11
12class ShippingService:
13    def calculate_tax(self, order: "Order") -> float:
14        city = order.destination_city()
15        return TAX_RATES.get(city, 0.0) * order.total

The diagram below contrasts the two designs. The train wreck on the left forces ShippingService to know about three layers of internal structure; the Law of Demeter design on the right lets it talk only to its direct neighbor, Order.

The train wreck order.getCustomer().getAddress().getCity() means ShippingService knows that orders have customers, customers have addresses, and addresses have cities. If Address is refactored to split city and postal code into a Location sub-object, ShippingService breaks even though it has nothing to do with customer data. The good version moves that knowledge inside Order, which already knows about its customer structure. ShippingService asks for the one fact it needs, and Order decides how to find it.

Tell, don't ask is the principle behind Law of Demeter: ask an object for its result, not for the intermediate objects you need to compute it yourself.

One point worth clarifying: LoD constrains object navigation — reaching through one object to obtain a third, structurally unrelated object. It is not a "one-dot rule" that prohibits method chaining in general. Builder chains (new Pizza.Builder().size("large").crust("thin").build()) and stream or LINQ pipelines (order.items().stream().filter(...).map(...)) are not violations because each call returns the same conceptual object or stream — you are not tunneling into the internal structure of a collaborator. The concern is coupling to ownership structure (customer has an address that has a city), not the depth of fluent expression on a single type.

How the four work together

DRY eliminates redundant knowledge. KISS keeps each piece of knowledge as readable as possible. YAGNI keeps the total surface area of knowledge small. The Law of Demeter keeps each piece of code from knowing too much about the structure of its neighbors. Applied together, the codebase stays navigable as it grows: each change touches one place, each function does one thing legibly, and each class is as coupled to its collaborators as the problem requires — no more.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro