SOLID是由Robert C. Martin(Uncle Bob)創建的縮寫。它代表面向對象編程的五個原則:單一職責原則,開放/封閉原則,Liskov替換原則,接口隔離原則和依賴反轉原則。
應用這些原則,您可以解決架構的以下幾個主要問題:
當然,Uncle Bob在他的文章中指出,這些規則并不是嚴格的,而是提高架構質量的指導方針。
規則不會把壞的程序員變成一個好的程序員。這些原則必須靈活使用。如果生搬硬套,那就像根本不應用一樣糟糕。你必須足夠聰明才能理解何時應用這些原則。
“一個類應該有且只有一個變化的原因” -SRP:單一職責原則
每次你創建/改變一個類,你應該問自己:這個類有多少職責?
我們來看一個例子:
class Handler {
func handle() {
let data = requestDataToAPI()
let array = parse(data: data)
saveToDB(array: array)
}
private func requestDataToAPI() -> Data {
// send API request and wait the response
}
private func parse(data: Data) -> [String] {
// parse the data and create the array
}
private func saveToDB(array: [String]) {
// save the array in a database (CoreData/Realm/…)
}
}
這個類有多少職責?
Handler類從API檢索數據,解析API響應,創建一個String數組,并將數組保存在數據庫)中。
一旦您認為必須在類Alamofire中以同樣的方式處理API請求,使用ObjectMapper進行解析,并將CoreData堆棧保存在數據庫中,您將開始了解此類的意義。
你可以把職責轉移到小類來解決這個問題:
class Handler {
let apiHandler: APIHandler
let parseHandler: ParseHandler
let dbHandler: DBHandler
init(apiHandler: APIHandler, parseHandler: ParseHandler, dbHandler: DBHandler) {
self.apiHandler = apiHandler
self.parseHandler = parseHandler
self.dbHandler = dbHandler
}
func handle() {
let data = apiHandler.requestDataToAPI()
let array = parseHandler.parse(data: data)
dbHandler.saveToDB(array: array)
}
}
class APIHandler {
func requestDataToAPI() -> Data {
// send API request and wait the response
}
}
class ParseHandler {
func parse(data: Data) -> [String] {
// parse the data and create the array
}
}
class DBHandler {
func saveToDB(array: [String]) {
// save the array in a database (CoreData/Realm/…)
}
}
這個原則可以幫助你保持代碼盡可能的干凈。此外,在個示例中,您無法直接測試requestDataToAPI,parse和saveToDB,因為這些是私有方法。重構后,您可以輕松地測試APIHandler,ParseHandler和DBHandler。
“軟件實體(類,模塊,功能等)應該面向擴展開放,面向修改封閉。 – 開放封閉原則
如果要創建一個易于維護的類,它必須具有兩個重要特征:
- 面向擴展開放:應該能夠擴展或改變類的行為,而不用付出太多精力。
- 面向修改封閉:擴展一個類而不改變類代碼的實現。
您可以通過抽象實現這些特性。
作為一個例子,我們有一個類Logger,它遍歷數組類Cars,打印出每輛車的細節:
class Logger {
func printData() {
let cars = [
Car(name: “Batmobile”, color: “Black”),
Car(name: “SuperCar”, color: “Gold”),
Car(name: “FamilyCar”, color: “Grey”)
]
cars.forEach { car in
print(car.printDetails())
}
}
}
class Car {
let name: String
let color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
func printDetails() -> String {
return “I’m (name) and my color is (color)”
}
}
如果您想添加打印一個新類的詳細信息的可能,每次要記錄一個新類,就要更改printData的實現--破壞了OCP原則:
class Logger {
func printData() {
let cars = [
Car(name: “Batmobile”, color: “Black”),
Car(name: “SuperCar”, color: “Gold”),
Car(name: “FamilyCar”, color: “Grey”)
]
cars.forEach { car in
print(car.printDetails())
}
let bicycles = [
Bicycle(type: “BMX”),
Bicycle(type: “Tandem”)
]
bicycles.forEach { bicycles in
print(bicycles.printDetails())
}
}
}
class Car {
let name: String
let color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
func printDetails() -> String {
return “I’m (name) and my color is (color)”
}
}
class Bicycle {
let type: String
init(type: String) {
self.type = type
}
func printDetails() -> String {
return “I’m a (type)”
}
}
為了解決這個問題創建一個新的協議類Printable。后,printData將打印一個Printable數組。
這樣,我們在printData和類之間創建一個新的抽象層來記錄,允許打印其他類,如Bicycle,而不需要更改printData的實現。
protocol Printable {
func printDetails() -> String
}
class Logger {
func printData() {
let cars: [Printable] = [
Car(name: “Batmobile”, color: “Black”),
Car(name: “SuperCar”, color: “Gold”),
Car(name: “FamilyCar”, color: “Grey”),
Bicycle(type: “BMX”),
Bicycle(type: “Tandem”)
]
cars.forEach { car in
print(car.printDetails())
}
}
}
class Car: Printable {
let name: String
let color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
func printDetails() -> String {
return “I’m (name) and my color is (color)”
}
}
class Bicycle: Printable {
let type: String
init(type: String) {
self.type = type
}
func printDetails() -> String {
return “I’m a (type)”
}
}
“使用基類對象指針或引用的函數必須能夠在不了解衍生類的條件下使用衍生類的對象。 - Liskov替換原則
繼承可能是危險的,您應該使用組合繼承來避免一個凌亂的代碼庫,如果您以不正確的方式使用繼承,更是如此。
這個原則可以幫助你使用繼承,而不會弄亂代碼。讓我們看看破壞LSP原則的主要問題:
我們有一個類Handler,它負責將一個字符串保存在云服務中。后來,業務邏輯發生變化,有時候,如果字付串長度大于5,則必須保存此字符串。因此,我們決定創建一個子類FilteredHandler:
class Handler {
func save(string: String) {
// Save string in the Cloud
}
}
class FilteredHandler: Handler {
override func save(string: String) {
guard string.characters.count > 5 else { return }
super.save(string: string)
}
}
此示例打破了LSP原則,因為在子類中,我們添加了string長度大于5的前提條件。Handler的客戶端不期望FilteredHandler具有不同的前提條件,因為它對于Handler及其子類都應該是相同的。
我們可以解決這個問題,去掉FilteredHandler類,添加一個新的注入參數過濾字付串的小長度:
class Handler {
func save(string: String, minChars: Int = 0) {
guard string.characters.count >= minChars else { return }
// Save string in the Cloud
}
}
我們有一個項目,計算一些矩形對象的面積,所以我們創建了類Rectangle。幾個月后,我們還需要計算正方形對象的面積,所以我們決定創建一個子類Square。由于在一個正方形中,我們只需要一條邊來計算面積,而且我們不想覆蓋面積計算方法-我們決定將width的值分配給length:
class Rectangle {
var width: Float = 0
var length: Float = 0
var area: Float {
return width * length
}
}
class Square: Rectangle {
override var width: Float {
didSet {
length = width
}
}
}
使用這種方法,我們打破LSP原則,因為如果客戶端具有以下方法:
func printArea(of rectangle: Rectangle) {
rectangle.length = 5
rectangle.width = 2
print(rectangle.area)
}
兩個方法的結果應該是一致的:
let rectangle = Rectangle()
printArea(of: rectangle) // 10
// ——————————-
let square = Square()
printArea(of: square) // 4
相反,個打印10,第二個打印4,這意味著,通過這種繼承,我們剛剛破壞了width設置器的后置條件:((width = newValue)&&(height == height))。
我們可以使用具有方法area的協議來解決它,由Rectangle和Square以不同的方式實現。后,我們更改printArea參數類型以接受實現此協議的對象:
protocol Polygon {
var area: Float { get }
}
class Rectangle: Polygon {
private let width: Float
private let length: Float
init(width: Float, length: Float) {
self.width = width
self.length = length
}
var area: Float {
return width * length
}
}
class Square: Polygon {
private let side: Float
init(side: Float) {
self.side = side
}
var area: Float {
return pow(side, 2)
}
}
// Client Method
func printArea(of polygon: Polygon) {
print(polygon.area)
}
// Usage
let rectangle = Rectangle(width: 2, length: 5)
printArea(of: rectangle) // 10
let square = Square(side: 2)
printArea(of: square) // 4
“客戶不應該被迫依賴于他們不使用的接口。” – 接口隔離原則
這個原則引入了面向對象編程的一個問題:胖接口。
當有太多的成員/方法,這個接口被稱為“胖”,它們不是一致的,并且包含比我們真正想要的更多的信息。此問題可能會影響類和協議。
我們從協議GestureProtocol開始,它有方法didTap:
protocol GestureProtocol {
func didTap()
}
一段時間后,您必須向協議添加新的手勢,變成:
protocol GestureProtocol {
func didTap()
func didDoubleTap()
func didLongPress()
}
我們的SuperButton類很樂意實現所需的方法:
class SuperButton: GestureProtocol {
func didTap() {
// send tap action
}
func didDoubleTap() {
// send double tap action
}
func didLongPress() {
// send long press action
}
}
問題是我們的應用程序還有一個PoorButton,它只需要didTap。它必須實現它不需要的方法,打破了ISP原則:
class PoorButton: GestureProtocol {
func didTap() {
// send tap action
}
func didDoubleTap() { }
func didLongPress() { }
}
我們可以使用很小的協議解決這個問題,而不是一個大的協議:
protocol TapProtocol {
func didTap()
}
protocol DoubleTapProtocol {
func didDoubleTap()
}
protocol LongPressProtocol {
func didLongPress()
}
class SuperButton: TapProtocol, DoubleTapProtocol, LongPressProtocol {
func didTap() {
// send tap action
}
func didDoubleTap() {
// send double tap action
}
func didLongPress() {
// send long press action
}
}
class PoorButton: TapProtocol {
func didTap() {
// send tap action
}
}
作為示例,我們使用具有可播放視頻的集合的應用程序。此應用程序的video類代表用戶集合的視頻:
class Video {
var title: String = “My Video”
var description: String = “This is a beautiful video”
var author: String = “Marco Santarossa”
var url: String = “https://marcosantadev.com/my_video”
var duration: Int = 60
var created: Date = Date()
var update: Date = Date()
}
我們將其注入視頻播放器:
func play(video: Video) {
// load the player UI
// load the content at video.url
// add video.title to the player UI title
// update the player scrubber with video.duration
}
不幸的是,我們在方法play中注入了太多的信息,因為它只需要url,title和duration。
您可以使用協議Playable解決此問題,只保留播放器需要的信息:
protocol Playable {
var title: String { get }
var url: String { get }
var duration: Int { get }
}
class Video: Playable {
var title: String = “My Video”
var description: String = “This is a beautiful video”
var author: String = “Marco Santarossa”
var url: String = “https://marcosantadev.com/my_video”
var duration: Int = 60
var created: Date = Date()
var update: Date = Date()
}
func play(video: Playable) {
// load the player UI
// load the content at video.url
// add video.title to the player UI title
// update the player scrubber with video.duration
}
這種方法對于單元測試非常有用。我們可以創建一個實現協議的骨架類Playable:
class StubPlayable: Playable {
private(set) var isTitleRead = false
var title: String {
self.isTitleRead = true
return “My Video”
}
var duration = 60
var url: String = “https://marcosantadev.com/my_video”
}
func test_Play_IsUrlRead() {
let stub = StubPlayable()
play(video: stub)
XCTAssertTrue(stub.isTitleRead)
}
“ - A.高級模塊不應該依賴于低級模塊,兩者都應該取決于抽象。
- B.抽象不應該依賴于細節,細節應該取決于抽象。“ –依賴反轉原則
如果您相信可重復使用的組件,則該原則是正確的。
DIP與開放封閉原則非常相似:使用這個原則,具有清晰的體系結構并用解耦依賴關系。可以通過抽象層來實現它。
我們來看一下類Handler,它將一個字符串保存在文件系統中。它在內部調用FilesystemManager來管理在文件系統中如何保存字符串:
class Handler {
let fm = FilesystemManager()
func handle(string: String) {
fm.save(string: string)
}
}
class FilesystemManager {
func save(string: String) {
// Open a file
// Save the string in this file
// Close the file
}
}
FilesystemManager是低級模塊,在其他項目中很容易重用。問題是高級模塊Handler,它不可重復使用,因為與FilesystemManager緊密耦合。我們應該能夠使用不同類型的存儲目標,如數據庫,云等來重用高級模塊。
我們可以使用協議Storage來解決這種依賴。以這種方式,Handler可以使用這個抽象協議,而不關心使用的具體存儲類型。通過這種方法,我們可以輕松地將文件系統更改為數據庫:
class Handler {
let storage: Storage
init(storage: Storage) {
self.storage = storage
}
func handle(string: String) {
storage.save(string: string)
}
}
protocol Storage {
func save(string: String)
}
class FilesystemManager: Storage {
func save(string: String) {
// Open a file in read-mode
// Save the string in this file
// Close the file
}
}
class DatabaseManager: Storage {
func save(string: String) {
// Connect to the database
// Execute the query to save the string in a table
// Close the connection
}
}
這種方法對測試非常有用。您可以輕松地使用一個骨架類(它實現了Storage),并且測試是否handle調用了注入Storage對象的方法save:
class StubStorage: Storage {
var isSavedCalled = false
func save(string: String) {
isSavedCalled = true
}
}
class HandlerTests {
func test_Handle_IsSaveCalled() {
let handler = Handler()
let stubStorage = StubStorage()
handler.handle(string: “test”, storage: stubStorage)
XCTAssertTrue(stubStorage.isSavedCalled)
}
}
如果您明智地遵循SOLID原則,您可以提高代碼的質量。此外,您的組件可以變得更加可維護和可重用。
掌握這些原則不是成為完美開發人員的后一步;其實這只是一個開始。您將不得不處理項目中的不同問題,了解佳方法,后檢查您是否違反了某些原則。
如果你有三個敵人要打敗:脆弱性,復用性差和僵化性。SOLID原則是你的武器。請享用!
本站文章版權歸原作者及原出處所有 。內容為作者個人觀點, 并不代表本站贊同其觀點和對其真實性負責,本站只提供參考并不構成任何投資及應用建議。本站是一個個人學習交流的平臺,網站上部分文章為轉載,并不用于任何商業目的,我們已經盡可能的對作者和來源進行了通告,但是能力有限或疏忽,造成漏登,請及時聯系我們,我們將根據著作權人的要求,立即更正或者刪除有關內容。本站擁有對此聲明的最終解釋權。