Skip to main content

Core Software Engineering Principles

These fundamental principles help developers write maintainable, scalable, and robust code. Let's explore each with Python examples.

DRY (Don't Repeat Yourself)

Avoid code duplication by abstracting common functionality into reusable components.

Why it matters

  • Reduces maintenance overhead
  • Minimizes bugs from inconsistent changes
  • Improves readability

Bad Example (Violates DRY)

def calculate_area_square(side):
return side * side

def calculate_volume_cube(side):
return side * side * side # Duplicated multiplication logic

Good Example (DRY)

def square(x):
return x * x

def calculate_area_square(side):
return square(side)

def calculate_volume_cube(side):
return square(side) * side # Reuses square() function

When to break DRY

  • When abstraction would make code harder to understand
  • For truly one-off operations

KISS (Keep It Simple, Stupid)

Favor simple, straightforward solutions over complex ones.

Why it matters

  • Easier to maintain and debug
  • Faster to implement
  • More accessible to other developers

Complex Solution (Violates KISS)

def is_even(num):
return True if num % 2 == 0 else False # Unnecessary ternary

Simple Solution (KISS)

def is_even(num):
return num % 2 == 0 # Direct boolean expression

KISS in Practice

  • Avoid premature optimization
  • Prefer clear variable names over clever one-liners
  • Limit abstraction layers to what's necessary

YAGNI (You Aren't Gonna Need It)

Don't implement functionality until it's actually needed.

Why it matters

  • Reduces wasted development time
  • Keeps codebase lean
  • Avoids maintaining unused code

Bad Example (Violates YAGNI)

class User:
def __init__(self, name):
self.name = name
self.age = None # Added "just in case" we need it later
self.address = None # Not currently used anywhere

Good Example (YAGNI)

class User:
def __init__(self, name):
self.name = name # Only what we need now

YAGNI Exception

When building extensible frameworks where future needs are well-understood

Law of Demeter (Principle of Least Knowledge)

An object should only talk to its immediate neighbors (friends), not strangers.

Why it matters

  • Reduces coupling between classes
  • Makes code more modular
  • Easier to refactor

Violation Example

# Bad: Digging deep into object relationships
user.get_account().get_balance().format_currency()

Proper Implementation

# Good: Delegate to immediate neighbors
user.format_account_balance()

# Inside User class:
def format_account_balance(self):
return self.account.format_balance() # Only talks to direct dependency

Demeter Exception

  • Builder pattern method chaining
  • Fluent interfaces

Composition Over Inheritance

Favor building functionality by combining simple objects rather than inheriting from base classes.

Why it matters

  • More flexible than deep inheritance hierarchies
  • Avoids fragile base class problem
  • Easier to change behavior at runtime

Inheritance Example (Problematic)

class Vehicle:
def move(self):
pass

class Car(Vehicle):
def move(self):
print("Driving")

class FlyingCar(Car): # What if we need different movement?
def move(self):
print("Flying") # Overriding parent behavior

Composition Solution

class Engine:
def move(self):
print("Driving")

class JetEngine:
def move(self):
print("Flying")

class Vehicle:
def __init__(self, engine):
self.engine = engine

def move(self):
self.engine.move()

# Usage
car = Vehicle(Engine())
car.move() # Driving

flying_car = Vehicle(JetEngine())
flying_car.move() # Flying

When to Use Inheritance

  • For true "is-a" relationships
  • When you need method overriding
  • For interface implementation

Separation of Concerns (SoC)

Divide a program into distinct sections, each addressing a separate concern.

Why it matters

  • Easier maintenance
  • Better code organization
  • Enables parallel development

Violation Example (Monolithic Function)

def process_order(order):
# Validate order
if not order.items:
raise ValueError("Empty order")

# Calculate total
total = sum(item.price * item.quantity for item in order.items)

# Charge payment
payment_service.charge(order.customer, total)

# Send confirmation
email_service.send_confirmation(order.customer)

# Update inventory
inventory_manager.update_stock(order.items)

Proper Separation

class OrderValidator:
def validate(self, order):
if not order.items:
raise ValueError("Empty order")

class OrderCalculator:
def get_total(self, order):
return sum(item.price * item.quantity for item in order.items)

class OrderProcessor:
def __init__(self):
self.validator = OrderValidator()
self.calculator = OrderCalculator()

def process(self, order):
self.validator.validate(order)
total = self.calculator.get_total(order)
payment_service.charge(order.customer, total)
email_service.send_confirmation(order.customer)
inventory_manager.update_stock(order.items)

SoC Benefits

  • Each class has single responsibility
  • Easier to test components in isolation
  • Changes to one concern don't affect others

Summary Table

PrincipleKey BenefitPython ExampleWhen to Violate
DRYReduce duplicationReusable functionsWhen abstraction hurts clarity
KISSMaintainabilitySimple boolean returnsPerformance-critical sections
YAGNIAvoid wasteMinimal class attributesFramework development
Law of DemeterLoose couplingMethod delegationFluent interfaces
CompositionFlexibilityEngine injectionTrue "is-a" relationships
SoCModularitySeparate validator classTrivial applications