Design Pattern 1: Object-Oriented Concepts - Foundation for Understanding Complex Design Patterns

Download the complete Design Pattern series code from the design_pattern repo.

Introduction: The Foundation of Modern Software Design

Object-Oriented Programming (OOP) is the cornerstone of modern software development. Understanding the four core concepts—Encapsulation, Inheritance, Polymorphism, and Abstraction—is essential for mastering design patterns and building robust, maintainable software systems.

Real-World Applications

Object-oriented concepts are fundamental to:

  • Software Architecture: Building modular, scalable systems
  • Design Patterns: Implementing proven solutions to common problems
  • Framework Development: Creating reusable software components
  • API Design: Designing clean, intuitive interfaces
  • Database Design: Modeling complex data relationships

The Four Pillars of Object-Oriented Programming

The four core concepts of object-oriented design provide the foundation for understanding complex design patterns. Let’s explore each concept with practical examples.

1. Encapsulation (å°č£)

Encapsulation is the practice of hiding internal implementation details within a class, exposing only necessary methods to users. This protects internal properties and methods from unauthorized access.

Real-World Analogy: When driving a car, you only need to know that pressing the gas pedal accelerates and pressing the brake stops the car. You don’t need to understand the engine, battery, or transmission details—these are encapsulated under the hood.

Code Example

class BankAccount {
    private var balance: Double = 0.0
    private val accountNumber: String
    
    constructor(accountNumber: String) {
        this.accountNumber = accountNumber
    }
    
    fun deposit(amount: Double): Boolean {
        if (amount > 0) {
            balance += amount
            return true
        }
        return false
    }
    
    fun withdraw(amount: Double): Boolean {
        if (amount > 0 && balance >= amount) {
            balance -= amount
            return true
        }
        return false
    }
    
    fun getBalance(): Double = balance
    fun getAccountNumber(): String = accountNumber
}

// Usage
val account = BankAccount("123456789")
account.deposit(1000.0)
println("Balance: ${account.getBalance()}") // 1000.0
// account.balance = -500 // Compilation error - private access

2. Inheritance (繼承)

Inheritance allows a subclass to inherit properties and methods from a parent class, enabling code reuse and establishing hierarchical relationships.

Real-World Analogy: In nature, dogs and cats are both animals that can breathe and move; flowers and trees are both plants that can perform photosynthesis. This represents the concept of inheritance.

Code Example

open class Animal {
    protected var name: String = ""
    protected var age: Int = 0
    
    open fun makeSound() {
        println("Some animal sound")
    }
    
    open fun move() {
        println("Moving like an animal")
    }
}

class Dog : Animal() {
    private var breed: String = ""
    
    fun setBreed(breed: String) {
        this.breed = breed
    }
    
    override fun makeSound() {
        println("Woof! Woof!")
    }
    
    override fun move() {
        println("Running on four legs")
    }
    
    fun fetch() {
        println("Fetching the ball")
    }
}

class Cat : Animal() {
    private var color: String = ""
    
    fun setColor(color: String) {
        this.color = color
    }
    
    override fun makeSound() {
        println("Meow! Meow!")
    }
    
    override fun move() {
        println("Walking gracefully")
    }
    
    fun climb() {
        println("Climbing the tree")
    }
}

// Usage
val dog = Dog()
dog.makeSound() // Woof! Woof!
dog.fetch() // Fetching the ball

val cat = Cat()
cat.makeSound() // Meow! Meow!
cat.climb() // Climbing the tree

3. Polymorphism (多型)

Polymorphism provides a unified interface for different classes, allowing objects of different types to be treated uniformly.

Real-World Analogy: Consider the iPhone 6S—whether the chip is manufactured by TSMC or Samsung, users get the same iPhone 6S functionality. This is polymorphism in action.

Code Example

interface PaymentMethod {
    fun processPayment(amount: Double): Boolean
    fun getPaymentType(): String
}

class CreditCard : PaymentMethod {
    private var cardNumber: String = ""
    private var expiryDate: String = ""
    
    fun setCardDetails(cardNumber: String, expiryDate: String) {
        this.cardNumber = cardNumber
        this.expiryDate = expiryDate
    }
    
    override fun processPayment(amount: Double): Boolean {
        println("Processing $amount via Credit Card")
        return true
    }
    
    override fun getPaymentType(): String = "Credit Card"
}

class PayPal : PaymentMethod {
    private var email: String = ""
    
    fun setEmail(email: String) {
        this.email = email
    }
    
    override fun processPayment(amount: Double): Boolean {
        println("Processing $amount via PayPal")
        return true
    }
    
    override fun getPaymentType(): String = "PayPal"
}

class PaymentProcessor {
    fun processPayment(paymentMethod: PaymentMethod, amount: Double): Boolean {
        println("Using ${paymentMethod.getPaymentType()}")
        return paymentMethod.processPayment(amount)
    }
}

// Usage
val processor = PaymentProcessor()
val creditCard = CreditCard()
val paypal = PayPal()

processor.processPayment(creditCard, 100.0) // Using Credit Card
processor.processPayment(paypal, 50.0) // Using PayPal

4. Abstraction (抽豔)

Abstraction hides implementation details and provides only essential features to users through interfaces or abstract classes.

Real-World Analogy: When you install apps on your phone, ā€œAppā€ is an abstract name for various applications. When you buy fruits at the market, ā€œFruitā€ is an abstract term for apples, bananas, etc.

Code Example

abstract class Database {
    abstract fun connect(): Boolean
    abstract fun disconnect()
    abstract fun executeQuery(query: String): List<Map<String, Any>>
    
    fun isConnected(): Boolean {
        // Common implementation for all databases
        return true
    }
}

class MySQLDatabase : Database() {
    override fun connect(): Boolean {
        println("Connecting to MySQL database")
        return true
    }
    
    override fun disconnect() {
        println("Disconnecting from MySQL database")
    }
    
    override fun executeQuery(query: String): List<Map<String, Any>> {
        println("Executing query on MySQL: $query")
        return emptyList()
    }
}

class PostgreSQLDatabase : Database() {
    override fun connect(): Boolean {
        println("Connecting to PostgreSQL database")
        return true
    }
    
    override fun disconnect() {
        println("Disconnecting from PostgreSQL database")
    }
    
    override fun executeQuery(query: String): List<Map<String, Any>> {
        println("Executing query on PostgreSQL: $query")
        return emptyList()
    }
}

class DatabaseManager {
    fun performOperation(database: Database, query: String): List<Map<String, Any>> {
        database.connect()
        val result = database.executeQuery(query)
        database.disconnect()
        return result
    }
}

// Usage
val manager = DatabaseManager()
val mysql = MySQLDatabase()
val postgres = PostgreSQLDatabase()

manager.performOperation(mysql, "SELECT * FROM users")
manager.performOperation(postgres, "SELECT * FROM products")

Advanced Concepts and Best Practices

1. Composition vs Inheritance

// Prefer composition over inheritance
class Engine {
    fun start() = println("Engine started")
    fun stop() = println("Engine stopped")
}

class Car {
    private val engine = Engine() // Composition
    
    fun start() = engine.start()
    fun stop() = engine.stop()
}

// vs Inheritance (less flexible)
open class Vehicle {
    fun start() = println("Vehicle started")
    fun stop() = println("Vehicle stopped")
}

class Car : Vehicle() // Inheritance - less flexible

2. Interface Segregation Principle

// Good: Segregated interfaces
interface Readable {
    fun read(): String
}

interface Writable {
    fun write(data: String)
}

interface Deletable {
    fun delete()
}

// Classes implement only what they need
class ReadOnlyFile : Readable {
    override fun read(): String = "File content"
}

class WritableFile : Readable, Writable {
    override fun read(): String = "File content"
    override fun write(data: String) = println("Writing: $data")
}

// Avoid: Fat interface
interface FileOperations {
    fun read(): String
    fun write(data: String)
    fun delete()
    fun compress()
    fun encrypt()
}

3. Dependency Inversion Principle

// Good: Depend on abstractions
interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println("Console: $message")
}

class FileLogger : Logger {
    override fun log(message: String) = println("File: $message")
}

class UserService(private val logger: Logger) {
    fun createUser(username: String) {
        logger.log("Creating user: $username")
        // User creation logic
    }
}

// Usage
val consoleLogger = ConsoleLogger()
val fileLogger = FileLogger()

val userService1 = UserService(consoleLogger)
val userService2 = UserService(fileLogger)

Performance Considerations

Concept Memory Usage Performance Flexibility Maintainability
Encapsulation Low High Medium High
Inheritance Medium Medium Low Medium
Polymorphism Medium Medium High High
Abstraction Medium Medium High High

Common Anti-Patterns to Avoid

1. Inheritance Abuse

// Avoid: Deep inheritance hierarchies
class Animal
class Mammal : Animal()
class Dog : Mammal()
class GoldenRetriever : Dog()
class MyGoldenRetriever : GoldenRetriever() // Too deep!

2. Breaking Encapsulation

// Avoid: Exposing internal state
class BankAccount {
    var balance: Double = 0.0 // Public - breaks encapsulation
    var accountNumber: String = "" // Public - breaks encapsulation
}

3. Tight Coupling

// Avoid: Direct dependencies
class UserService {
    private val database = MySQLDatabase() // Tight coupling
    fun createUser(user: User) {
        database.connect()
        // Implementation
    }
}
  • Factory Method: Uses polymorphism to create objects
  • Strategy: Uses abstraction to define algorithms
  • Decorator: Uses composition and inheritance
  • Adapter: Uses abstraction to adapt interfaces

Conclusion

Understanding object-oriented concepts is essential for mastering design patterns and building robust software systems. Key benefits include:

  • Modularity: Code is organized into logical, reusable components
  • Maintainability: Changes are localized and easier to implement
  • Extensibility: New features can be added without modifying existing code
  • Reusability: Components can be reused across different parts of the application

These concepts form the foundation for understanding and implementing the 23 classic design patterns that solve common software design problems.




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • How to Use Multiple GitHub Accounts on One Computer: Complete SSH Setup Guide
  • Excalidraw AI: Create Professional Diagrams with Text Commands - Complete Guide
  • Complete macOS Development Environment Setup Guide for 2024
  • Design Pattern 28: Interpreter Pattern - Complete Guide with Examples
  • Design Pattern 27: Visitor Pattern - Complete Guide with Real-World IoT Examples