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