OOP Principles: Building Blocks of Object Oriented Design

Why Object Oriented Programming?

Before we dive into the four pillars, let's understand why OOP exists.
In the early days of programming, code was written as long sequences of instructions. As programs grew, they became impossible to manage. Variables were global. Functions could access anything. Changing one part broke ten others.
OOP emerged as a way to organize code into self-contained units (objects) that:
  • Protect their internal state (Encapsulation)
  • Hide complexity (Abstraction)
  • Share common behavior (Inheritance/Composition)
  • Behave differently based on type (Polymorphism)
Think of it like the difference between a pile of car parts on the floor and an assembled car. Both have the same components, but the organized car actually works.
Design principles diagram 1

Design principles diagram 1


Pillar 1: Encapsulation

What is Encapsulation?

Encapsulation is bundling data and the methods that operate on that data into a single unit, while restricting direct access to the internal state.
It's about:
  1. Keeping data and behavior together
  2. Controlling how data is accessed and modified
  3. Hiding implementation details

Real World Analogy: The ATM Machine

When you use an ATM, you interact with a simple interface:
  • Insert card
  • Enter PIN
  • Select amount
  • Get cash
You don't see (or need to see):
  • How the card is read
  • Where the money is stored
  • How the machine communicates with the bank
  • The security protocols being used
The ATM encapsulates all its complexity behind a simple interface. This protects both you (from complexity) and the bank (from tampering).
Design principles diagram 2

Design principles diagram 2

Without Encapsulation (The Problem)

go
// Filename: bad_no_encapsulation.go // WITHOUT ENCAPSULATION: Data exposed, anyone can mess with it package main import "fmt" // BankAccount with public fields - DANGEROUS! type BankAccount struct { Owner string Balance float64 // Anyone can access and modify! PIN string // Sensitive data exposed! } func main() { account := BankAccount{ Owner: "Alice", Balance: 1000.00, PIN: "1234", } // PROBLEM 1: Anyone can directly modify balance account.Balance = 999999999.99 // Free money! fmt.Printf("Balance: $%.2f\n", account.Balance) // PROBLEM 2: Anyone can read sensitive data fmt.Printf("PIN: %s\n", account.PIN) // Security breach! // PROBLEM 3: No validation on modifications account.Balance = -5000 // Invalid state! fmt.Printf("Balance: $%.2f\n", account.Balance) // PROBLEM 4: Business rules can't be enforced // There's no way to require PIN verification for withdrawals }

With Encapsulation (The Solution)

go
// Filename: good_encapsulation.go // WITH ENCAPSULATION: Data protected, access controlled package main import ( "errors" "fmt" ) // ============================================================================= // ENCAPSULATED BANK ACCOUNT // ============================================================================= // BankAccount encapsulates account data and operations type BankAccount struct { // Private fields - lowercase means unexported in Go owner string balance float64 pinHash string // Store hash, not actual PIN isLocked bool failedPINs int } // NewBankAccount is a constructor - the ONLY way to create an account // Why: Ensures account is always created in a valid state func NewBankAccount(owner string, initialDeposit float64, pin string) (*BankAccount, error) { // Validation if owner == "" { return nil, errors.New("owner name is required") } if initialDeposit < 0 { return nil, errors.New("initial deposit cannot be negative") } if len(pin) != 4 { return nil, errors.New("PIN must be 4 digits") } return &BankAccount{ owner: owner, balance: initialDeposit, pinHash: hashPIN(pin), // Don't store raw PIN isLocked: false, }, nil } // Owner returns the account owner (read-only access) // Why: External code can read but not modify func (a *BankAccount) Owner() string { return a.owner } // Balance returns the current balance // Why: Controlled read access, balance can only change through Deposit/Withdraw func (a *BankAccount) Balance() float64 { return a.balance } // IsLocked returns whether the account is locked func (a *BankAccount) IsLocked() bool { return a.isLocked } // Deposit adds money to the account // Why: Validates amount, enforces business rules func (a *BankAccount) Deposit(amount float64) error { if a.isLocked { return errors.New("account is locked") } if amount <= 0 { return errors.New("deposit amount must be positive") } if amount > 10000 { return errors.New("deposit exceeds daily limit of $10,000") } a.balance += amount return nil } // Withdraw removes money from the account (requires PIN) // Why: Business rules enforced - PIN required, sufficient funds checked func (a *BankAccount) Withdraw(amount float64, pin string) error { if a.isLocked { return errors.New("account is locked") } // Verify PIN if !a.verifyPIN(pin) { a.failedPINs++ if a.failedPINs >= 3 { a.isLocked = true return errors.New("account locked due to too many failed PIN attempts") } return fmt.Errorf("invalid PIN (%d attempts remaining)", 3-a.failedPINs) } a.failedPINs = 0 // Reset on successful PIN if amount <= 0 { return errors.New("withdrawal amount must be positive") } if amount > a.balance { return errors.New("insufficient funds") } if amount > 500 { return errors.New("withdrawal exceeds daily limit of $500") } a.balance -= amount return nil } // ChangePIN changes the account PIN // Why: Requires old PIN verification before changing func (a *BankAccount) ChangePIN(oldPIN, newPIN string) error { if a.isLocked { return errors.New("account is locked") } if !a.verifyPIN(oldPIN) { return errors.New("current PIN is incorrect") } if len(newPIN) != 4 { return errors.New("new PIN must be 4 digits") } a.pinHash = hashPIN(newPIN) return nil } // verifyPIN checks if the provided PIN matches (private method) // Why: Internal method, not exposed to outside code func (a *BankAccount) verifyPIN(pin string) bool { return hashPIN(pin) == a.pinHash } // hashPIN creates a simple hash of the PIN (private function) // In production, use proper cryptographic hashing func hashPIN(pin string) string { // Simplified - in production use bcrypt or similar hash := 0 for _, char := range pin { hash = hash*31 + int(char) } return fmt.Sprintf("%x", hash) } // ============================================================================= // DEMONSTRATION // ============================================================================= func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ ENCAPSULATION DEMONSTRATION ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Create account through constructor (only valid way) account, err := NewBankAccount("Alice", 1000.00, "1234") if err != nil { fmt.Printf("Failed to create account: %v\n", err) return } fmt.Printf("\n✅ Account created for %s\n", account.Owner()) fmt.Printf(" Balance: $%.2f\n", account.Balance()) // Try to access private fields - WON'T COMPILE // account.balance = 999999 // Error: balance is unexported // fmt.Println(account.pinHash) // Error: pinHash is unexported fmt.Println("\n📥 Depositing $500...") if err := account.Deposit(500); err != nil { fmt.Printf(" ❌ Failed: %v\n", err) } else { fmt.Printf(" ✅ New balance: $%.2f\n", account.Balance()) } fmt.Println("\n📤 Withdrawing $200 with correct PIN...") if err := account.Withdraw(200, "1234"); err != nil { fmt.Printf(" ❌ Failed: %v\n", err) } else { fmt.Printf(" ✅ New balance: $%.2f\n", account.Balance()) } fmt.Println("\n📤 Withdrawing $100 with WRONG PIN...") if err := account.Withdraw(100, "0000"); err != nil { fmt.Printf(" ❌ Failed: %v\n", err) } fmt.Println("\n📤 Trying to withdraw more than balance...") if err := account.Withdraw(10000, "1234"); err != nil { fmt.Printf(" ❌ Failed: %v\n", err) } fmt.Println("\n📤 Trying to deposit negative amount...") if err := account.Deposit(-100); err != nil { fmt.Printf(" ❌ Failed: %v\n", err) } fmt.Printf("\n📊 Final balance: $%.2f\n", account.Balance()) fmt.Println("\n" + "═"*60) fmt.Println("ENCAPSULATION BENEFITS:") fmt.Println(" ✓ Data can only be modified through controlled methods") fmt.Println(" ✓ Business rules enforced (PIN required for withdrawals)") fmt.Println(" ✓ Invalid states prevented (negative balance)") fmt.Println(" ✓ Sensitive data protected (PIN hash, not raw PIN)") fmt.Println("═"*60) }
Expected Output:
╔══════════════════════════════════════════════════════════╗ ║ ENCAPSULATION DEMONSTRATION ║ ╚══════════════════════════════════════════════════════════╝ ✅ Account created for Alice Balance: $1000.00 📥 Depositing $500... ✅ New balance: $1500.00 📤 Withdrawing $200 with correct PIN... ✅ New balance: $1300.00 📤 Withdrawing $100 with WRONG PIN... ❌ Failed: invalid PIN (2 attempts remaining) 📤 Trying to withdraw more than balance... ❌ Failed: withdrawal exceeds daily limit of $500 📤 Trying to deposit negative amount... ❌ Failed: deposit amount must be positive 📊 Final balance: $1300.00 ════════════════════════════════════════════════════════════ ENCAPSULATION BENEFITS: ✓ Data can only be modified through controlled methods ✓ Business rules enforced (PIN required for withdrawals) ✓ Invalid states prevented (negative balance) ✓ Sensitive data protected (PIN hash, not raw PIN) ════════════════════════════════════════════════════════════

Pillar 2: Abstraction

What is Abstraction?

Abstraction is hiding complex implementation details and showing only the essential features of an object.
While encapsulation is about protecting data, abstraction is about hiding complexity.

Real World Analogy: Driving a Car

When you drive a car, you interact with:
  • Steering wheel
  • Gas pedal
  • Brake pedal
  • Gear shifter
You don't need to understand:
  • How the engine combustion works
  • How fuel injection is timed
  • How the transmission shifts gears
  • How ABS prevents wheel lock-up
The car abstracts all this complexity into simple controls. A five-year-old can understand "press pedal to go, turn wheel to steer."
Design principles diagram 3

Design principles diagram 3

Abstraction in Code

go
// Filename: abstraction_example.go // ABSTRACTION: Hide complexity, expose simplicity package main import ( "fmt" "math/rand" "time" ) // ============================================================================= // ABSTRACT INTERFACE: What users care about // ============================================================================= // PaymentGateway abstracts all payment processing complexity // Why: Users just want to "charge a card" or "refund" - not deal with // network protocols, retry logic, fraud detection, etc. type PaymentGateway interface { Charge(amount float64, cardToken string) (transactionID string, err error) Refund(transactionID string) error } // ============================================================================= // COMPLEX IMPLEMENTATION: Hidden from users // ============================================================================= // StripeGateway implements PaymentGateway with all the complexity hidden type StripeGateway struct { apiKey string retryCount int fraudThreshold float64 rateLimiter *rateLimiter circuitBreaker *circuitBreaker logger *transactionLogger } // NewStripeGateway creates a properly configured gateway func NewStripeGateway(apiKey string) *StripeGateway { return &StripeGateway{ apiKey: apiKey, retryCount: 3, fraudThreshold: 0.8, rateLimiter: newRateLimiter(100), circuitBreaker: newCircuitBreaker(5), logger: newTransactionLogger(), } } // Charge implements the simple interface but does complex work func (s *StripeGateway) Charge(amount float64, cardToken string) (string, error) { // Step 1: Rate limiting (hidden complexity) if !s.rateLimiter.allow() { return "", fmt.Errorf("rate limit exceeded, please try again") } // Step 2: Circuit breaker check (hidden complexity) if s.circuitBreaker.isOpen() { return "", fmt.Errorf("payment service temporarily unavailable") } // Step 3: Fraud detection (hidden complexity) fraudScore := s.calculateFraudScore(amount, cardToken) if fraudScore > s.fraudThreshold { s.logger.logFraudAttempt(cardToken, fraudScore) return "", fmt.Errorf("transaction declined") } // Step 4: Retry logic (hidden complexity) var lastErr error for attempt := 1; attempt <= s.retryCount; attempt++ { transactionID, err := s.callStripeAPI(amount, cardToken) if err == nil { s.circuitBreaker.recordSuccess() s.logger.logSuccess(transactionID, amount) return transactionID, nil } lastErr = err s.circuitBreaker.recordFailure() time.Sleep(time.Duration(attempt*100) * time.Millisecond) // Exponential backoff } return "", lastErr } // Refund implements refund with hidden complexity func (s *StripeGateway) Refund(transactionID string) error { // All the complexity of refunds hidden here fmt.Printf("[Internal] Processing refund for %s...\n", transactionID) s.logger.logRefund(transactionID) return nil } // Hidden complexity - user doesn't need to know about these func (s *StripeGateway) calculateFraudScore(amount float64, token string) float64 { // Complex ML-based fraud detection // Checking velocity, location, device fingerprint, etc. return rand.Float64() * 0.5 // Simplified } func (s *StripeGateway) callStripeAPI(amount float64, token string) (string, error) { // HTTP call to Stripe, SSL, authentication, etc. fmt.Printf("[Internal] Calling Stripe API for $%.2f...\n", amount) return fmt.Sprintf("txn_%d", rand.Int63()), nil } // Support structures (all hidden from the user) type rateLimiter struct { requestsPerSecond int } func newRateLimiter(rps int) *rateLimiter { return &rateLimiter{requestsPerSecond: rps} } func (r *rateLimiter) allow() bool { return true // Simplified } type circuitBreaker struct { threshold int failures int } func newCircuitBreaker(threshold int) *circuitBreaker { return &circuitBreaker{threshold: threshold} } func (c *circuitBreaker) isOpen() bool { return c.failures >= c.threshold } func (c *circuitBreaker) recordSuccess() { c.failures = 0 } func (c *circuitBreaker) recordFailure() { c.failures++ } type transactionLogger struct{} func newTransactionLogger() *transactionLogger { return &transactionLogger{} } func (t *transactionLogger) logSuccess(txnID string, amount float64) { fmt.Printf("[Internal] Logged successful transaction: %s ($%.2f)\n", txnID, amount) } func (t *transactionLogger) logFraudAttempt(token string, score float64) { fmt.Printf("[Internal] Fraud attempt logged: token=%s, score=%.2f\n", token, score) } func (t *transactionLogger) logRefund(txnID string) { fmt.Printf("[Internal] Refund logged: %s\n", txnID) } // ============================================================================= // USER'S PERSPECTIVE: Simple and clean // ============================================================================= // PaymentService is what application code interacts with type PaymentService struct { gateway PaymentGateway } func NewPaymentService(gateway PaymentGateway) *PaymentService { return &PaymentService{gateway: gateway} } // ProcessPayment - simple for the user! func (p *PaymentService) ProcessPayment(amount float64, cardToken string) error { // Look how simple this is! txnID, err := p.gateway.Charge(amount, cardToken) if err != nil { return err } fmt.Printf("✅ Payment successful! Transaction: %s\n", txnID) return nil } func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ ABSTRACTION DEMONSTRATION ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Setup - complexity hidden behind simple interface gateway := NewStripeGateway("sk_test_abc123") paymentService := NewPaymentService(gateway) fmt.Println("\n🔄 Processing payment...") fmt.Println("(Watch the hidden complexity at work)\n") // User's code is SIMPLE - all complexity is abstracted away err := paymentService.ProcessPayment(99.99, "tok_visa_1234") if err != nil { fmt.Printf("❌ Payment failed: %v\n", err) } fmt.Println("\n" + "═"*60) fmt.Println("ABSTRACTION BENEFITS:") fmt.Println(" ✓ User code is simple: gateway.Charge(amount, token)") fmt.Println(" ✓ Complex logic hidden: retries, rate limiting, fraud") fmt.Println(" ✓ Easy to use: No need to understand internals") fmt.Println(" ✓ Easy to change: Swap Stripe for PayPal without changes") fmt.Println("═"*60) }

Pillar 3: Inheritance vs Composition

What is Inheritance?

Inheritance is a mechanism where a new class acquires the properties and behaviors of an existing class.
Go doesn't have classical inheritance, but it has composition and embedding, which are often better.

The "Is-A" vs "Has-A" Debate

  • Inheritance (Is-A): A Dog IS-A Animal
  • Composition (Has-A): A Car HAS-A Engine
Modern software design favors composition over inheritance because:
  1. It's more flexible
  2. It avoids deep inheritance hierarchies
  3. It allows combining behaviors from multiple sources

Real World Analogy: LEGO vs Pre-made Toys

Inheritance is like pre-made action figures. A "SuperHero" figure comes with cape, muscles, and mask baked in. If you want a superhero without a cape, too bad.
Composition is like LEGO. You build what you need from smaller pieces. Want a superhero without a cape but with wings? Just swap the pieces.
Design principles diagram 4

Design principles diagram 4

Composition in Go

go
// Filename: composition_example.go // COMPOSITION: Building complex objects from simple pieces package main import ( "fmt" "time" ) // ============================================================================= // SMALL, FOCUSED COMPONENTS (Building Blocks) // ============================================================================= // Logger provides logging capability type Logger struct { prefix string } func NewLogger(prefix string) *Logger { return &Logger{prefix: prefix} } func (l *Logger) Log(message string) { fmt.Printf("[%s][%s] %s\n", time.Now().Format("15:04:05"), l.prefix, message) } func (l *Logger) Error(message string) { fmt.Printf("[%s][%s][ERROR] %s\n", time.Now().Format("15:04:05"), l.prefix, message) } // Metrics provides metrics collection capability type Metrics struct { namespace string counters map[string]int } func NewMetrics(namespace string) *Metrics { return &Metrics{ namespace: namespace, counters: make(map[string]int), } } func (m *Metrics) Increment(metric string) { m.counters[metric]++ fmt.Printf("[METRICS] %s.%s = %d\n", m.namespace, metric, m.counters[metric]) } func (m *Metrics) Get(metric string) int { return m.counters[metric] } // Cache provides caching capability type Cache struct { data map[string]interface{} ttl time.Duration } func NewCache(ttl time.Duration) *Cache { return &Cache{ data: make(map[string]interface{}), ttl: ttl, } } func (c *Cache) Set(key string, value interface{}) { c.data[key] = value fmt.Printf("[CACHE] Set: %s\n", key) } func (c *Cache) Get(key string) (interface{}, bool) { value, exists := c.data[key] if exists { fmt.Printf("[CACHE] Hit: %s\n", key) } else { fmt.Printf("[CACHE] Miss: %s\n", key) } return value, exists } // ============================================================================= // COMPOSED SERVICES (Built from components) // ============================================================================= // UserService composes multiple capabilities // Why: UserService HAS-A Logger, HAS-A Metrics, HAS-A Cache // Why: Not "UserService IS-A Logger" - that doesn't make sense! type UserService struct { logger *Logger metrics *Metrics cache *Cache users map[int]*User } type User struct { ID int Name string } // NewUserService creates a UserService with all its components func NewUserService() *UserService { return &UserService{ logger: NewLogger("UserService"), metrics: NewMetrics("users"), cache: NewCache(5 * time.Minute), users: make(map[int]*User), } } // GetUser demonstrates using composed components func (s *UserService) GetUser(id int) (*User, error) { s.logger.Log(fmt.Sprintf("GetUser called with id=%d", id)) // Try cache first if cached, exists := s.cache.Get(fmt.Sprintf("user:%d", id)); exists { s.metrics.Increment("cache_hits") return cached.(*User), nil } // Cache miss - get from "database" s.metrics.Increment("cache_misses") user, exists := s.users[id] if !exists { s.logger.Error(fmt.Sprintf("User %d not found", id)) s.metrics.Increment("not_found") return nil, fmt.Errorf("user not found") } // Store in cache for next time s.cache.Set(fmt.Sprintf("user:%d", id), user) s.metrics.Increment("successful_gets") return user, nil } // CreateUser creates a user using composed components func (s *UserService) CreateUser(name string) *User { s.logger.Log(fmt.Sprintf("Creating user: %s", name)) user := &User{ ID: len(s.users) + 1, Name: name, } s.users[user.ID] = user s.metrics.Increment("users_created") s.logger.Log(fmt.Sprintf("User created with ID %d", user.ID)) return user } // ============================================================================= // GO EMBEDDING (Composition with convenience) // ============================================================================= // EmbeddedLogger shows Go's embedding feature type EmbeddedLogger struct { *Logger // Embedded - all Logger methods are promoted } // EnhancedService embeds Logger for convenience type EnhancedService struct { *Logger // Embedded - can call Log() directly on EnhancedService *Metrics // Embedded - can call Increment() directly // Regular field name string } func NewEnhancedService(name string) *EnhancedService { return &EnhancedService{ Logger: NewLogger(name), Metrics: NewMetrics(name), name: name, } } func (e *EnhancedService) DoWork() { // Can call Logger methods directly due to embedding! e.Log("Starting work...") // From embedded Logger e.Increment("work_started") // From embedded Metrics e.Log("Work completed!") e.Increment("work_completed") } // ============================================================================= // DEMONSTRATION // ============================================================================= func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ COMPOSITION OVER INHERITANCE ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Example 1: UserService composed of multiple components fmt.Println("\n📦 UserService (Composition):") fmt.Println("─"*50) userService := NewUserService() // Create some users userService.CreateUser("Alice") userService.CreateUser("Bob") fmt.Println() // Get user (cache miss, then cache hit) userService.GetUser(1) fmt.Println() userService.GetUser(1) // Second call hits cache fmt.Println() userService.GetUser(999) // Not found // Example 2: Embedding for convenience fmt.Println("\n\n📦 EnhancedService (Embedding):") fmt.Println("─"*50) enhanced := NewEnhancedService("Worker") enhanced.DoWork() fmt.Println("\n" + "═"*60) fmt.Println("COMPOSITION BENEFITS:") fmt.Println(" ✓ Flexible: Mix and match capabilities") fmt.Println(" ✓ Testable: Mock individual components") fmt.Println(" ✓ Simple: No deep inheritance hierarchies") fmt.Println(" ✓ Clear: Explicit dependencies via fields") fmt.Println("═"*60) }

Pillar 4: Polymorphism

What is Polymorphism?

Polymorphism means "many forms" - the same interface can be implemented in different ways by different types.
It allows you to:
  1. Write code that works with any type implementing an interface
  2. Swap implementations without changing the code that uses them
  3. Extend behavior without modifying existing code

Real World Analogy: USB Ports

A USB port is polymorphic. It doesn't care what you plug into it:
  • Phone charger
  • Keyboard
  • Mouse
  • Hard drive
  • Camera
All these devices "implement" the USB interface. The port works with all of them the same way.
Design principles diagram 5

Design principles diagram 5

Polymorphism in Go

go
// Filename: polymorphism_example.go // POLYMORPHISM: One interface, many implementations package main import ( "fmt" "strings" "time" ) // ============================================================================= // THE INTERFACE: Defines what all implementations must do // ============================================================================= // Notifier is the interface for sending notifications // Why: Any type that can send a notification implements this type Notifier interface { Send(to string, message string) error Name() string } // ============================================================================= // IMPLEMENTATIONS: Different ways to send notifications // ============================================================================= // EmailNotifier sends notifications via email type EmailNotifier struct { smtpHost string } func NewEmailNotifier(host string) *EmailNotifier { return &EmailNotifier{smtpHost: host} } func (e *EmailNotifier) Send(to string, message string) error { fmt.Printf("📧 [EMAIL] Sending to %s via %s\n", to, e.smtpHost) fmt.Printf(" Message: %s\n", message) return nil } func (e *EmailNotifier) Name() string { return "Email" } // SMSNotifier sends notifications via SMS type SMSNotifier struct { apiKey string } func NewSMSNotifier(apiKey string) *SMSNotifier { return &SMSNotifier{apiKey: apiKey} } func (s *SMSNotifier) Send(to string, message string) error { // SMS has character limit truncated := message if len(message) > 160 { truncated = message[:157] + "..." } fmt.Printf("📱 [SMS] Sending to %s\n", to) fmt.Printf(" Message: %s\n", truncated) return nil } func (s *SMSNotifier) Name() string { return "SMS" } // SlackNotifier sends notifications via Slack type SlackNotifier struct { webhookURL string } func NewSlackNotifier(webhookURL string) *SlackNotifier { return &SlackNotifier{webhookURL: webhookURL} } func (s *SlackNotifier) Send(to string, message string) error { fmt.Printf("💬 [SLACK] Sending to channel #%s\n", to) fmt.Printf(" Message: %s\n", message) return nil } func (s *SlackNotifier) Name() string { return "Slack" } // PushNotifier sends push notifications type PushNotifier struct { appID string } func NewPushNotifier(appID string) *PushNotifier { return &PushNotifier{appID: appID} } func (p *PushNotifier) Send(to string, message string) error { fmt.Printf("🔔 [PUSH] Sending to device %s\n", to) fmt.Printf(" Message: %s\n", message) return nil } func (p *PushNotifier) Name() string { return "Push" } // ConsoleNotifier for testing (prints to console) type ConsoleNotifier struct{} func (c *ConsoleNotifier) Send(to string, message string) error { fmt.Printf("🖥️ [CONSOLE] To: %s | Message: %s\n", to, message) return nil } func (c *ConsoleNotifier) Name() string { return "Console" } // ============================================================================= // POLYMORPHIC CODE: Works with ANY Notifier // ============================================================================= // NotificationService uses any Notifier through the interface type NotificationService struct { notifiers []Notifier } func NewNotificationService(notifiers ...Notifier) *NotificationService { return &NotificationService{notifiers: notifiers} } // AddNotifier adds a new notification channel func (n *NotificationService) AddNotifier(notifier Notifier) { n.notifiers = append(n.notifiers, notifier) } // NotifyAll sends to all configured notifiers // Why: This method doesn't care about specific types // Why: It works with Email, SMS, Slack, or any future Notifier func (n *NotificationService) NotifyAll(to string, message string) { fmt.Printf("\n📢 Sending notification to all %d channels...\n", len(n.notifiers)) fmt.Println(strings.Repeat("-", 50)) for _, notifier := range n.notifiers { if err := notifier.Send(to, message); err != nil { fmt.Printf(" ❌ [%s] Failed: %v\n", notifier.Name(), err) } else { fmt.Printf(" ✅ [%s] Sent successfully\n", notifier.Name()) } fmt.Println() } } // NotifyVia sends via a specific notifier type // Why: Polymorphism allows passing any Notifier implementation func NotifyVia(notifier Notifier, to string, message string) { fmt.Printf("\n📤 Sending via %s...\n", notifier.Name()) notifier.Send(to, message) } // ============================================================================= // DEMONSTRATION // ============================================================================= func main() { fmt.Println("╔══════════════════════════════════════════════════════════╗") fmt.Println("║ POLYMORPHISM DEMONSTRATION ║") fmt.Println("╚══════════════════════════════════════════════════════════╝") // Create different notifiers email := NewEmailNotifier("smtp.company.com") sms := NewSMSNotifier("api_key_123") slack := NewSlackNotifier("https://hooks.slack.com/xxx") push := NewPushNotifier("app_id_456") console := &ConsoleNotifier{} // Example 1: Service with multiple notifiers fmt.Println("\n📦 Example 1: Multi-Channel Notification") fmt.Println("═"*50) service := NewNotificationService(email, sms, slack) service.NotifyAll("user@example.com", "Your order has shipped!") // Example 2: Function that accepts any notifier fmt.Println("\n📦 Example 2: Polymorphic Function") fmt.Println("═"*50) // Same function, different notifiers! NotifyVia(email, "admin@example.com", "Server CPU at 90%") NotifyVia(push, "device_xyz", "You have a new message") NotifyVia(console, "developer", "Debug notification") // Example 3: Easy to add new notifier types fmt.Println("\n📦 Example 3: Adding New Notifier at Runtime") fmt.Println("═"*50) service.AddNotifier(push) service.AddNotifier(console) fmt.Println("\nAfter adding Push and Console notifiers:") service.NotifyAll("team", "System maintenance in 10 minutes") // Example 4: Testing with mock notifier fmt.Println("\n📦 Example 4: Testing with Console Notifier") fmt.Println("═"*50) testService := NewNotificationService(console) testService.NotifyAll("test_user", "This is a test notification") fmt.Println("\n" + "═"*60) fmt.Println("POLYMORPHISM BENEFITS:") fmt.Println(" ✓ One interface, multiple implementations") fmt.Println(" ✓ Code works with any Notifier type") fmt.Println(" ✓ Easy to add new notification channels") fmt.Println(" ✓ Easy to test with mock implementations") fmt.Println(" ✓ Follows Open/Closed Principle") fmt.Println("═"*60) }

OOP Principles Summary

Design principles diagram 6

Design principles diagram 6

Quick Reference

PrinciplePurposeIn Go
EncapsulationProtect internal stateUnexported fields (lowercase)
AbstractionHide complexityInterfaces with minimal methods
CompositionBuild from partsStruct embedding, has-a relationships
PolymorphismSame interface, different behaviorInterface implementations

Your Next Steps

  1. Practice Encapsulation: Add validation to one of your structs
  2. Practice Abstraction: Create an interface for a complex operation
  3. Practice Composition: Refactor an inheritance hierarchy to use composition
  4. Practice Polymorphism: Create multiple implementations of one interface
  5. Read Next: Go Interfaces Deep Dive

"Object-oriented programming is an exceptionally bad idea which could only have originated in California." - Edsger Dijkstra
(He was wrong, but it's a funny quote. OOP, when used wisely, creates maintainable, extensible software.)
All Blogs
Tags:oopdesign-principlesgolangencapsulationpolymorphismabstraction