SOLID Principles applied in Swift programming
Let’s explore how to apply the SOLID principles of Object-Oriented Programming (OOP) when designing a Swift SDK for an iOS app, specifically one for Oracle to manage customer cloud services. SOLID stands for Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP). Below, I’ll provide practical examples tailored to an Oracle Cloud Services SDK in Swift.
Assume the SDK handles tasks like authentication, fetching customer data, managing cloud resources (e.g., databases, compute instances), and logging usage analytics.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have a single responsibility.
Example: Separate authentication logic from resource management.
// Bad: One class handling both authentication and resource fetching
class CloudManager {
func authenticate(username: String, password: String) { /* ... */ }
func fetchResources() { /* ... */ }
}
// Good: Split responsibilities
class AuthenticationManager {
func authenticate(username: String, password: String) -> Result<String, Error> {
// Authenticate with Oracle Cloud API and return token
return .success("auth_token")
}
}
class ResourceManager {
private let authToken: String
init(authToken: String) {
self.authToken = authToken
}
func fetchResources() -> [CloudResource] {
// Fetch resources using authToken
return [CloudResource(id: "db1", type: .database)]
}
}
- Why it’s SRP:
AuthenticationManager
is responsible only for authentication, andResourceManager
handles resource-related tasks. If authentication logic changes (e.g., OAuth vs. API key), onlyAuthenticationManager
is affected.
2. Open/Closed Principle (OCP)
Definition: Classes should be open for extension but closed for modification.
Example: Allow adding new cloud resource types (e.g., database, compute) without modifying existing code.
// Bad: Hardcoding resource types in a switch
class ResourceHandler {
func processResource(type: String) {
switch type {
case "database": print("Processing database")
case "compute": print("Processing compute")
default: break
}
}
}
// Good: Use protocol and extensions
protocol CloudResource {
var id: String { get }
func process()
}
struct DatabaseResource: CloudResource {
let id: String
func process() {
print("Processing database: \(id)")
}
}
struct ComputeResource: CloudResource {
let id: String
func process() {
print("Processing compute: \(id)")
}
}
class ResourceHandler {
func processResource(_ resource: CloudResource) {
resource.process()
}
}
// Usage
let handler = ResourceHandler()
let db = DatabaseResource(id: "db1")
let compute = ComputeResource(id: "comp1")
handler.processResource(db) // "Processing database: db1"
handler.processResource(compute) // "Processing compute: comp1"
- Why it’s OCP: New resource types (e.g.,
StorageResource
) can be added by conforming toCloudResource
without changingResourceHandler
.
3. Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.
Example: Ensure a base CloudService
protocol can be swapped with specific implementations.
protocol CloudService {
func start() throws
func stop() throws
}
class DatabaseService: CloudService {
func start() throws {
print("Starting database service")
}
func stop() throws {
print("Stopping database service")
}
}
class ComputeService: CloudService {
func start() throws {
print("Starting compute service")
}
func stop() throws {
print("Stopping compute service")
}
}
class CloudController {
func manageService(_ service: CloudService) throws {
try service.start()
try service.stop()
}
}
// Usage
let controller = CloudController()
let dbService = DatabaseService()
let computeService = ComputeService()
try controller.manageService(dbService) // Works with DatabaseService
try controller.manageService(computeService) // Works with ComputeService
- Why it’s LSP:
DatabaseService
andComputeService
can substituteCloudService
without breakingCloudController
. If a subtype violated this (e.g., throwing unexpected errors), it would break LSP.
4. Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they don’t use.
Example: Split a broad CloudManager
protocol into smaller, focused protocols.
// Bad: One large protocol
protocol CloudManager {
func authenticate()
func fetchResources()
func logAnalytics()
}
// Good: Segregated protocols
protocol Authenticator {
func authenticate()
}
protocol ResourceFetcher {
func fetchResources()
}
protocol AnalyticsLogger {
func logAnalytics()
}
class OracleCloudClient: Authenticator, ResourceFetcher, AnalyticsLogger {
func authenticate() {
print("Authenticating with Oracle Cloud")
}
func fetchResources() {
print("Fetching cloud resources")
}
func logAnalytics() {
print("Logging usage data")
}
}
// Usage: Clients only depend on what they need
func setupAuthenticator(_ auth: Authenticator) {
auth.authenticate()
}
let client = OracleCloudClient()
setupAuthenticator(client) // Only uses Authenticator
- Why it’s ISP: A client needing only authentication isn’t forced to implement or depend on resource fetching or analytics logging.
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules; both should depend on abstractions. Also, abstractions should not depend on details.
Example: Decouple networking logic from the SDK using a protocol.
// Bad: Direct dependency on URLSession
class ResourceManager {
private let session = URLSession.shared
func fetchResources() {
session.dataTask(with: URL(string: "https://oraclecloud.com/resources")!) { _, _, _ in }
}
}
// Good: Depend on an abstraction
protocol NetworkClient {
func fetchData(from url: URL) async throws -> Data
}
class URLSessionClient: NetworkClient {
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func fetchData(from url: URL) async throws -> Data {
let (data, _) = try await session.data(from: url)
return data
}
}
class ResourceManager {
private let networkClient: NetworkClient
init(networkClient: NetworkClient) {
self.networkClient = networkClient
}
func fetchResources() async throws -> [CloudResource] {
let data = try await networkClient.fetchData(from: URL(string: "https://oraclecloud.com/resources")!)
// Parse data and return resources
return [CloudResource(id: "db1", type: .database)]
}
}
// Usage
let networkClient = URLSessionClient()
let resourceManager = ResourceManager(networkClient: networkClient)
- Why it’s DIP:
ResourceManager
depends on theNetworkClient
abstraction, notURLSession
. This allows swapping implementations (e.g., a mock client for testing) without changingResourceManager
.
Putting It Together: Oracle Cloud SDK
Here’s a cohesive example combining these principles:
// Abstractions
protocol Authenticator {
func authenticate() async throws -> String
}
protocol ResourceFetcher {
func fetchResources() async throws -> [CloudResource]
}
protocol CloudResource {
var id: String { get }
func process()
}
// Concrete implementations
struct DatabaseResource: CloudResource {
let id: String
func process() { print("Processing database: \(id)") }
}
class OracleAuthenticator: Authenticator {
private let networkClient: NetworkClient
init(networkClient: NetworkClient) {
self.networkClient = networkClient
}
func authenticate() async throws -> String {
let data = try await networkClient.fetchData(from: URL(string: "https://oraclecloud.com/auth")!)
return "auth_token" // Simplified
}
}
class OracleResourceFetcher: ResourceFetcher {
private let authToken: String
private let networkClient: NetworkClient
init(authToken: String, networkClient: NetworkClient) {
self.authToken = authToken
self.networkClient = networkClient
}
func fetchResources() async throws -> [CloudResource] {
let data = try await networkClient.fetchData(from: URL(string: "https://oraclecloud.com/resources?token=\(authToken)")!)
return [DatabaseResource(id: "db1")]
}
}
// Usage
let networkClient = URLSessionClient()
let authenticator = OracleAuthenticator(networkClient: networkClient)
let token = try await authenticator.authenticate()
let fetcher = OracleResourceFetcher(authToken: token, networkClient: networkClient)
let resources = try await fetcher.fetchResources()
resources.forEach { $0.process() }
- SRP: Each class has one job (authentication, fetching, processing).
- OCP: New resource types can be added via
CloudResource
. - LSP:
DatabaseResource
substitutesCloudResource
seamlessly. - ISP: Clients use only the protocols they need (
Authenticator
,ResourceFetcher
). - DIP: Dependencies are injected via abstractions (
NetworkClient
).
This design makes the SDK modular, testable, and extensible—perfect for managing Oracle Cloud services!