SOLID: The First 5 Principles of Object Oriented Design | DigitalOcean #weblinks
Dependency Inversion Principle
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
-
The Dependency Inversion Principle is a software design principle that states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. This allows for greater flexibility and maintainability of the code.
-
In this example, the
DataAccess
protocol defines an interface for fetching data from some source. TheDatabaseDataAccess
andFileSystemDataAccess
structs both conform to theDataAccess
protocol and provide their own implementation of thefetchData()
function. TheDataAnalysis
class depends on theDataAccess
protocol, rather than any specific implementation, to fetch data for analysis. -
By following the Dependency Inversion Principle, the
DataAnalysis
class is not directly dependent on the low-levelDatabaseDataAccess
andFileSystemDataAccess
structs. This allows for greater flexibility because you can easily swap in a different implementation of theDataAccess
protocol without changing theDataAnalysis
class. It also makes the code easier to maintain because changes to the low-level implementations will not affect the high-levelDataAnalysis
class. -
Here’s an example of how you might apply the Dependency Inversion Principle in Swift:
protocol DataAccess {
func fetchData() -> String
}
struct DatabaseDataAccess: DataAccess {
func fetchData() -> String {
// implementation details for fetching data from a database
}
}
struct FileSystemDataAccess: DataAccess {
func fetchData() -> String {
// implementation details for fetching data from the file system
}
}
class DataAnalysis {
private let dataAccess: DataAccess
init(dataAccess: DataAccess) {
self.dataAccess = dataAccess
}
func analyze() {
let data = dataAccess.fetchData()
// implementation details for analyzing the data
}
}
Interface Segregation Principle
-
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
-
The Interface Segregation Principle is a software design principle that states that clients (i.e., objects that use other objects) should not be forced to depend on interfaces they do not use. In other words, a class should not be required to implement interfaces that it does not need.
-
Here’s an example of how you might apply the Interface Segregation Principle in Swift:
protocol Swimming {
func swim() -> String
}
protocol Flying {
func fly() -> String
}
protocol Walking {
func walk() -> String
}
struct Duck: Swimming, Flying {
func swim() -> String {
return "I'm swimming!"
}
func fly() -> String {
return "I'm flying!"
}
}
struct Penguin: Swimming {
func swim() -> String {
return "I'm swimming!"
}
}
struct Ostrich: Walking {
func walk() -> String {
return "I'm walking!"
}
}
-
In this example, the
Swimming
,Flying
, andWalking
protocols define interfaces for different types of movement. TheDuck
struct implements both theSwimming
andFlying
protocols, while thePenguin
struct only implements theSwimming
protocol and theOstrich
struct only implements theWalking
protocol. -
By separating the interfaces into smaller, more specific protocols, we avoid forcing classes to implement unnecessary methods. For example, the
Ostrich
class does not need to implement theswim()
orfly()
methods because it cannot swim or fly. This leads to a more maintainable and scalable design.
Liskov Substitution Principle
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
-
The Liskov Substitution Principle is a software design principle that states that if a program is using a base type, then it should be able to use a subtype of that base type without knowing it. In other words, a subclass should be able to be used as a substitute for its superclass without affecting the correctness of the program.
-
In this example, the
Animal
protocol defines an interface for making a noise. TheDog
andCat
structs both conform to theAnimal
protocol and provide their own implementation of themakeNoise()
function. TheAnimalShelter
class has an array ofAnimal
s and a function that makes all the animals in the array make a noise. -
Because the
Dog
andCat
structs both conform to theAnimal
protocol, they can be used as substitutes for each other in theAnimalShelter
class without affecting the correctness of the program. This allows for greater flexibility and reuse of code.
protocol Animal {
func makeNoise() -> String
}
struct Dog: Animal {
func makeNoise() -> String {
return "Woof!"
}
}
struct Cat: Animal {
func makeNoise() -> String {
return "Meow!"
}
}
class AnimalShelter {
private var animals: [Animal] = []
func add(_ animal: Animal) {
animals.append(animal)
}
func makeNoises() {
for animal in animals {
print(animal.makeNoise())
}
}
}
let shelter = AnimalShelter()
shelter.add(Dog())
shelter.add(Cat())
shelter.makeNoises()
Open Closed Principle
-
Objects or entities should be open for extension but closed for modification.
-
The Open-Closed Principle is a software design principle that states that software entities (such as classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, you should be able to add new functionality to a software entity without changing its existing code.
-
In this example, the
Shape
protocol defines an interface for calculating the area of a shape. TheCircle
andRectangle
structs both conform to theShape
protocol and provide their own implementation of thearea()
function. TheShapeCalculator
class has a function that calculates the total area of an array of shapes by iterating over the array and summing the areas of each shape. -
If you want to add a new type of shape to the program, you can simply define a new struct that conforms to the
Shape
protocol and provide an implementation of thearea()
function. You don’t need to modify the existingShapeCalculator
class in any way. This allows you to add new functionality to the program without changing existing code, which makes the program more flexible and maintainable. -
Here’s an example of how you might apply the Open-Closed Principle in Swift:
protocol Shape {
func area() -> Double
}
struct Circle: Shape {
let radius: Double
func area() -> Double {
return .pi * radius * radius
}
}
struct Rectangle: Shape {
let width: Double
let height: Double
func area() -> Double {
return width * height
}
}
class ShapeCalculator {
func totalArea(shapes: [Shape]) -> Double {
return shapes.reduce(0) { $0 + $1.area() }
}
}
Single-responsibility Principle (SRP)
-
A class should have one and only one reason to change, meaning that a class should have only one job.
-
The Single Responsibility Principle is a software design principle that states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.
-
Here’s an example of how you might apply the Single Responsibility Principle in Swift:
-
In this example, the
UserPersistence
protocol and its implementationUserPersistenceImpl
have the single responsibility of persisting aUser
object to and from disk. TheUserService
class has the responsibility of interacting with theUserPersistence
implementation to register and retrieve users, but it does not have any responsibility for the actual persistence of the data. This separation of responsibilities allows for better maintainability and testability of the code.
struct User {
let name: String
let email: String
}
protocol UserPersistence {
func save(_ user: User)
func load() -> User?
}
struct UserPersistenceImpl: UserPersistence {
private let fileManager: FileManager
init(fileManager: FileManager) {
self.fileManager = fileManager
}
func save(_ user: User) {
// implementation details for saving the user to disk
}
func load() -> User? {
// implementation details for loading the user from disk
}
}
class UserService {
private let userPersistence: UserPersistence
init(userPersistence: UserPersistence) {
self.userPersistence = userPersistence
}
func register(name: String, email: String) {
let user = User(name: name, email: email)
userPersistence.save(user)
}
func getUser() -> User? {
return userPersistence.load()
}
}