Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
POP
Written by chuotfx on March 19, 2024

Lập trình hướng giao thức (POP) với Swift

iOS & Swift . Tutorials

Contents

  • Chuẩn bị
  • POP – Lập trình hướng giao thức là gì?
    • Khái niệm chung về POP
    • Tính chất
    • Các tương tác với POP
    • Ví dụ
  • OOP vs. POP
    • Ưu nhược điểm
    • Diamond Problem
    • OOP to POP
    • Ví dụ
  • Protocol Conform
  • Enum, Struct & Class with Protocol
  • Protocol Extensions
  • Enumerations with Associated Values
  • Enumerations as a Replacement for Classes
  • Protocol Inheritance
  • Protocol Types
  • Protocol Checking and Casting
  • Protocols with Associated Types
  • Protocols as Generic Constraints
  • Protocols with Self Requirements
  • Protocols with Initializer Requirements
  • Tạm kết

Chào mừng bạn đến với Fx Studio. Chủ đề bài viết lần này sẽ vừa quen vừa lạ. Đó là Lập trình hướng giao thức (Protocol-oriented programming – POP). Đây được xem là trái tim của ngôn ngữ lập trình Swift. Về cốt lõi, Swift được Apple xây dựng theo hướng giao thức để có tính linh hoạt cao. Còn vừa quen và vừa lạ như thế nào, thì bạn sẽ tìm hiểu ở các phần dưới đây.

Nếu mọi việc đã ổn rồi, thì …

Bắt đầu thôi!

Chuẩn bị

Đây thuộc phần kiến thức nâng cao. Do đó, bạn cũng cần chuẩn bị kha khá kiến thức để tiếp thu được. Để dễ tiếp cận hơn, mình sẽ liệt kê vài bài cơ bản trước nhóe!

    • Basic Swift trong 10 phút
    • Lập trình hướng đối tượng (OOP) với Swift
    • Protocol trong 10 phút
    • Generics trong 10 phút – Swift

Ngoài lượng kiến thức nhiều và để bắt đầu bạn cũng cần chuẩn bị thêm …

Một tâm hồn đẹp!

POP – Lập trình hướng giao thức là gì?

Khái niệm chung về POP

Lập trình hướng giao thức (Protocol-oriented programming – POP) trong Swift là một phương pháp lập trình. Mà ở đó bạn xây dựng chương trình dựa trên “giao thức” (protocols) hơn là dựa trên lớp (classes) hoặc cấu trúc (structures). Trong đó các giao thức (protocols) đóng vai trò chính.

Đây là một khái niệm phổ biến trong ngôn ngữ lập trình Swift của Apple.

Trong lập trình hướng giao thức, thay vì xác định lớp cơ sở và kế thừa từ nó (như trong lập trình hướng đối tượng). Bạn xác định một giao thức mà các đối tượng hoặc cấu trúc phải tuân theo. Giao thức định nghĩa một “hợp đồng” mà các class, struct, enum phải thực hiện.

Điều này cho phép bạn tạo ra các đối tượng có thể tương tác với nhau. Mà không cần biết chi tiết cụ thể về cách chúng được thực hiện. Điều này giúp tăng tính linh hoạt và tái sử dụng của mã nguồn.

Tính chất

Lập trình hướng giao thức (Protocol-oriented programming – POP) trong Swift có một số tính chất đặc trưng sau:

  1. Tính linh hoạt và tái sử dụng cao: Giao thức có thể được tuân thủ bởi nhiều lớp, cấu trúc hoặc kiểu liệt kê, giúp tăng tính tái sử dụng và linh hoạt của mã nguồn.
  2. Triển khai mặc định: Swift cho phép bạn mở rộng giao thức để cung cấp một triển khai mặc định cho các phương thức, thuộc tính tính toán, và các yêu cầu chỉ mục.
  3. Thừa kế giao thức: Một giao thức có thể thừa kế từ một hoặc nhiều giao thức khác và có thể thêm các yêu cầu mới.
  4. Kiểu giao thức: Bạn có thể sử dụng giao thức như một kiểu đầy đủ trong Swift.
  5. Kiểm tra và ép kiểu giao thức: Swift cung cấp các toán tử để kiểm tra xem một thể hiện có tuân thủ một giao thức cụ thể hay không, và để ép kiểu giữa các giao thức.
  6. Giao thức với yêu cầu chỉ mục: Các giao thức có thể định nghĩa các yêu cầu chỉ mục, tạo ra một khái niệm về “giao thức chung”.
  7. Giao thức như một yêu cầu chung: Bạn có thể sử dụng giao thức như một ràng buộc chung trong các hàm và kiểu chung.
  8. Giao thức với các yêu cầu Self: Một số giao thức yêu cầu các phương thức hoặc thuộc tínhtrả về kiểu của chính đối tượng thực hiện giao thức, được biểu thị bằng từ khóa Self .
  9. Giao thức với các yêu cầu khởi tạo: Các giao thức có thể yêu cầu các lớp tuân thủ phải cung cấp một hoặc nhiều khởi tạo cụ thể.

Các tương tác với POP

  1. Protocols: Đây là khái niệm cốt lõi của POP. Một giao thức định nghĩa một “hợp đồng” mà class, struct, enum phải tuân theo.
  2. Protocol Extensions: mở rộng giao thức để cung cấp một triển khai mặc định cho các phương thức, thuộc tính tính toán và các yêu cầu chỉ mục.
  3. Enumerations with Associated Values: định nghĩa các kiểu liệt kê có thể lưu trữ các giá trị khác nhau và các kiểu dữ liệu khác nhau.
  4. Enumerations as a Replacement for Classes: Bạn có thể sử dụng kiểu liệt kê thay vì lớp để tạo ra các đối tượng có trạng thái và hành vi.
  5. Protocol Inheritance: Một giao thức có thể thừa kế từ một hoặc nhiều giao thức khác và có thể thêm các yêu cầu mới.
  6. Protocol Types: Bạn có thể sử dụng giao thức như một kiểu đầy đủ trong Swift.
  7. Protocol Checking and Casting: Swift cung cấp các toán tử để kiểm tra xem một thể hiện có tuân thủ một giao thức cụ thể hay không và để ép kiểu giữa các giao thức.
  8. Protocols with Associated Types: Các giao thức có thể định nghĩa các yêu cầu chỉ mục. Tạo ra một khái niệm về “giao thức chung”.
  9. Protocols as Generic Constraints: Bạn có thể sử dụng giao thức như một ràng buộc chung trong các hàm và kiểu chung.
  10. Protocols with Self Requirements: Một số giao thức yêu cầu các phương thức hoặc thuộc tính trả về kiểu của chính đối tượng thực hiện giao thức. Được biểu thị bằng từ khóa Self .
  11. Protocols with Initializer Requirements: Các giao thức có thể yêu cầu các lớp tuân thủ phải cung cấp một hoặc nhiều khởi tạo cụ thể.

Ví dụ

Dưới đây là một ví dụ về lập trình hướng giao thức trong Swift:

// Định nghĩa một giao thức
protocol Flyable {
    var airspeedVelocity: Double { get }
}

// Mở rộng giao thức để cung cấp một triển khai mặc định
extension Flyable {
    var airspeedVelocity: Double {
        return 1000.0
    }
}

// Định nghĩa một lớp tuân thủ giao thức
class Bird: Flyable {
    var airspeedVelocity: Double {
        return 500.0
    }
}

// Sử dụng giao thức như một kiểu
func race(competitor: Flyable) {
    print("Racing at speed: \(competitor.airspeedVelocity)")
}

let bird = Bird()
race(competitor: bird) // In ra: "Racing at speed: 500.0"

Trong ví dụ trên,

  • Chúng ta đã định nghĩa một giao thức Flyable với một thuộc tính airspeedVelocity.
  • Chúng ta sau đó mở rộng giao thức này để cung cấp một triển khai mặc định cho airspeedVelocity.
  • Lớp Bird tuân thủ giao thức Flyable và cung cấp một triển khai riêng của airspeedVelocity.
  • Cuối cùng, chúng ta định nghĩa một hàm race nhận một đối số kiểu Flyable, cho phép chúng ta sử dụng giao thức như một kiểu đầy đủ.

Tìm hiểu giữa Lập trình hướng đối tượng (OOP) & Lập trình hướng giao thức (POP) như thế nào nhóe!

OOP vs. POP

Ưu nhược điểm

OOP:

  1. Tính đóng gói: Dữ liệu và phương thức được gói gọn trong các đối tượng. Điều này giúp giảm sự phức tạp và tăng tính tái sử dụng.
  2. Tính kế thừa: Cho phép lớp con kế thừa các thuộc tính và phương thức của lớp cha. Tuy nhiên, điều này có thể dẫn đến “vấn đề kim cương” (Diamond Problem). Khi một lớp kế thừa từ nhiều lớp có phương thức giống nhau.
  3. Tính đa hình: Cho phép một đối tượng được xem như một đối tượng của lớp khác.

POP:

  1. Tính linh hoạt và tái sử dụng cao: Giao thức có thể được tuân thủ bởi nhiều lớp, cấu trúc hoặc kiểu liệt kê. Giúp tăng tính tái sử dụng và linh hoạt của mã nguồn.
  2. Triển khai mặc định: Swift cho phép bạn mở rộng giao thức để cung cấp một triển khai mặc định cho các phương thức, thuộc tính tính toán, và các yêu cầu chỉ mục.
  3. Thừa kế giao thức: Một giao thức có thể thừa kế từ một hoặc nhiều giao thức khác và có thể thêm các yêu cầu mới.
  4. Không có “vấn đề kim cương”: Vì Swift không hỗ trợ đa kế thừa cho lớp. Nên “vấn đề kim cương” không tồn tại trong POP.

Tóm lại, POP có thể giúp giải quyết một số vấn đề của OOP như “vấn đề kim cương”. Và cung cấp một cấu trúc linh hoạt hơn cho mã nguồn của bạn. Tuy nhiên, lựa chọn giữa OOP và POP phụ thuộc vào yêu cầu cụ thể của dự án của bạn.

Diamond Problem

Trong lập trình hướng đối tượng, Diamond Problem là một vấn đề phát sinh khi một lớp kế thừa từ hai lớp khác. Và cả hai lớp đó lại kế thừa từ một lớp chung. Điều này dẫn đến một mô hình thừa kế hình kim cương (diamond-shaped). Gây ra nhầm lẫn về việc phương thức hoặc thuộc tính nào nên được kế thừa khi có sự trùng lặp.

Trong Swift, Diamond Problem không phải là vấn đề bởi vì Swift không hỗ trợ đa thừa kế cho lớp. Thay vào đó, Swift sử dụng thừa kế giao thức (Protocol Inheritance) và mở rộng giao thức (Protocol Extensions) để cung cấp tính năng tương tự như đa thừa kế mà không gặp phải Diamond Problem.

Khi một kiểu tuân thủ nhiều giao thức hoặc một giao thức kế thừa từ nhiều giao thức khác. Nếu có sự trùng lặp về phương thức hoặc thuộc tính, Swift sẽ tuân theo quy tắc:

  • Nếu kiểu cung cấp triển khai của riêng nó, triển khai đó sẽ được sử dụng.
  • Nếu không, Swift sẽ chọn triển khai từ giao thức mở rộng mà kiểu tuân thủ cuối cùng.
protocol A {
    func foo()
}

extension A {
    func foo() {
        print("A foo")
    }
}

protocol B: A {
    func foo()
}

extension B {
    func foo() {
        print("B foo")
    }
}

protocol C: A {
    func foo()
}

extension C {
    func foo() {
        print("C foo")
    }
}

struct D: B, C {}

let d = D()
d.foo() // What will this print?

Ví dụ nhóe!

OOP to POP

Chuyển đổi sang lập trình hướng giao thức (POP) có thể được thực hiện theo các bước sau:

  1. Xác định các giao thức: Xác định các giao thức mà các lớp của bạn sẽ tuân thủ. Các giao thức này nên đại diện cho các hành vi mà bạn muốn các đối tượng của bạn có.
  2. Triển khai giao thức: Đối với mỗi lớp, hãy thay thế các thuộc tính và phương thức cụ thể của lớp bằng các yêu cầu giao thức tương ứng.
  3. Sử dụng triển khai mặc định: Nếu có các phương thức hoặc thuộc tính mà nhiều lớp có cùng cách triển khai, hãy xem xét việc sử dụng triển khai mặc định trong giao thức.
  4. Sử dụng thừa kế giao thức: Nếu có các giao thức chia sẻ các yêu cầu tương tự, hãy xem xét việc sử dụng thừa kế giao thức để giảm bớt sự trùng lặp.
  5. Sử dụng kiểu giao thức: Thay thế các tham chiếu cụ thể đến lớp bằng các tham chiếu đến giao thức, nếu có thể.
  6. Kiểm tra và ép kiểu giao thức: Sử dụng kiểm tra kiểu và ép kiểu giao thức khi cần xử lý các đối tượng như là thể hiện của giao thức cụ thể.
  7. Sử dụng giao thức như một yêu cầu chung: Khi làm việc với các hàm và kiểu chung, hãy xem xét việc sử dụng giao thức như một ràng buộc chung.

Lưu ý rằng việc chuyển đổi từ OOP sang POP có thể đòi hỏi một số thay đổi lớn đối với cấu trúc của mã nguồn của bạn, và có thể không phải lúc nào cũng là lựa chọn tốt nhất tùy thuộc vào yêu cầu cụ thể của dự án của bạn.

Ví dụ

Dưới đây là một ví dụ về cách chuyển đổi một lớp của lập trình hướng đối tượng sang lập trình hướng giao thức trong Swift.

Giả sử chúng ta có một lớp Bird trong lập trình hướng đối tượng như sau:

class Bird {
    var name: String
    var canFly: Bool

    init(name: String, canFly: Bool) {
        self.name = name
        self.canFly = canFly
    }

    func fly() {
        if canFly {
            print("\(name) can fly")
        } else {
            print("\(name) can't fly")
        }
    }
}

let penguin = Bird(name: "Penguin", canFly: false)
penguin.fly() // Prints "Penguin can't fly"

Chúng ta có thể chuyển đổi nó sang lập trình hướng giao thức như sau:

protocol Bird {
    var name: String { get set }
    var canFly: Bool { get }
}

protocol Flyable {
    func fly()
}

extension Bird where Self: Flyable {
    func fly() {
        print("\(name) can fly")
    }
}

struct Penguin: Bird {
    var name: String
    var canFly: Bool {
        return false
    }
}

struct Eagle: Bird, Flyable {
    var name: String
    var canFly: Bool {
        return true
    }
}

let penguin = Penguin(name: "Penguin")
let eagle = Eagle(name: "Eagle")
eagle.fly() // Prints "Eagle can fly"

Trong ví dụ trên,

  • Chúng ta đã tạo ra hai giao thức Bird và Flyable.
  • Bird định nghĩa các thuộc tính cơ bản của một con chim, trong khi Flyable định nghĩa hành vi bay.
  • Chúng ta sau đó tạo ra hai cấu trúc Penguin và Eagle tuân theo các giao thức này.
  • Eagle còn tuân theo thêm giao thức Flyable nữa, cho phép nó bay.

Dưới đây là phần trình bày những gì mà bạn làm được với POP.

Protocol Conform

Khi một kiểu tuân thủ một giao thức, nó phải cung cấp triển khai cho tất cả các yêu cầu mà giao thức đó định nghĩa. Điều này được gọi là “conform” đến giao thức. Dưới đây là một ví dụ về việc sử dụng giao thức trong Swift:

// Định nghĩa một giao thức
protocol CanMakeNoise {
    func makeNoise()
}

// Định nghĩa một lớp tuân thủ giao thức
class Human: CanMakeNoise {
    func makeNoise() {
        print("Hello!")
    }
}

// Định nghĩa một cấu trúc tuân thủ giao thức
struct Cat: CanMakeNoise {
    func makeNoise() {
        print("Meow!")
    }
}

// Định nghĩa một kiểu liệt kê tuân thủ giao thức
enum Dog: CanMakeNoise {
    case small, big

    func makeNoise() {
        switch self {
        case .small:
            print("Woof!")
        case .big:
            print("WOOF!")
        }
    }
}

let human = Human()
let cat = Cat()
let dog = Dog.big

human.makeNoise() // In ra: "Hello!"
cat.makeNoise()   // In ra: "Meow!"
dog.makeNoise()   // In ra: "WOOF!"

Trong ví dụ trên,

  • Chúng ta đã định nghĩa một giao thức CanMakeNoise với một phương thức makeNoise.
  • Chúng ta sau đó định nghĩa một lớp Human, một cấu trúc Cat, và một kiểu liệt kê Dog
  • Tất cả đều tuân thủ giao thức CanMakeNoise và cung cấp một triển khai riêng của phương thức makeNoise.

Enum, Struct & Class with Protocol

Trong Swift, bạn có thể áp dụng lập trình hướng giao thức vào Enum, Struct và Class. Dưới đây là một ví dụ:

// Định nghĩa giao thức
protocol CanFly {
    func fly()
}

// Tạo Enum Bird tuân theo giao thức CanFly
enum Bird: CanFly {
    case eagle, penguin, swift

    func fly() {
        switch self {
        case .eagle:
            print("The eagle is flying")
        case .penguin:
            print("Penguins can't fly!")
        case .swift:
            print("The swift bird is flying")
        }
    }
}

// Tạo Struct Airplane tuân theo giao thức CanFly
struct Airplane: CanFly {
    func fly() {
        print("The airplane is flying")
    }
}

// Tạo Class Helicopter tuân theo giao thức CanFly
class Helicopter: CanFly {
    func fly() {
        print("The helicopter is flying")
    }
}

// Tạo một hàm để thử nghiệm
func letItFly(flyer: CanFly) {
    flyer.fly()
}

// Tạo một đối tượng Bird, Airplane và Helicopter
let eagle = Bird.eagle
let airplane = Airplane()
let helicopter = Helicopter()

// Gọi hàm letItFly với eagle, airplane và helicopter
letItFly(flyer: eagle)
letItFly(flyer: airplane)
letItFly(flyer: helicopter)

Trong ví dụ trên,

  • Chúng ta đã tạo ra một giao thức CanFly và một Enum Bird, một Struct Airplane và một Class Helicopter tuân theo giao thức này.
  • Hàm letItFly nhận vào một đối tượng tuân theo giao thức CanFly và gọi phương thức fly của đối tượng đó.
  • Khi chúng ta gọi hàm letItFly với eagle, airplane và helicopter, chúng ta thấy rằng cả ba đều có thể “bay”, mặc dù chúng được thực hiện theo cách khác nhau.

Protocol Extensions

Mở rộng giao thức (Protocol Extensions) là một tính năng mạnh mẽ của Swift. Nó cho phép bạn mở rộng một giao thức để cung cấp một triển khai mặc định cho các phương thức, thuộc tính tính toán, và các yêu cầu chỉ mục. Điều này có nghĩa là bạn có thể định nghĩa một giao thức với các phương thức và thuộc tính. Sau đó cung cấp một triển khai mặc định cho chúng trong một mở rộng giao thức. Các class, struct, enum tuân thủ giao thức có thể sử dụng triển khai mặc định hoặc cung cấp triển khai riêng của chúng.

Dưới đây là một ví dụ về việc sử dụng mở rộng giao thức trong Swift:

// Định nghĩa một giao thức
protocol CanFly {
    var altitude: Double { get }
    func ascend()
}

// Mở rộng giao thức để cung cấp một triển khai mặc định
extension CanFly {
    var altitude: Double {
        return 1000.0
    }

    func ascend() {
        print("Ascending to \(altitude) feet.")
    }
}

// Định nghĩa một lớp tuân thủ giao thức
class Bird: CanFly {
    // Không cần cung cấp triển khai cho 'altitude' và 'ascend' vì đã có triển khai mặc định từ giao thức
}

let bird = Bird()
bird.ascend() // In ra: "Ascending to 1000.0 feet."

Trong ví dụ trên,

  • Chúng ta đã định nghĩa một giao thức CanFly với một thuộc tính altitude và một phương thức ascend.
  • Chúng ta sau đó mở rộng giao thức này để cung cấp một triển khai mặc định cho altitude và ascend.
  • Lớp Bird tuân thủ giao thức CanFly nhưng không cần cung cấp triển khai cho altitude và ascend vì đã có triển khai mặc định từ giao thức.

Enumerations with Associated Values

Kiểu liệt kê với các giá trị liên kết là một cách mạnh mẽ để mô hình hóa các loại dữ liệu. Trong POP, chúng có thể được sử dụng để tạo ra các kiểu dữ liệu mà mỗi trường hợp của nó có thể chứa các giá trị khác nhau.

Trong Swift, associatedtype được sử dụng trong giao thức để khai báo một kiểu placeholder. Kiểu thực tế sẽ được xác định bởi lớp, cấu trúc hoặc kiểu liệt kê cụ thể tuân thủ giao thức đó.

Khi một giao thức chứa một hoặc nhiều định nghĩa associatedtype, nó sẽ trở thành giao thức có kiểu liên kết. Giao thức này không thể được sử dụng như một kiểu đầy đủ. Thay vào đó, nó chỉ có thể được sử dụng như một kiểu giao thức (protocol type) khi các associatedtype đã được xác định. Ví dụ code:

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Triển khai giao thức với một cấu trúc như sau.

struct IntContainer: Container {
    typealias Item = Int
    var items = [Int]()
    
    mutating func append(_ item: Int) {
        items.append(item)
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

var container = IntContainer()
container.append(12)
print(container.count) // Prints "1"
print(container[0]) // Prints "12"

Dưới đây là một ví dụ về việc sử dụng Enumerations với Associated Types và tuân thủ một Protocol:

enum Stack<Element>: Container {
    case empty
    indirect case node(Element, next: Stack<Element>)

    mutating func append(_ item: Element) {
        self = .node(item, next: self)
    }

    var count: Int {
        switch self {
        case .empty: return 0
        case let .node(_, next): return next.count + 1
        }
    }

    subscript(i: Int) -> Element {
        switch self {
        case .empty: fatalError("Index out of range")
        case let .node(x, next):
            if i == 0 {
                return x
            } else {
                return next[i - 1]
            }
        }
    }
}

var stack = Stack<Int>.empty
stack.append(10)
stack.append(20)
print(stack.count) // Prints "2"
print(stack[1]) // Prints "10"

Trong ví dụ trên,

  • ItemRepresentable là một giao thức có một associatedtype được gọi là Item và một phương thức represent.
  • IntOrString là một kiểu liệt kê với hai trường hợp: intItem và stringItem
  • Mỗi trường hợp có một giá trị liên kết khác nhau và tuân thủ giao thức ItemRepresentable.

Enumerations as a Replacement for Classes

Kiểu liệt kê như một thay thế cho lớp là một khái niệm trong lập trình hướng giao thức.

Trong Swift, enumerations không chỉ là một danh sách các giá trị. Chúng cũng có thể có các phương thức và có thể tuân thủ các giao thức, giống như classes và structures. Điều này cho phép chúng được sử dụng như một thay thế linh hoạt cho lớp trong một số trường hợp.

Ví dụ, bạn có thể sử dụng kiểu liệt kê để mô tả các đối tượng có một số lượng cố định các trạng thái có thể xảy ra. Mỗi trạng thái có thể được biểu diễn bởi một giá trị của kiểu liệt kê. Và các hành vi liên quan đến trạng thái có thể được định nghĩa bằng các phương thức của kiểu liệt kê.

Điều này tạo ra một cách tiếp cận mạnh mẽ và linh hoạt để mô tả trạng thái & hành vi trong mã của bạn. Mà không cần phải sử dụng lớp và kế thừa.

Dưới đây là một ví dụ về việc sử dụng kiểu liệt kê như một thay thế cho lớp trong Swift:

protocol CanMakeNoise {
    func makeNoise()
}

enum Animal: CanMakeNoise {
    case cat, dog, cow

    func makeNoise() {
        switch self {
        case .cat:
            print("Meow!")
        case .dog:
            print("Woof!")
        case .cow:
            print("Moo!")
        }
    }
}

let animals: [CanMakeNoise] = [Animal.cat, Animal.dog, Animal.cow]
animals.forEach { $0.makeNoise() }

Trong ví dụ này,

  • Animal là một kiểu liệt kê tuân thủ giao thức CanMakeNoise.
  • Mỗi trường hợp của Animal biểu diễn một loại động vật khác nhau và cách chúng phát ra tiếng ồn.
  • Khi chúng ta gọi phương thức makeNoise() trên một đối tượng Animal. Nó sẽ in ra tiếng ồn phù hợp với loại động vật đó.

Protocol Inheritance

Thừa kế giao thức cho phép một giao thức có thể thừa kế từ một hoặc nhiều giao thức khác. Điều này giống như thừa kế lớp trong OOP, nhưng cho giao thức.

Dưới đây là một ví dụ về thừa kế giao thức:

protocol Printable {
    func printDetails()
}

protocol Loggable {
    func logDetails()
}

// Protocol Inheritance
protocol Item: Printable, Loggable {
    var name: String { get set }
}

class Product: Item {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func printDetails() {
        print("Printing Details for Item: \(name)")
    }
    
    func logDetails() {
        print("Logging Details for Item: \(name)")
    }
}

let item: Item = Product(name: "Apple")
item.printDetails() // In ra: Printing Details for Item: Apple
item.logDetails() // In ra: Logging Details for Item: Apple

Trong ví dụ trên,

  • Item là một giao thức kế thừa từ hai giao thức khác là Printable và Loggable.
  • Lớp Product tuân thủ giao thức Item và triển khai các phương thức yêu cầu.

Protocol Types

Trong Swift có thể sử dụng giao thức như một kiểu đầy đủ. Bạn có thể tạo ra các biến hoặc hàm mà kiểu của chúng là một giao thức. Điều này rất hữu ích khi bạn muốn viết mã linh hoạt và tái sử dụng được.

Dưới đây là một ví dụ về việc sử dụng giao thức như một kiểu đầy đủ:

protocol CanFly {
    func fly()
}

class Bird: CanFly {
    func fly() {
        print("The bird flies")
    }
}

class Airplane: CanFly {
    func fly() {
        print("The airplane flies")
    }
}

func makeItFly(flyer: CanFly) {
    flyer.fly()
}

let bird = Bird()
let airplane = Airplane()

makeItFly(flyer: bird) // Outputs: The bird flies
makeItFly(flyer: airplane) // Outputs: The airplane flies

Trong ví dụ trên,

  • CanFly được sử dụng như một kiểu đầy đủ trong hàm makeItFly(flyer: CanFly).
  • Hàm này có thể nhận bất kỳ đối tượng nào tuân thủ giao thức CanFly.

Protocol Checking and Casting

Trong Swift có thể kiểm tra xem một thể hiện có tuân thủ một giao thức cụ thể hay không. Và bạn cũng có thể ép kiểu giữa các giao thức. Điều này được thực hiện thông qua việc sử dụng các toán tử is và as.

Dưới đây là một ví dụ về việc kiểm tra và ép kiểu giao thức:

protocol CanFly {
    func fly()
}

class Bird: CanFly {
    func fly() {
        print("The bird flies")
    }
}

class Airplane: CanFly {
    func fly() {
        print("The airplane flies")
    }
}

let bird: CanFly = Bird()
let airplane: CanFly = Airplane()

if bird is Bird {
    print("This flyer is a bird")
}

if let birdAsBird = bird as? Bird {
    birdAsBird.fly() // Outputs: The bird flies
}

if let airplaneAsAirplane = airplane as? Airplane {
    airplaneAsAirplane.fly() // Outputs: The airplane flies

Trong ví dụ trên,

  • bird và airplane đều là kiểu CanFly, nhưng chúng được khởi tạo bằng Bird và Airplane tương ứng.
  • Toán tử is được sử dụng để kiểm tra xem bird có phải là một thể hiện của Bird hay không.
  • Toán tử as? được sử dụng để ép kiểu bird và airplane về Bird và Airplane tương ứng, cho phép chúng ta gọi phương thức fly() của chúng.

Protocols with Associated Types

Giao thức với yêu cầu chỉ mục cho phép bạn tạo ra các giao thức mà một phần của định nghĩa của chúng phụ thuộc vào kiểu cụ thể được cung cấp khi giao thức được tuân thủ. Điều này tạo ra một khái niệm về “giao thức chung”.

Dưới đây là một ví dụ về việc sử dụng giao thức với yêu cầu chỉ mục:

protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

struct IntContainer: Container {
    var items = [Int]()
    
    mutating func append(_ item: Int) {
        items.append(item)
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

var container = IntContainer()
container.append(12)
print(container.count) // Outputs: 1
print(container[0]) // Outputs: 12

Trong ví dụ trên,

  • Container là một giao thức có một yêu cầu chỉ mục Item.
  • IntContainer tuân thủ Container và định nghĩa Item là Int.

Protocols as Generic Constraints

Trong Swift, bạn có thể sử dụng giao thức như một ràng buộc chung trong các hàm và kiểu chung. Điều này cho phép bạn viết các hàm và kiểu chung. Mà yêu cầu các đối số hoặc kiểu chung phải tuân thủ một giao thức cụ thể.

Dưới đây là một ví dụ về việc sử dụng giao thức như một ràng buộc chung:

protocol CanFly {
    func fly()
}

class Bird: CanFly {
    func fly() {
        print("The bird flies")
    }
}

class Airplane: CanFly {
    func fly() {
        print("The airplane flies")
    }
}

func makeItFly<T: CanFly>(flyer: T) {
    flyer.fly()
}

let bird = Bird()
let airplane = Airplane()

makeItFly(flyer: bird) // Outputs: The bird flies
makeItFly(flyer: airplane) // Outputs: The airplane flies

Trong ví dụ trên,

  • Hàm makeItFly(flyer: T) có một ràng buộc chung T: CanFly
  • Yêu cầu T phải tuân thủ giao thức CanFly.
  • Điều này có nghĩa là bạn chỉ có thể truyền vào hàm makeItFly(flyer: T) các đối tượng của các kiểu tuân thủ giao thức CanFly.

Protocols with Self Requirements

Một số giao thức có thể yêu cầu các phương thức hoặc thuộc tính trả về kiểu của chính đối tượng thực hiện giao thức. Điều này được biểu thị bằng từ khóa Self.

Dưới đây là một ví dụ về việc sử dụng giao thức với các yêu cầu Self:

protocol Copyable {
    func copy() -> Self
}

class MyClass: Copyable {
    var num = 1

    func copy() -> Self {
        let result = type(of: self).init()
        result.num = num
        return result
    }
    
    required init() {
    }
}

let object = MyClass()
object.num = 5

let newObject = object.copy()
print(newObject.num) // Outputs: 5

Trong ví dụ trên,

  • Copyable là một giao thức có một yêu cầu Self trong phương thức copy().
  • MyClass tuân thủ Copyable và triển khai phương thức copy(), trả về một thể hiện mới của chính nó.
  • Lưu ý rằng chúng ta cần phải thêm một khởi tạo yêu cầu required init() để có thể tạo một thể hiện mới của Self.

Protocols with Initializer Requirements

Các giao thức có thể yêu cầu các lớp tuân thủ phải cung cấp một hoặc nhiều khởi tạo cụ thể. Điều này được thực hiện bằng cách định nghĩa các khởi tạo trong phần định nghĩa giao thức.

Dưới đây là một ví dụ về việc sử dụng giao thức với các yêu cầu khởi tạo:

protocol Initializable {
    init(value: Int)
}

class MyClass: Initializable {
    var value: Int
    
    required init(value: Int) {
        self.value = value
    }
}

let object = MyClass(value: 5)
print(object.value) // Outputs: 5

Trong ví dụ trên,

  • Initializable là một giao thức có một yêu cầu khởi tạo init(value: Int).
  • MyClass tuân thủ Initializable và triển khai khởi tạo yêu cầu.
  • Lưu ý rằng chúng ta cần phải sử dụng từ khóa required khi triển khai khởi tạo yêu cầu trong lớp.

Tới đây, bài viết đã dài quá rồi và mình xin tạm kết thúc nó. Chúc bạn code vui vẻ!

Tạm kết

  • Giới thiệu về Lập trình hướng giao thức
  • Tìm hiểu ưu nhược điểm của 2 mô hình OOP & POP
  • Áp dụng POP vào trong dự án với Swift
  • Các tương tác cơ bản khi sử dụng POP

Bạn có thể checkout demo code tại đây.

Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Protocol-oriented programming. Nếu có gì thắc mắc hay góp ý cho mình. Thì bạn có thể để lại bình luận hoặc gửi email theo trang Contact.

Cảm ơn bạn đã đọc bài viết này!

FacebookTweetPinYummlyLinkedInPrintEmailShares34

Related Posts:

  • SMART
    SMART - Hướng dẫn dành tạo Prompt cho người mới bắt đầu
  • feature_bg_blog_012
    Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • dart
    Tìm hiểu về ngôn ngữ lập trình Dart
  • feature_bg_swiftui_4
    Regular Expression (Regex) trong Swift
Tags: Advanced Swift, iOS, protocol, Swift
Written by chuotfx

Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!

2 comments

  • Nguyen Hong Linh has written: September 24, 2024 at 10:54 am Reply

    struct D: B, C {} => chỗ này bị lỗi biên dịch mà a. Vẫn phải conform foo(). Như vậy nói POP không gặp Diamond Problem có đúng không ?

    • chuotfx has written: September 24, 2024 at 11:24 am Reply

      Đúng rồi em. Ví dụ trên sẽ bị lỗi, do trình biên dịch buộc D phải có conform foo(). Lúc này, mình sẽ không biết foo() của A, B hay C thôi. Mục đích bài viết là cho bạn đọc tự ngẫm nghĩ ra à. Nên có câu “What will this print?”

      Múc đích xa hơn là mình áp dụng cho thực tế để tránh trường hợp như vậy à, còn ví dụ chỉ đơn giản vậy thôi.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?
  • Khi Cô Đơn Gặp Python

You may also like:

  • SMART - Hướng dẫn dành tạo Prompt cho người mới bắt đầu
    SMART
  • Tìm hiểu về ngôn ngữ lập trình Dart
    dart
  • Instructions - Cung cấp hướng dẫn cho các Gen AI
    Instructions
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
    feature_bg_blog_011
  • Complete Concurrency với Swift 6
    feature_bg_swift_04

Archives

  • May 2025 (1)
  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • December 2024 (4)
  • September 2024 (1)
  • July 2024 (1)
  • June 2024 (1)
  • May 2024 (4)
  • April 2024 (2)
  • March 2024 (5)
  • January 2024 (4)
  • February 2023 (1)
  • January 2023 (2)
  • November 2022 (2)
  • October 2022 (1)
  • September 2022 (5)
  • August 2022 (6)
  • July 2022 (7)
  • June 2022 (8)
  • May 2022 (5)
  • April 2022 (1)
  • March 2022 (3)
  • February 2022 (5)
  • January 2022 (4)
  • December 2021 (6)
  • November 2021 (8)
  • October 2021 (8)
  • September 2021 (8)
  • August 2021 (8)
  • July 2021 (9)
  • June 2021 (8)
  • May 2021 (7)
  • April 2021 (11)
  • March 2021 (12)
  • February 2021 (3)
  • January 2021 (3)
  • December 2020 (3)
  • November 2020 (9)
  • October 2020 (7)
  • September 2020 (17)
  • August 2020 (1)
  • July 2020 (3)
  • June 2020 (1)
  • May 2020 (2)
  • April 2020 (3)
  • March 2020 (20)
  • February 2020 (5)
  • January 2020 (2)
  • December 2019 (12)
  • November 2019 (12)
  • October 2019 (19)
  • September 2019 (17)
  • August 2019 (10)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (43)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (86)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2025 Fx Studio - All rights reserved.