Architectural Coding and System Design
Advanced Paradigm Patterns
Beyond Inheritance
In object-oriented programming, inheritance is often one of the first major concepts we learn. The idea of an is-a relationship is intuitive: a Dog is an Animal. While useful, relying too heavily on deep inheritance hierarchies can create rigid and fragile code. When a base class changes, all its descendants can break in unexpected ways. This is often called the "fragile base class" problem.
A more flexible approach is to think in terms of what an object has or what it does, rather than what it is. This is the core idea behind the principle of composition over inheritance.
Instead of inheriting abilities from a parent class, you compose objects from smaller, independent components or behaviors. Think of it like building with LEGO bricks. With inheritance, you might start with a pre-built car chassis and are limited by its design. With composition, you have a box of wheels, engines, and blocks that you can combine in countless ways to build a car, a plane, or something entirely new.
// Behavior interfaces
interface Walker {
walk(): void;
}
interface Swimmer {
swim(): void;
}
// Concrete implementations of behaviors
const walkingBehavior: Walker = {
walk: () => console.log("Taking a stroll..."),
};
const swimmingBehavior: Swimmer = {
swim: () => console.log("Paddling around..."),
};
// Create objects by composing behaviors
function createDog() {
return {
...walkingBehavior,
...swimmingBehavior,
bark: () => console.log("Woof!"),
};
}
function createCat() {
return {
...walkingBehavior,
meow: () => console.log("Meow."),
};
}
const myDog = createDog();
myDog.walk(); // Taking a stroll...
myDog.swim(); // Paddling around...
const myCat = createCat();
myCat.walk(); // Taking a stroll...
In this TypeScript example, we define Walker and Swimmer behaviors. We then create Dog and Cat objects by mixing in the behaviors they need. This approach is far more modular. If we wanted to create a Duck, we could easily give it both walking and swimming behaviors without wrestling with a complex class hierarchy.
Modeling State Safely
Another area where traditional patterns can fall short is in modeling complex states. Imagine fetching data from an API. The request could be loading, it could have succeeded, or it could have failed. A common but error-prone approach is to use boolean flags like isLoading, isError, and hasData.
This can lead to impossible situations. What if isLoading and isError are both true? Or hasData is true, but the data is actually null? We can eliminate these invalid states using a more precise modeling technique.
In TypeScript, provide a robust way to model a state that can be one of several distinct shapes. Each shape in the union has a common property, the “discriminant,” which is typically a string literal. This allows the TypeScript compiler to help you handle every possible state correctly.
type NetworkState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleState<T>(state: NetworkState<T>): void {
switch (state.status) {
case 'loading':
console.log('Fetching data...');
break;
case 'success':
console.log('Data fetched:', state.data);
break;
case 'error':
console.error('Error:', state.error.message);
break;
}
}
Here, NetworkState can only be one of the three specified shapes. It's impossible for status to be 'success' without a data property. When you use a switch statement, TypeScript will warn you if you forget to handle a case, making your code safer.
Defining Behavior with Protocols
Python takes a different approach to defining shared behavior. While it supports traditional class inheritance, it also embraces a concept called "duck typing": if it walks like a duck and quacks like a duck, it must be a duck. This means Python often cares more about an object's methods and properties than its specific class type. formalize this idea.
A protocol defines a set of methods and attributes that a class should have. Any class that implements this structure implicitly conforms to the protocol, without needing to inherit from it. This is known as and is a powerful alternative to inheritance for defining interfaces.
from typing import Protocol
class Serializable(Protocol):
def to_json(self) -> str:
...
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def to_json(self) -> str:
return f'{{"name": "{self.name}", "email": "{self.email}"}}'
class BlogPost:
def __init__(self, title: str):
self.title = title
def to_json(self) -> str:
return f'{{"title": "{self.title}"}}'
# This function expects any object that conforms to the Serializable protocol.
def print_json(obj: Serializable):
print(obj.to_json())
user = User("Alice", "alice@example.com")
blog = BlogPost("My First Post")
print_json(user) # Works because User has a to_json method
print_json(blog) # Works because BlogPost has a to_json method
Notice that neither User nor BlogPost explicitly inherits from Serializable. They just need to have the right method signature. This decouples our code, allowing components to interact through shared behaviors without being tied to a common base class.
Isolating Side Effects
As systems grow, managing complexity becomes the primary challenge. One powerful architectural pattern for this is the "Functional Core, Imperative Shell." The idea is to separate your program into two parts:
- The Functional Core: This is where the pure business logic lives. Functions here are deterministic: given the same input, they always produce the same output. They don't have side effects like network requests, database writes, or logging.
- The Imperative Shell: This thin outer layer interacts with the outside world. It handles user input, makes API calls, and writes to the console. It calls functions in the core to perform logic and then uses the results to produce side effects.
This separation makes the core logic incredibly easy to test, since you don't need to mock databases or network calls. You can verify its correctness with simple unit tests. The messy, unpredictable parts of the system are pushed to the edges, where they are easier to manage.
A key practice that supports this architecture is immutability. When data structures are immutable, they cannot be changed after they are created. Instead of modifying an object, you create a new one with the updated values. This prevents side effects where one part of the program unexpectedly changes data that another part depends on, simplifying state management and making the functional core truly pure.
Let's check your understanding of these advanced patterns.
What is the "fragile base class" problem primarily associated with?
In Python, a class can conform to a Protocol without explicitly inheriting from it. This concept is known as ____.
Mastering these patterns moves you beyond simply writing code that works to engineering software that is robust, flexible, and maintainable. They provide a powerful toolkit for structuring complex applications in any language.