Các thể loại protocol trong Swift.
Bài đăng này đã không được cập nhật trong 4 năm
-
Vai trò chủ yếu của
protocol
/interface
là cho phép cácabstraction
chung được xác định trên các triển khai cụ thể . Một kỹ thuật được gọi làpolymorphism
cho phép chúng taswap
/morph
implement của mình mà không ảnh hưởng đến API được công khai. -
Mặc dù
Swift
đã hỗ trợ đầy đủ chopolymorphism
dựa trêninterface
nhưng cácprotocol
vẫn đóng vai trò lớn trong thiết kế tổng thể củalanguage
vàlib
như một phần chức năng chính mà Swift triển khai trực tiếp phía trên các protocol khác. -
Thiết kế
protocol-oriented design
cho phép chúng ta sử dụng cácprotocol
theo nhiều cách khác nhau trongsource code
. Chúng ta cùng lướt qua các cách đó và cả hai hãy xem cách Apple sử dụng cácprotocol
của họ.
1/ Enabling unified actions:
- Bắt đầu bằng cách xem các
protocol
yêu cầu cáctype
phù hợp để có thể thực hiện cácaction
cụ thể. Ví dụ:protocol Equatable
được sử dụng để đánh dấu rằng mộttype
có thể thực hiện kiểm tra đẳng thức giữa hai trường hợp, trong khiprotocol Hashable
được chấp nhận bởi cáctype
có thể đượchashed
:
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
protocol Hashable: Equatable {
func hash(into hasher: inout Hasher)
}
-
Lợi ích lớn của hai khả năng đó được cụ thể hóa bằng cách sử dụng hệ thống (thay vì được mã hóa vào trình biên dịch) là nó cho phép chúng ta viết
code
chung ràng buộc với cácprotocol
từ đó cho phép chúng ta sử dụng đầy đủ của những khả năng trongcode
đó. -
Cách thức chúng ta có thể mở rộng
Array
bằng mộtmethod
giúp chúng ta đếm tất cả các lần xuất hiện của mộtvalue
với điều kiện là kiểuElement
mảng phù hợp vớiEquatable
:
extension Array where Element: Equatable {
func numberOfOccurences(of value: Element) -> Int {
reduce(into: 0) { count, element in
// We can check whether two values are equal here
// since we have a guarantee that they both conform
// to the Equatable protocol:
if element == value {
count += 1
}
}
}
}
-
Bất cứ khi nào xác định
protocol
dựa trên cácaction
chúng ta nên làm cho cácprotocol
đó chung chung nhất có thể (giống nhưEquitable
vàHashable
) vì chúng vẫn tập trung vào chính các hành động thay vì ràng buộc với têndomain
cụ thể. -
VD: Nếu chúng ta muốn thống nhất một số loại tải các đối tượng hoặc giá trị khác nhau, chúng ta có thể định nghĩa một
protocol
có thể tải với một loại liên quan - sẽ cho phép mỗi loại tuân thủ khai báo loại kết quả mà nó tải:
protocol Loadable {
associatedtype Result
func load() throws -> Result
}
Tuy nhiên không phải mọi protocol
đều định nghĩa các action
. Trong khi tên của protocol
:
protocol Cachable: Codable {
var cacheKey: String { get }
}
-
So sánh
protocol Codable
vàCachable
ta xác định các hành động cho cảencode
vàdecode
. -
Không phải tất cả các
protocol
đều cần sử dụngsuffix
. Việc ràng buộcsuffix
vào bất kỳ danh từ cụ thể nào chỉ để xác địnhprotocol
có thể dẫn đến khá nhiều nhầm lẫn:
protocol Titleable {
var title: String { get }
}
- Khó hiểu hơn nữa là khi sử dụng
suffix
có thể tạo ra một tên có ý nghĩa hoàn toàn khácmong muốn. Ví dụ, chúng ta đã định nghĩa mộtprotocol
với mục đích hoạt động như mộtAPI
cho cáccolor container
nhưng tên lại gợi ý rằng nó có thể được tô màu cho cáctype
mà chính chúng có thể được tô màu:
protocol Colorable {
var foregroundColor: UIColor { get }
var backgroundColor: UIColor { get }
}
- Chúng ta có thể cải thiện một số các
protocol
này cả về cách đặt tên cũng như cách chúng có cấu trúc. Bằng cách bước ra khỏi mục một và xem một vài cách khác nhau để xác định cácprotocol
trongSwift
.
2/ Defining requirements:
- Mục số hai dành cho các
protocol
được sử dụng để xác định các yêu cầu chính thức cho mộttype
đối tượng hoặcAPI
nhất định. Trongstandard library
, cácprotocol
được sử dụng để xác định ý nghĩa củaCollection
,Numberic
hoặcSequence
:
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
}
- Định nghĩa trên của
Sequence
cho chúng ta biết rằng vai trò chính của bất kỳ Swiftsequence
nào (chẳng hạn nhưArray
,Dictionary
hoặcRange
) hoạt động nhưfactory
để tạo các vòng lặpđược chính thức hóa thông qua các điều sau đâyprotocol
:
protocol IteratorProtocol {
associatedtype Element
mutating func next() -> Element?
}
- Với hai
protocol
trên hãy quay trở lại cácprotocol
có thể lưu và có thể tạo màu mà chúng ta đã xác định trước đó, để xem liệu chúng có thể được cải thiện hay không bằng cách chuyển đổi chúng thành các định nghĩa yêu cầu thay thế.
protocol ColorProvider {
var foregroundColor: UIColor { get }
var backgroundColor: UIColor { get }
}
- Tương tự như vậy, chúng ta có thể đổi tên
Cachabl
thành:
protocol CachingProtocol: Codable {
var cacheKey: String { get }
}
- Hãy cùng chuyển mã tạo khóa của chúng ta thành các loại riêng biệt - sau đó chúng ta có thể chính thức hóa các yêu cầu để sử dụng
protocol CacheKeyGenerator
:
protocol CacheKeyGenerator {
associatedtype Value: Codable
func cacheKey(for value: Value) -> String
}
3/ Type conversions:
- Chúng ta hãy xem các
protocol
được sử dụng để khai báo rằng một loại có thể chuyển đổi sang và từ cácvalue
khác. Chúng ta lại bắt đầu với một ví dụ từstandard library
làCustomStringConvertible
được sử dụng để cho phép bất kỳ loại nào được chuyển đổi thànhstring
mô tả:
protocol CustomStringConvertible {
var description: String { get }
}
-
Kiểu viết đó đặc biệt hữu ích khi chúng ta có thể trích xuất một phần
dât
từ nhiềutype
hoàn toàn phù hợp với mục đích củaprotocol
Titleable
. -
Bằng cách đổi tên
protocol
đó thànhTitleConvertible
chúng không chỉ dễ hiểu hơn mà còn làm chocode
của chúng ta phù hợp hơn vớistandard library
:
protocol TitleConvertible {
var title: String { get }
}
- Các
protocol
chuyển đổitype
cũng có thể sử dụng cácmethod
thay vì cácproperty
. Điều này thường phù hợp hơn khi chúng ta muốn triển khai các yêu cầu tính toán hợp lý :
protocol ImageConvertible {
// Since rendering an image can be a somewhat expensive
// operation (depending on the type being rendered), we're
// defining our protocol requirement as a method, rather
// than as a property:
func makeImage() -> UIImage
}
- Chúng ta cũng có thể sử dụng loại
protocol
này để cho phép cáctype
nhất định theo các cách khác nhau:
protocol ExpressibleByArrayLiteral {
associatedtype ArrayLiteralElement
init(arrayLiteral elements: ArrayLiteralElement...)
}
protocol ExpressibleByNilLiteral {
init(nilLiteral: ())
}
- Ví dụ: Cách thức chúng ta có thể xác định
protocol`` ExpressibleByUUID
cho các loại định danh có thể được tạo bằngUUID
:
protocol ExpressibleByUUID {
init(uuid: UUID)
}
4/ Abstract interfaces:
-
Cuối cùng hãy để xem có lẽ cách sử dụng
protocol
phổ biến nhất trongcode
của bên thứ ba để xác địnhabstract
để giao tiếp với nhiềutype
cơ bản. -
Một ví dụ có thể được tìm thấy trong
Apple Metal framework
đó làAPI
lập trình đồ họalow level
. Vì GPU thay đổi rất nhiều giữa các thiết bị vàMetal
nhằm mục đích cung cấpAPI
phù hợp mọi loại phần cứng mà nó hỗ trợ, nên nó sử dụng mộtprotocol
để xác địnhAPI
:
protocol MTLDevice: NSObjectProtocol {
var name: String { get }
var registryID: UInt64 { get }
...
}
- Khi sử dụng
Metal
chúng ta có thể gọi hàmMTLCreateSystemDefaultDevice
và hệ thống sẽ trả về việc thực hiệnprotocol
phù hợp với thiết bị màcode
của chúng ta hiện đang chạy:
func MTLCreateSystemDefaultDevice() -> MTLDevice?
- Ví dụ: chúng ta có thể xác định
protocol
NetworkEngine
để tách rời cách chúng ta thực hiện cácnetwork call
từ bất kỳ phương tiệnnetwork
nào:
protocol NetworkEngine {
func perform(
_ request: NetworkRequest,
then handler: @escaping (Result<Data, Error>) -> Void
)
}
- Chúng ta hiện có thể tự do định nghĩa số lượng triển khai
network
cơ bản mà chúng ta cần - ví dụ: một ứng dụng dựa trênURLSession
để sản xuất và một phiên bản giả định để thử nghiệm:
extension URLSession: NetworkEngine {
func perform(
_ request: NetworkRequest,
then handler: @escaping (Result<Data, Error>) -> Void
) {
...
}
}
struct MockNetworkEngine: NetworkEngine {
var result: Result<Data, Error>
func perform(
_ request: NetworkRequest,
then handler: @escaping (Result<Data, Error>) -> Void
) {
handler(result)
}
}
All rights reserved