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
DataAccessprotocol defines an interface for fetching data from some source. TheDatabaseDataAccessandFileSystemDataAccessstructs both conform to theDataAccessprotocol and provide their own implementation of thefetchData()function. TheDataAnalysisclass depends on theDataAccessprotocol, rather than any specific implementation, to fetch data for analysis. -
By following the Dependency Inversion Principle, the
DataAnalysisclass is not directly dependent on the low-levelDatabaseDataAccessandFileSystemDataAccessstructs. This allows for greater flexibility because you can easily swap in a different implementation of theDataAccessprotocol without changing theDataAnalysisclass. It also makes the code easier to maintain because changes to the low-level implementations will not affect the high-levelDataAnalysisclass. -
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, andWalkingprotocols define interfaces for different types of movement. TheDuckstruct implements both theSwimmingandFlyingprotocols, while thePenguinstruct only implements theSwimmingprotocol and theOstrichstruct only implements theWalkingprotocol. -
By separating the interfaces into smaller, more specific protocols, we avoid forcing classes to implement unnecessary methods. For example, the
Ostrichclass 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
Animalprotocol defines an interface for making a noise. TheDogandCatstructs both conform to theAnimalprotocol and provide their own implementation of themakeNoise()function. TheAnimalShelterclass has an array ofAnimals and a function that makes all the animals in the array make a noise. -
Because the
DogandCatstructs both conform to theAnimalprotocol, they can be used as substitutes for each other in theAnimalShelterclass 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
Shapeprotocol defines an interface for calculating the area of a shape. TheCircleandRectanglestructs both conform to theShapeprotocol and provide their own implementation of thearea()function. TheShapeCalculatorclass 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
Shapeprotocol and provide an implementation of thearea()function. You don’t need to modify the existingShapeCalculatorclass 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
UserPersistenceprotocol and its implementationUserPersistenceImplhave the single responsibility of persisting aUserobject to and from disk. TheUserServiceclass has the responsibility of interacting with theUserPersistenceimplementation 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()
}
}