Loading...
Loading...
Use when designing modules and components requiring Composition Over Inheritance, Law of Demeter, Tell Don't Ask, and Encapsulation principles that transcend programming paradigms.
npx skill4agent add thebushidocollective/han structural-design-principlesuseimportalias|># GOOD - Composition with pipes
def process_payment(order) do
order
|> validate_items()
|> calculate_total()
|> apply_discounts()
|> charge_payment()
|> send_receipt()
end
# GOOD - Compose behaviors
defmodule User do
use YourApp.Model
use Ecto.Schema
import Ecto.Changeset
# Composes functionality from multiple modules
end
# GOOD - Struct embedding
defmodule Address do
embedded_schema do
field :street, :string
field :city, :string
end
end
defmodule User do
schema "users" do
embeds_one :address, Address
end
end// BAD - Inheritance hierarchy
class Animal {
move() { }
}
class FlyingAnimal extends Animal {
fly() { }
}
class SwimmingAnimal extends Animal {
swim() { }
}
class Duck extends FlyingAnimal {
// Problem: Can't also inherit from SwimmingAnimal
// Forced to duplicate swim() logic
}
// GOOD - Composition with interfaces
interface Movable {
move(): void;
}
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
class Duck implements Movable, Flyable, Swimmable {
move() { this.walk(); }
fly() { /* flying logic */ }
swim() { /* swimming logic */ }
private walk() { /* walking logic */ }
}// GOOD - React composition (pattern)
// Instead of class inheritance, compose components
// Base behaviors as hooks
function useTaskData(gigId: string) {
// Data fetching logic
}
function useTaskActions(gig: Task) {
// Action handlers
}
function useTaskValidation(gig: Task) {
// Validation logic
}
// Compose in component
function TaskDetails({ gigId }: Props) {
const gig = useTaskData(gigId);
const actions = useTaskActions(gig);
const validation = useTaskValidation(gig);
// Combines all behaviors through composition
return <View>{/* render */}</View>;
}engagement.worker.address.city# BAD - Violates Law of Demeter (train wreck)
def get_worker_city(engagement) do
engagement.worker.address.city
# Knows too much about internal structure
end
# What if worker doesn't have address?
# What if address structure changes?
# Tightly coupled to implementation
# GOOD - Delegate to the module that owns the data type
defmodule Assignment do
def worker_city(%{worker: worker}) do
User.city(worker)
end
end
defmodule User do
def city(%{address: address}) do
Address.city(address)
end
def city(_), do: nil
end
# Now can call: Assignment.worker_city(engagement)
# Each module is responsible for its own data
# Coupling minimized to immediate collaborators# BAD - Reaching through associations
def total_gig_hours(user_id) do
user = Repo.get!(User, user_id)
assignments = user.assignments
Enum.reduce(assignments, 0, fn eng, acc ->
acc + eng.shift.hours # Reaching through
end)
end
# GOOD - Delegate to the domain
def total_gig_hours(user_id) do
user = Repo.get!(User, user_id)
User.total_hours(user)
end
defmodule User do
def total_hours(%{assignments: assignments}) do
Enum.reduce(assignments, 0, fn eng, acc ->
acc + Assignment.hours(eng)
end)
end
end
defmodule Assignment do
def hours(%{shift: shift}), do: WorkPeriod.hours(shift)
end// BAD - Chain of doom
function displayUserLocation(engagement: Assignment) {
const location = engagement.worker.profile.address.city;
// Knows about 4 levels of object structure!
return `Location: ${location}`;
}
// GOOD - Each object provides what you need
function displayUserLocation(engagement: Assignment) {
const location = engagement.getUserCity();
return `Location: ${location}`;
}
class Assignment {
getUserCity(): string {
return this.worker.getCity();
}
}
class User {
getCity(): string {
return this.address.city;
}
}// BAD - GraphQL fragments violating Law of Demeter
const fragment = graphql`
fragment TaskCard_gig on Task {
id
requester {
organization {
billing {
paymentMethod {
last4
}
}
}
}
}
`;
// TaskCard shouldn't know about payment details!
// GOOD - Only query what you need
const fragment = graphql`
fragment TaskCard_gig on Task {
id
title
payRate
location {
city
state
}
}
`;
// TaskCard only knows about gig display dataobject.method()object.property.property.method()# This is okay - designed for chaining
User.changeset(%{})
|> cast(attrs, [:email])
|> validate_required([:email])
|> unique_constraint(:email)
# This is okay - Ecto.Query builder pattern
from(u in User)
|> where([u], u.active == true)
|> join(:inner, [u], p in assoc(u, :profile))
|> select([u, p], {u, p})
# These patterns are explicitly designed for method chaining
# Each function returns a chainable structure.worker.address.city# BAD - Asking for data and making decisions
def process_engagement(engagement) do
if engagement.status == "pending" and engagement.worker_id != nil do
attrs = %{status: "confirmed", confirmed_at: DateTime.utc_now()}
Repo.update(Assignment.changeset(engagement, attrs))
end
end
# We're asking about the engagement's state and deciding what to do
# GOOD - Delegate to the module that owns the Assignment struct
def process_engagement(engagement) do
Assignment.confirm(engagement)
end
defmodule Assignment do
def confirm(%{status: "pending", worker_id: worker_id} = engagement)
when not is_nil(worker_id) do
changeset = change(engagement, %{
status: "confirmed",
confirmed_at: DateTime.utc_now()
})
Repo.update(changeset)
end
def confirm(engagement), do: {:error, :invalid_state}
# Assignment module knows its own business rules
# Callers just "tell" it to confirm, don't "ask" about status
end# BAD - Asking and deciding
def charge_gig(gig) do
if gig.payment_type == "per_hour" do
rate = gig.hourly_rate
hours = gig.total_hours
Money.multiply(rate, hours)
else
gig.fixed_amount
end
end
# GOOD - Delegate to the module that owns the Task struct
def charge_gig(gig) do
Task.total_charge(gig)
end
defmodule Task do
def total_charge(%{payment_type: "per_hour", hourly_rate: rate,
total_hours: hours}) do
Money.multiply(rate, hours)
end
def total_charge(%{payment_type: "fixed", fixed_amount: amount}) do
amount
end
# Business logic lives with the data in the owning module
end// BAD - Asking for data
function renderTaskStatus(gig: Task) {
let statusText: string;
let statusColor: string;
if (gig.status === 'active' && gig.workerCount > 0) {
statusText = 'In Progress';
statusColor = 'green';
} else if (gig.status === 'active') {
statusText = 'Waiting for Users';
statusColor = 'yellow';
} else {
statusText = 'Completed';
statusColor = 'gray';
}
return <Badge text={statusText} color={statusColor} />;
}
// GOOD - Tell the gig to provide display info
function renderTaskStatus(gig: Task) {
const { text, color } = gig.getStatusDisplay();
return <Badge text={text} color={color} />;
}
class Task {
getStatusDisplay(): { text: string; color: string } {
if (this.status === 'active' && this.workerCount > 0) {
return { text: 'In Progress', color: 'green' };
} else if (this.status === 'active') {
return { text: 'Waiting for Users', color: 'yellow' };
}
return { text: 'Completed', color: 'gray' };
}
}# Command tells the system what to do
%CreateTask{requester_id: id, title: "Landscaping"}
|> CreateTaskHandler.handle()
# Handler tells domain objects to execute
# Not: Handler asks domain for data and decidesdefp@opaque# BAD - Exposing internals
defmodule PaymentProcessor do
defstruct [:stripe_client, :api_key, :retry_count]
def process(processor, amount) do
# Callers can access processor.stripe_client directly
# Breaks if we change internal implementation
end
end
# GOOD - Encapsulate internals
defmodule PaymentProcessor do
@type t :: %__MODULE__{
stripe_client: term(),
api_key: String.t(),
retry_count: integer()
}
@enforce_keys [:stripe_client, :api_key]
defstruct [:stripe_client, :api_key, retry_count: 3]
# Public API
def new(api_key), do: %__MODULE__{
stripe_client: Stripe.Client.new(api_key),
api_key: api_key
}
def process(%__MODULE__{} = processor, amount) do
# Internal implementation hidden
do_process(processor, amount)
end
# Private implementation
defp do_process(processor, amount) do
# Can change internals without affecting callers
end
end# BAD - Ecto schema with map fields (no structure)
defmodule Task do
schema "tasks" do
field :data, :map # Anything goes!
end
end
# GOOD - Explicit fields (encapsulation via type system)
defmodule Task do
schema "tasks" do
field :title, :string
field :description, :string
field :pay_rate, Money.Ecto.Composite.Type
field :status, Ecto.Enum, values: [:draft, :published, :active, :completed]
end
# Changesets enforce valid transitions
def publish_changeset(gig) do
gig
|> change(%{status: :published})
|> validate_required([:title, :description, :pay_rate])
end
# Can't publish without required fields (encapsulated business rule)
end// BAD - Public mutable state
class ShoppingCart {
public items: Item[] = []; // Anyone can modify directly!
public total(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
const cart = new ShoppingCart();
cart.items.push(invalidItem); // Bypasses validation!
// GOOD - Encapsulated state
class ShoppingCart {
private items: Item[] = []; // Hidden implementation
public addItem(item: Item): void {
if (this.isValid(item)) {
this.items.push(item);
} else {
throw new Error('Invalid item');
}
}
public removeItem(itemId: string): void {
this.items = this.items.filter(item => item.id !== itemId);
}
public getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
public getItemCount(): number {
return this.items.length;
}
private isValid(item: Item): boolean {
return item.price > 0 && item.quantity > 0;
}
// All state changes go through controlled methods
}// GOOD - React component encapsulation
function TaskCard({ gigRef }: Props) {
// Encapsulate internal state
const [expanded, setExpanded] = useState(false);
const gig = useFragment(fragment, gigRef);
// Private helpers
const handleToggle = () => setExpanded(!expanded);
// Public interface is just the props
return (
<Pressable onPress={handleToggle}>
{/* Internal implementation */}
</Pressable>
);
}
// Parent components don't know about 'expanded' state
// Clean interface: pass gig data, get rendered cardprivateprotecteddefpa.b.c.d()user.profile.address.cityif type == 'x'solid-principlesecto-patternscqrs-patternatomic-design-patternsimplicity-principles