OOP Basics - Senior Level¶
SOLID Principles¶
SOLID is five design principles that make software more maintainable, flexible, and understandable.
S — Single Responsibility Principle (SRP)¶
A class should have only one reason to change.
Bad: One class does everything
// BAD: UserManager handles users, email, and persistence
public class UserManager {
public void createUser(String name, String email) { /* ... */ }
public void sendWelcomeEmail(String email) { /* ... */ }
public void saveToDatabase(Object user) { /* ... */ }
public String generateReport() { /* ... */ }
}
Good: Each class has one responsibility
// GOOD: Separate responsibilities
public class UserService {
private final UserRepository repository;
private final EmailService emailService;
public UserService(UserRepository repository, EmailService emailService) {
this.repository = repository;
this.emailService = emailService;
}
public User createUser(String name, String email) {
User user = new User(name, email);
repository.save(user);
emailService.sendWelcome(user);
return user;
}
}
public class UserRepository {
public void save(User user) { /* database logic */ }
public User findById(long id) { /* ... */ }
}
public class EmailService {
public void sendWelcome(User user) { /* email logic */ }
}
public class UserReportGenerator {
public String generate(List<User> users) { /* report logic */ }
}
Go:
// Each struct has one job
type UserService struct {
repo UserRepository
email EmailService
}
type UserRepository interface {
Save(user *User) error
FindByID(id int64) (*User, error)
}
type EmailService interface {
SendWelcome(user *User) error
}
func (s *UserService) CreateUser(name, email string) (*User, error) {
user := &User{Name: name, Email: email}
if err := s.repo.Save(user); err != nil {
return nil, err
}
if err := s.email.SendWelcome(user); err != nil {
return nil, err
}
return user, nil
}
Python:
class UserService:
def __init__(self, repo: "UserRepository", email: "EmailService"):
self.repo = repo
self.email = email
def create_user(self, name: str, email: str) -> "User":
user = User(name=name, email=email)
self.repo.save(user)
self.email.send_welcome(user)
return user
class UserRepository:
def save(self, user: "User") -> None: ...
def find_by_id(self, user_id: int) -> "User": ...
class EmailService:
def send_welcome(self, user: "User") -> None: ...
O — Open/Closed Principle (OCP)¶
Software entities should be open for extension but closed for modification.
Add new behavior without modifying existing code.
Bad: Modifying existing code for each new type
# BAD: Must modify calculate_area every time a new shape is added
def calculate_area(shape_type: str, **kwargs) -> float:
if shape_type == "circle":
return 3.14159 * kwargs["radius"] ** 2
elif shape_type == "rectangle":
return kwargs["width"] * kwargs["height"]
elif shape_type == "triangle": # Had to modify!
return 0.5 * kwargs["base"] * kwargs["height"]
else:
raise ValueError(f"Unknown shape: {shape_type}")
Good: Extend through new classes
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
# Adding a new shape — NO modification to existing code
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
# Works with any Shape without modification
def total_area(shapes: list[Shape]) -> float:
return sum(s.area() for s in shapes)
Go:
type Shape interface {
Area() float64
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
// New shape — no existing code modified
type Triangle struct{ Base, Height float64 }
func (t Triangle) Area() float64 { return 0.5 * t.Base * t.Height }
func TotalArea(shapes []Shape) float64 {
total := 0.0
for _, s := range shapes {
total += s.Area()
}
return total
}
L — Liskov Substitution Principle (LSP)¶
Subtypes must be substitutable for their base types without altering program correctness.
Bad: Square violating Rectangle's contract
// BAD: Classic violation
class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
class Square extends Rectangle {
@Override
public void setWidth(int w) {
this.width = w;
this.height = w; // Surprise! Changes height too
}
@Override
public void setHeight(int h) {
this.width = h;
this.height = h;
}
}
// This code breaks with Square
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
assert r.area() == 50; // FAILS for Square! area = 100
}
Good: Separate types with a shared interface
// GOOD: Don't use inheritance when contracts differ
interface Shape {
int area();
}
class Rectangle implements Shape {
private final int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override public int area() { return width * height; }
}
class Square implements Shape {
private final int side;
public Square(int side) { this.side = side; }
@Override public int area() { return side * side; }
}
I — Interface Segregation Principle (ISP)¶
No client should be forced to depend on methods it does not use.
Bad: Fat interface
// BAD: Huge interface forces all implementors to implement everything
type Worker interface {
Work()
Eat()
Sleep()
Code()
ManagePeople()
DesignSystem()
}
Good: Small, focused interfaces
// GOOD: Small, composable interfaces
type Worker interface {
Work()
}
type Eater interface {
Eat()
}
type Coder interface {
Code()
}
type Manager interface {
ManagePeople()
}
// Compose interfaces as needed
type Developer interface {
Worker
Coder
}
type TechLead interface {
Worker
Coder
Manager
}
type Robot struct{}
func (r Robot) Work() { fmt.Println("Working...") }
func (r Robot) Code() { fmt.Println("Coding...") }
// Robot doesn't need to implement Eat or Sleep!
Python:
from abc import ABC, abstractmethod
# BAD
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass # Robot can't eat!
@abstractmethod
def sleep(self): pass # Robot can't sleep!
# GOOD: Segregated interfaces
class Workable(ABC):
@abstractmethod
def work(self): pass
class Feedable(ABC):
@abstractmethod
def eat(self): pass
class Human(Workable, Feedable):
def work(self): print("Working...")
def eat(self): print("Eating...")
class Robot(Workable):
def work(self): print("Working tirelessly...")
# No need to implement eat!
D — Dependency Inversion Principle (DIP)¶
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad: Direct dependency on concrete classes
# BAD: High-level NotificationService depends on low-level SMTPMailer
class SMTPMailer:
def send(self, to: str, subject: str, body: str) -> None:
print(f"Sending email via SMTP to {to}")
class NotificationService:
def __init__(self):
self.mailer = SMTPMailer() # HARD dependency!
def notify(self, user_email: str, message: str) -> None:
self.mailer.send(user_email, "Notification", message)
# Can't test without SMTP server!
# Can't switch to SendGrid without modifying this class!
Good: Depend on abstractions
# GOOD: Depend on abstraction
from abc import ABC, abstractmethod
class MessageSender(ABC):
@abstractmethod
def send(self, to: str, subject: str, body: str) -> None:
pass
class SMTPMailer(MessageSender):
def send(self, to: str, subject: str, body: str) -> None:
print(f"Sending email via SMTP to {to}")
class SendGridMailer(MessageSender):
def send(self, to: str, subject: str, body: str) -> None:
print(f"Sending email via SendGrid to {to}")
class SlackNotifier(MessageSender):
def send(self, to: str, subject: str, body: str) -> None:
print(f"Sending Slack message to {to}")
class NotificationService:
def __init__(self, sender: MessageSender): # Inject abstraction
self.sender = sender
def notify(self, user_email: str, message: str) -> None:
self.sender.send(user_email, "Notification", message)
# Easy to swap implementations
service = NotificationService(SMTPMailer())
service = NotificationService(SendGridMailer())
# Easy to test with a mock
class MockSender(MessageSender):
def __init__(self):
self.sent = []
def send(self, to, subject, body):
self.sent.append((to, subject, body))
mock = MockSender()
service = NotificationService(mock)
service.notify("test@example.com", "Hello")
assert len(mock.sent) == 1
Go (DIP is natural):
type MessageSender interface {
Send(to, subject, body string) error
}
type NotificationService struct {
sender MessageSender
}
func NewNotificationService(sender MessageSender) *NotificationService {
return &NotificationService{sender: sender}
}
func (n *NotificationService) Notify(email, message string) error {
return n.sender.Send(email, "Notification", message)
}
// Implementations
type SMTPMailer struct{}
func (s SMTPMailer) Send(to, subject, body string) error {
fmt.Printf("SMTP → %s: %s\n", to, subject)
return nil
}
type MockSender struct {
Sent []string
}
func (m *MockSender) Send(to, subject, body string) error {
m.Sent = append(m.Sent, to)
return nil
}
Design Patterns¶
Strategy Pattern¶
Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime.
Go:
// Strategy interface
type SortStrategy interface {
Sort(data []int) []int
}
type BubbleSort struct{}
func (b BubbleSort) Sort(data []int) []int {
n := len(data)
result := make([]int, n)
copy(result, data)
for i := 0; i < n-1; i++ {
for j := 0; j < n-i-1; j++ {
if result[j] > result[j+1] {
result[j], result[j+1] = result[j+1], result[j]
}
}
}
return result
}
type QuickSort struct{}
func (q QuickSort) Sort(data []int) []int {
result := make([]int, len(data))
copy(result, data)
sort.Ints(result)
return result
}
// Context
type Sorter struct {
strategy SortStrategy
}
func NewSorter(strategy SortStrategy) *Sorter {
return &Sorter{strategy: strategy}
}
func (s *Sorter) SetStrategy(strategy SortStrategy) {
s.strategy = strategy
}
func (s *Sorter) Sort(data []int) []int {
return s.strategy.Sort(data)
}
func main() {
data := []int{5, 2, 8, 1, 9, 3}
sorter := NewSorter(BubbleSort{})
fmt.Println(sorter.Sort(data)) // [1 2 3 5 8 9]
sorter.SetStrategy(QuickSort{})
fmt.Println(sorter.Sort(data)) // [1 2 3 5 8 9]
}
Java:
public interface SortStrategy {
int[] sort(int[] data);
}
public class BubbleSort implements SortStrategy {
@Override
public int[] sort(int[] data) {
int[] result = data.clone();
int n = result.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (result[j] > result[j + 1]) {
int temp = result[j];
result[j] = result[j + 1];
result[j + 1] = temp;
}
}
}
return result;
}
}
public class QuickSort implements SortStrategy {
@Override
public int[] sort(int[] data) {
int[] result = data.clone();
Arrays.sort(result);
return result;
}
}
public class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}
public int[] sort(int[] data) {
return strategy.sort(data);
}
}
Python:
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list[int]) -> list[int]:
pass
class BubbleSort(SortStrategy):
def sort(self, data: list[int]) -> list[int]:
result = data.copy()
n = len(result)
for i in range(n - 1):
for j in range(n - i - 1):
if result[j] > result[j + 1]:
result[j], result[j + 1] = result[j + 1], result[j]
return result
class QuickSort(SortStrategy):
def sort(self, data: list[int]) -> list[int]:
return sorted(data)
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def sort(self, data: list[int]) -> list[int]:
return self._strategy.sort(data)
# Python also supports strategy via functions (first-class functions)
def sort_with(data: list[int], key_func=None) -> list[int]:
return sorted(data, key=key_func)
Observer Pattern¶
Defines a one-to-many dependency. When one object changes state, all dependents are notified.
Go:
type Event struct {
Type string
Data any
}
type Observer interface {
OnEvent(event Event)
}
type EventBus struct {
observers map[string][]Observer
}
func NewEventBus() *EventBus {
return &EventBus{observers: make(map[string][]Observer)}
}
func (eb *EventBus) Subscribe(eventType string, observer Observer) {
eb.observers[eventType] = append(eb.observers[eventType], observer)
}
func (eb *EventBus) Publish(event Event) {
for _, observer := range eb.observers[event.Type] {
observer.OnEvent(event)
}
}
// Concrete observers
type LoggingObserver struct{}
func (l LoggingObserver) OnEvent(event Event) {
fmt.Printf("[LOG] Event: %s, Data: %v\n", event.Type, event.Data)
}
type MetricsObserver struct {
eventCount int
}
func (m *MetricsObserver) OnEvent(event Event) {
m.eventCount++
fmt.Printf("[METRICS] Total events: %d\n", m.eventCount)
}
type EmailObserver struct {
email string
}
func (e EmailObserver) OnEvent(event Event) {
fmt.Printf("[EMAIL] Notifying %s about %s\n", e.email, event.Type)
}
func main() {
bus := NewEventBus()
bus.Subscribe("user.created", LoggingObserver{})
bus.Subscribe("user.created", &MetricsObserver{})
bus.Subscribe("user.created", EmailObserver{email: "admin@example.com"})
bus.Subscribe("order.placed", LoggingObserver{})
bus.Publish(Event{Type: "user.created", Data: "alice"})
bus.Publish(Event{Type: "order.placed", Data: "ORD-001"})
}
Java:
public interface Observer {
void onEvent(String eventType, Object data);
}
public class EventBus {
private final Map<String, List<Observer>> observers = new HashMap<>();
public void subscribe(String eventType, Observer observer) {
observers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(observer);
}
public void unsubscribe(String eventType, Observer observer) {
List<Observer> list = observers.get(eventType);
if (list != null) list.remove(observer);
}
public void publish(String eventType, Object data) {
List<Observer> list = observers.getOrDefault(eventType, List.of());
for (Observer observer : list) {
observer.onEvent(eventType, data);
}
}
}
public class LoggingObserver implements Observer {
@Override
public void onEvent(String eventType, Object data) {
System.out.printf("[LOG] %s: %s%n", eventType, data);
}
}
public class EmailObserver implements Observer {
private final String email;
public EmailObserver(String email) { this.email = email; }
@Override
public void onEvent(String eventType, Object data) {
System.out.printf("[EMAIL] Notifying %s about %s%n", email, eventType);
}
}
Python:
from typing import Any, Callable
from collections import defaultdict
class EventBus:
def __init__(self):
self._observers: dict[str, list[Callable]] = defaultdict(list)
def subscribe(self, event_type: str, callback: Callable) -> None:
self._observers[event_type].append(callback)
def unsubscribe(self, event_type: str, callback: Callable) -> None:
self._observers[event_type].remove(callback)
def publish(self, event_type: str, data: Any = None) -> None:
for callback in self._observers[event_type]:
callback(event_type, data)
# Observers as functions (Pythonic approach)
def logging_observer(event_type: str, data: Any) -> None:
print(f"[LOG] {event_type}: {data}")
def email_observer(email: str):
def handler(event_type: str, data: Any) -> None:
print(f"[EMAIL] Notifying {email} about {event_type}")
return handler
# Usage
bus = EventBus()
bus.subscribe("user.created", logging_observer)
bus.subscribe("user.created", email_observer("admin@example.com"))
bus.publish("user.created", "alice")
Factory Pattern¶
Creates objects without specifying their exact class. Centralizes creation logic.
Go:
type Database interface {
Connect() error
Query(sql string) ([]map[string]any, error)
Close() error
}
type PostgresDB struct{ connStr string }
func (p *PostgresDB) Connect() error { fmt.Println("Connected to Postgres"); return nil }
func (p *PostgresDB) Query(sql string) ([]map[string]any, error) { return nil, nil }
func (p *PostgresDB) Close() error { return nil }
type MySQLDB struct{ connStr string }
func (m *MySQLDB) Connect() error { fmt.Println("Connected to MySQL"); return nil }
func (m *MySQLDB) Query(sql string) ([]map[string]any, error) { return nil, nil }
func (m *MySQLDB) Close() error { return nil }
type SQLiteDB struct{ path string }
func (s *SQLiteDB) Connect() error { fmt.Println("Connected to SQLite"); return nil }
func (s *SQLiteDB) Query(sql string) ([]map[string]any, error) { return nil, nil }
func (s *SQLiteDB) Close() error { return nil }
// Factory function
func NewDatabase(dbType, connectionStr string) (Database, error) {
switch dbType {
case "postgres":
return &PostgresDB{connStr: connectionStr}, nil
case "mysql":
return &MySQLDB{connStr: connectionStr}, nil
case "sqlite":
return &SQLiteDB{path: connectionStr}, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", dbType)
}
}
// Usage
db, err := NewDatabase("postgres", "host=localhost dbname=mydb")
if err != nil {
log.Fatal(err)
}
db.Connect()
Java:
public interface Database {
void connect();
List<Map<String, Object>> query(String sql);
void close();
}
public class DatabaseFactory {
public static Database create(String type, String connectionStr) {
return switch (type.toLowerCase()) {
case "postgres" -> new PostgresDB(connectionStr);
case "mysql" -> new MySQLDB(connectionStr);
case "sqlite" -> new SQLiteDB(connectionStr);
default -> throw new IllegalArgumentException("Unsupported: " + type);
};
}
}
// Usage
Database db = DatabaseFactory.create("postgres", "host=localhost");
db.connect();
Python:
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self) -> None: pass
@abstractmethod
def query(self, sql: str) -> list[dict]: pass
@abstractmethod
def close(self) -> None: pass
class PostgresDB(Database):
def __init__(self, conn_str: str):
self.conn_str = conn_str
def connect(self) -> None:
print(f"Connected to Postgres: {self.conn_str}")
def query(self, sql: str) -> list[dict]:
return []
def close(self) -> None:
pass
class MySQLDB(Database):
def __init__(self, conn_str: str):
self.conn_str = conn_str
def connect(self) -> None:
print(f"Connected to MySQL: {self.conn_str}")
def query(self, sql: str) -> list[dict]:
return []
def close(self) -> None:
pass
# Factory
_REGISTRY: dict[str, type[Database]] = {
"postgres": PostgresDB,
"mysql": MySQLDB,
}
def create_database(db_type: str, conn_str: str) -> Database:
cls = _REGISTRY.get(db_type)
if cls is None:
raise ValueError(f"Unsupported database type: {db_type}")
return cls(conn_str)
# Usage
db = create_database("postgres", "host=localhost")
db.connect()
Singleton Pattern¶
Ensures a class has only one instance. Use sparingly — singletons can make testing harder.
Go:
import "sync"
type Config struct {
DatabaseURL string
APIKey string
Debug bool
}
var (
configInstance *Config
configOnce sync.Once
)
func GetConfig() *Config {
configOnce.Do(func() {
configInstance = &Config{
DatabaseURL: "localhost:5432",
APIKey: "secret",
Debug: false,
}
})
return configInstance
}
// Usage
cfg := GetConfig()
fmt.Println(cfg.DatabaseURL)
Java:
public class Config {
private static volatile Config instance;
private String databaseURL;
private String apiKey;
private Config() {
this.databaseURL = "localhost:5432";
this.apiKey = "secret";
}
public static Config getInstance() {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) {
instance = new Config();
}
}
}
return instance;
}
// Better: use enum (thread-safe, serialization-safe)
// public enum Config {
// INSTANCE;
// private String databaseURL = "localhost:5432";
// }
}
Python:
class Config:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.database_url = "localhost:5432"
cls._instance.api_key = "secret"
cls._instance.debug = False
return cls._instance
# Usage
config1 = Config()
config2 = Config()
assert config1 is config2 # Same instance
Dependency Injection¶
DI is a technique where an object receives its dependencies from the outside rather than creating them internally.
Without DI (Hard to test)¶
type OrderService struct {
db *PostgresDB // Hard-coded dependency!
}
func NewOrderService() *OrderService {
return &OrderService{
db: &PostgresDB{connStr: "production_db"}, // Created internally
}
}
With DI (Testable, flexible)¶
Go:
type OrderRepository interface {
Save(order *Order) error
FindByID(id string) (*Order, error)
}
type PaymentProcessor interface {
Charge(amount float64, currency string) error
}
type NotificationService interface {
Notify(userID, message string) error
}
// All dependencies are injected through the constructor
type OrderService struct {
repo OrderRepository
payment PaymentProcessor
notifier NotificationService
}
func NewOrderService(
repo OrderRepository,
payment PaymentProcessor,
notifier NotificationService,
) *OrderService {
return &OrderService{
repo: repo,
payment: payment,
notifier: notifier,
}
}
func (s *OrderService) PlaceOrder(order *Order) error {
if err := s.payment.Charge(order.Total, "USD"); err != nil {
return fmt.Errorf("payment failed: %w", err)
}
if err := s.repo.Save(order); err != nil {
return fmt.Errorf("save failed: %w", err)
}
_ = s.notifier.Notify(order.UserID, "Order placed!")
return nil
}
// In production
func main() {
service := NewOrderService(
&PostgresOrderRepo{db: prodDB},
&StripeProcessor{key: "sk_live_..."},
&EmailNotifier{smtp: smtpClient},
)
// ...
}
// In tests
func TestPlaceOrder(t *testing.T) {
mockRepo := &MockOrderRepo{}
mockPayment := &MockPaymentProcessor{}
mockNotifier := &MockNotifier{}
service := NewOrderService(mockRepo, mockPayment, mockNotifier)
err := service.PlaceOrder(&Order{Total: 99.99, UserID: "user1"})
assert.NoError(t, err)
assert.Equal(t, 1, len(mockRepo.Saved))
}
Java:
public class OrderService {
private final OrderRepository repo;
private final PaymentProcessor payment;
private final NotificationService notifier;
// Constructor injection
public OrderService(
OrderRepository repo,
PaymentProcessor payment,
NotificationService notifier) {
this.repo = repo;
this.payment = payment;
this.notifier = notifier;
}
public void placeOrder(Order order) throws Exception {
payment.charge(order.getTotal(), "USD");
repo.save(order);
notifier.notify(order.getUserId(), "Order placed!");
}
}
// Spring framework example (annotation-based DI)
// @Service
// public class OrderService {
// @Autowired private OrderRepository repo;
// @Autowired private PaymentProcessor payment;
// }
Python:
class OrderService:
def __init__(
self,
repo: "OrderRepository",
payment: "PaymentProcessor",
notifier: "NotificationService",
):
self.repo = repo
self.payment = payment
self.notifier = notifier
def place_order(self, order: "Order") -> None:
self.payment.charge(order.total, "USD")
self.repo.save(order)
self.notifier.notify(order.user_id, "Order placed!")
# In tests
from unittest.mock import MagicMock
def test_place_order():
mock_repo = MagicMock()
mock_payment = MagicMock()
mock_notifier = MagicMock()
service = OrderService(mock_repo, mock_payment, mock_notifier)
order = Order(total=99.99, user_id="user1")
service.place_order(order)
mock_payment.charge.assert_called_once_with(99.99, "USD")
mock_repo.save.assert_called_once_with(order)
OOP in System Design (Domain Modeling)¶
When designing a system, OOP helps model real-world domains:
// Domain: E-commerce order system
type Money struct {
Amount int64 // in cents
Currency string
}
func (m Money) Add(other Money) Money {
if m.Currency != other.Currency {
panic("currency mismatch")
}
return Money{Amount: m.Amount + other.Amount, Currency: m.Currency}
}
type OrderStatus int
const (
OrderPending OrderStatus = iota
OrderConfirmed
OrderShipped
OrderDelivered
OrderCancelled
)
type OrderItem struct {
ProductID string
Name string
Price Money
Quantity int
}
func (oi OrderItem) Total() Money {
return Money{Amount: oi.Price.Amount * int64(oi.Quantity), Currency: oi.Price.Currency}
}
type Order struct {
ID string
UserID string
Items []OrderItem
Status OrderStatus
CreatedAt time.Time
}
func (o *Order) AddItem(item OrderItem) {
o.Items = append(o.Items, item)
}
func (o *Order) Total() Money {
total := Money{Amount: 0, Currency: "USD"}
for _, item := range o.Items {
total = total.Add(item.Total())
}
return total
}
func (o *Order) Cancel() error {
if o.Status == OrderShipped || o.Status == OrderDelivered {
return fmt.Errorf("cannot cancel order in status %d", o.Status)
}
o.Status = OrderCancelled
return nil
}
When OOP Hurts¶
Over-Engineering¶
// BAD: Abstract factory for a factory that creates builders for strategies...
public abstract class AbstractNotificationStrategyBuilderFactory {
public abstract NotificationStrategyBuilder createBuilder();
}
// GOOD: Just a function
public static void sendEmail(String to, String message) {
// Just do it
}
Deep Hierarchies¶
// BAD: 7 levels deep
Object
└── BaseEntity
└── AuditableEntity
└── SoftDeletableEntity
└── BaseUser
└── AuthenticatedUser
└── AdminUser
└── SuperAdminUser
Rule: Keep inheritance to 1-2 levels maximum.
Go's Approach: Simplicity¶
Go avoids these problems by design: - No inheritance — can't create deep hierarchies - Small interfaces — io.Reader has 1 method, io.Writer has 1 method - Composition — embed what you need - Package-level functions — not everything needs to be a method
// Go standard library: io.Reader is 1 method
type Reader interface {
Read(p []byte) (n int, err error)
}
// io.Writer is 1 method
type Writer interface {
Write(p []byte) (n int, err error)
}
// Compose them
type ReadWriter interface {
Reader
Writer
}
// This simplicity enables enormous flexibility
// Files, network connections, buffers, compressors, encryptors
// all implement Reader and/or Writer
Key Takeaways¶
- SOLID principles guide maintainable OOP design — learn them deeply
- Design patterns (Strategy, Observer, Factory, Singleton) are tools, not rules
- Dependency injection makes code testable and flexible
- Domain modeling with OOP helps map business concepts to code
- Over-engineering is real — use the simplest solution that works
- Go's philosophy (small interfaces, composition, no inheritance) prevents many OOP pitfalls
- Singletons should be used sparingly — they are global state in disguise