اصول SOLID، پنج اصل در برنامه نویسی شئگرا را بیان میکند:
این اصل بیان میکند که هر ماژول باید یک وظیفه و دلیل مشخص برای تغییر داشتهباشد. اصل تک وظیفهای با یک زیربنای کوچک و یک مورد مشخص مانند کلاس و یا یک آبجکتی که تنها یک هدف و کارائي دارد، شروع میشود. این اصل به شما کمک میکند که کلاسهایتان را تا حد امکان تمیز نگاه دارید.
یک مثال:
class Handler { func handle() { let data = requestDataToAPI() let array = parse(data: data) saveToDatabase(array: array) } private func requestDataToAPI() -> Data { // Network request and wait the response } private func parseResponse(data: Data) -> [String] { // Parse the network response into array } private func saveToDatabase(array: [String]) { // Save parsed response into database } }
در مثال بالا کلاس Handler وظایف مختلفی مانند برقراری یک فراخوانی شبکهای، پارس کردن جواب و ذخیرهسازی در دیتابیس را دارد.
شما میتوانید این مشکل را با سپردن مسئولیتها به کلاسهای کوچک برطرف نمائید.
class Handler { let apiHandler: APIHandler let parseHandler: ParseHandler let databaseHandler: 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) databaseHandler.saveToDatabase(array) } } class NetworkHandler { func requestDataToAPI() -> Data { // Network request and wait the response } } class ResponseHandler { func parseResponse(data: Data) -> [String] { // Parse the network response into array } } class DatabaseHandler { func saveToDatabase(array: [String]) { // Save parsed response into database } }
اگر بخواهیم این اصل را به صورت ساده بیان کنیم، به این معناست که برای گسترش باز است اما برای تغییرات بسته است.
یک کلاس به نام Logger داریم که آرایه Carها را تکرار میکند و جزئیات هر Car را پرینت میکند.
class Car { let name: String let color: String init(name: String, color: String) { self.name = name self.color = color } func printDetails() -> String { return "I have \(self.color) color \(self.name)." } } class Logger { func printData() { let cars = [ Car(name: "BMW", color: "Red"), Car(name: "Audi", color: "Black")] cars.forEach { car in print(car.printDetails()) } } }
اگر بخواهیم که امکان پرینت جزئیات را به یک کلاس جدید نیز اضافه کنیم، هربار که بخواهیم کلاس جدیدی اضافه کنیم مجبور به تغییر پیادهسازی تابع PrintData هستیم که این کار موجب شکستن اصل باز/بسته میشود.
class Bike { let name: String let color: String init(name: String, color: String) { self.name = name self.color = color } func printDetails() -> String { return "I have \(self.name) bike of color \(self.color)." } } class Logger { func printData() { let cars = [ Car(name: "BMW", color: "Red"), Car(name: "Audi", color: "Black")] cars.forEach { car in print(car.printDetails()) } let bikes = [ Bike(name: "Homda CBR", color: "Black"), Bike(name: "Triumph", color: "White")] bikes.forEach { bike in print(bike.printDetails()) } } }
ما میتوانیم این مشکل را با ساخت یک پروتوکل به نام Printable که توسط کلاسها پیادهسازی میشود، حل کنیم. در آخر تابع printData یک آرایهای از Printableها را پرینت خواهد کرد.
در این مسیر ما یک لایه انتزاعی بین printData و کلاس میسازیم، که به ما اجازه میدهد که کلاسهای دیگر مانند Bike را بدون تغییری در ساختار printData پرینت کنیم.
protocol Printable { func printDetails() -> String } 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 have \(self.color) color \(self.name)." } } class Bike: Printable { let name: String let color: String init(name: String, color: String) { self.name = name self.color = color } func printDetails() -> String { return "I have \(self.name) bike of color \(self.color)." } } class Logger { func printData() { let vehicles: [Printable] = [Car(name: "BMW", color: "Red"), Car(name: "Audi", color: "Black"), Bike(name: "Honda CBR", color: "Black"), Bike(name: "Triumph", color: "White")] vehicles.forEach { vehicle in print(vehicle.printDetails()) } }
توابعی که از نشانگرها و رفرنسها به کلاسهای پایه استفاده میکنند، باید قادر باشند آبجکتهای کلاسهای مشتق شده را بدون داشتن اطلاعاتی از آن استفاده کنند.
این اصل به شما کمک میکند از ارثبری بدون بهم ریختن آن استفاده کنید.
let requestKey: String = "NSURLRequestKey" // NSError subclass provide additional functionality but don't mess with original class. class RequestError: NSError { var request: NSURLRequest? { return self.userInfo[requestKey] as? NSURLRequest } } // I forcefully fail to fetch data and will return RequestError. func fetchData(request: NSURLRequest) -> (data: NSData?, error: RequestError?) { let userInfo: [String:Any] = [requestKey : request] return (nil, RequestError(domain:"DOMAIN", code:0, userInfo: userInfo)) } func willReturnObjectOrError() -> (object: AnyObject?, error: NSError?) { let request = NSURLRequest() let result = fetchData(request: request) return (result.data, result.error) } let result = willReturnObjectOrError() //RequestError if let requestError = result.error as? RequestError { requestError.request }
این اصل بیان میکند که کلاینتها نباید مجبور به پیادهسازی ارتباطاتی شوند که استفاده نمیکنند. به جای یک رابط چاق بهتر است که از چند رابط کوچک بسته به گروههای متدها استفاده کرد، هر کدام به یک زیرماژول خدمت رسانی میکند.
//We start with the protocol GestureProtocol with a method didTap: protocol GestureProtocol { func didTap() } //After some time, you have to add more gestures to the protocol protocol GestureProtocol { func didTap() func didDoubleTap()
اگر SuperButton تمام متدهای مورد نیازش را پیادهسازی کند.
class SuperButton: GestureProtocol { func didTap() { // Single tap operation } func didDoubleTap() { // double tap operation } func didLongPress() { // long press operation } } //But if implement Double Tab Button it implement all the action class DoubleTapButton: GestureProtocol { func didTap() { // Single tap operation } func didDoubleTap() { // double tap operation } func didLongPress() { // long press operation } }
کلاس DoubleTapButton اکشنهای didTap و didLongPress را نیاز ندارد. در اینجا قاعده تفکیک روابط رعایت نشده است. ما میتوانی این مشکل را با استفاده از پروتکلهای کوچک به جای پروتکلهای بزرگ برطرف نمائیم.
protocol TapProtocol { func didTap() } protocol DoubleTapProtocol { func didDoubleTap() } protocol LongPressProtocol { func didLongPress() } class SuperButton: TapProtocol, DoubleTapProtocol, LongPressProtocol { func didTap() { // Single tap operation } func didDoubleTap() { // double tap operation } func didLongPress() { // long press operation } } class DoubleTapButton: DoubleTapProtocol { func didDoubleTap() { // double tap operation } }
ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند، همچنین هردو نباید به Abstractionها وابسته باشند. (Abstractionها نباید به جزئیات و جزئیات نباید به Abstractionها وابستگی داشته باشند.)
class FileSystemManager { func save(string: String) { // Open a file // Save the string in this file // Close the file } } class Handler { let fileManager = FilesystemManager() func handle(string: String) { fileManager.save(string: string) } }
ماژول FileSystemManager یک ماژول سطح پائین است و استفاده دوباره از آن در دیگر پروژهها ساده است. مشکل اینجاست که ماژول سطح بالای Handler قابل استفاده دوباره نیست چرا که با FileSystemManager جفت شده است. ما باید بتوانیم از ماژولهای سطح بالا با انواع مختلف حافظه ذخیرهسازی دوباره استفاده کنیم مانند دیتابیس، cloud و غیره.
ما میتوانیم با استفاده از پروتکل Storage این وابستگی را برطرف کنیم. در این راه Handler میتواند از این پروتکل abstract بدون نیاز به حمل نوع حافظه استفاده کند. با این رویکرد ما میتوانیم به راحتی از یک filesystem به یک دیتابیس تغییر وضعیت دهیم.
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 } } class Handler { let storage: Storage // Storage types init(storage: Storage) { self.storage = storage } func handle(string: String) { storage.save(string: string) } }
اصل وارونگی وابستگی به اصل باز/بسته از نظر راه حل، هدف داشتن یک معماری تمیز و عدم جفت شدگی وابستگیها، بسیار شباهت دارد.
مقاله اصلی:
منابع مقاله اصلی: