Protocol-oriented programming: Trái tim của Swift!

2627

Bài viết được sự cho phép của tác giả Lê Xuân Quỳnh

Nếu bạn đang code Swift và vẫn không biết gì về Protocol-oriented programming(POP) thì đúng là bạn chưa biết gì về Swift. Nếu bạn vẫn giữ khư khư quan điểm lập trình hướng đối tượng(object-oriented paradigm) trong swift và không chịu tiếp nhận POP, thì thật là buồn khi tôi nói bạn không nên tiếp tục đọc bài viết này. Tất nhiên điều đó cũng làm tôi buồn lắm, vì bạn đã bỏ lỡ 1 bài viết quan trọng 😭

  Các cách sử dụng AS, AS?, AS! một cách hiệu quả và an toàn trong code Swift
  Các ưu nhược điểm của Swift so với Objective C

Xem thêm nhiều việc làm Swift hấp dẫn trên TopDev

Nếu bạn vẫn ở lại, hãy cùng xem những ví dụ đơn giản của POP để thấy được sức mạnh của nó mang lại nhé.

Bài viết tiếng Anh vui lòng xem tại:

Lập trình hướng đối tượng(OOP) là 1 trong những mô hình mà đa số developer sẽ biết đến đầu tiên, là 1 cách tiếp cận phổ biến nhất ở đại đa số các ngôn ngữ lập trình. Tuy nhiên, nó không phải là tất cả. Trong những năm gần đây, cách tiếp cận POP được cộng đồng Swift sử dụng rất nhiều. Nó không phải là thứ gì đó mới và sáng bóng, cũng không phải là viên đạn bạc cho mọi vấn đề. Tuy nhiên, nó đóng vai trò hữu ích trong việc cấu trúc source code. Trong bài viết này, chúng ta cùng nhau đi qua các ví dụ để thấy sự hữu ích của nó.

Object-oriented approach trong Swift

Hãy tưởng tượng bạn được yêu cầu viết 1 chương trình trò chơi, chỉ với 2 level: Cấp độ 1 là trên mặt đất, cấp độ 2 là dưới lòng đất.

Trước tiên hãy dùng hướng đối tượng – OOP để viết. Chúng ta cần liệt kê những điểm chung nhất để bố trí các lớp. Chúng ta cũng sẽ cần 2 loại nhân vật: một cho người chơi, một cho các sinh vật kẻ thù trên đất liền và một đại diện cho quái vật địa ngục. Hãy bắt đầu với class như sau:

Creature

Các thuộc tính và hành vi chung được mô tả như sau:

class Creature {
    let name: String
    init(name: String) {
        self.name = name
    }

    func fight() {
        print("👊")
    }
}

Kẻ thù trên mặt đất có thể chạy bộ và chiến đấu với người chơi. Lớp sinh vật mặt đất được mô tả như sau:

class LandCreature: Creature {
    func walk() {
        print("🚶🏻‍♀️")
    }

    func run() {
        print("🏃🏻")
    }
}
A screenshot with a fragment of a code (landcreature)

Còn những con quái vật trong lòng đất rất nguy hiểm, nó có thể thiêu cháy mọi thứ nó gặp. Chúng nó cũng có thể đi bộ và chạy tới nạn nhân, sau đó thiêu đốt nạn nhân:

class HellCreature: Creature {
    func walk() {
        print("🚶🏻‍♀️")
    }
    func run() {
        print("🏃🏻")
    }
    func burn() {
        print("🔥")
    }
}

Tại thời điểm này, chúng ta có thể sẽ quyết định rằng chạy(run) và đi bộ(walk) là khả năng cơ bản của tất cả các nhân vật, vì vậy chúng tôi nên chuyển chúng vào lớp cha:

class Creature {
    let name: String
    init(name: String) {
        self.name = name
    }

    func walk() {
        print("🚶🏻‍♀️")
    }
    func run() {
        print("🏃🏻")
    }
    func fight() {
        print("👊")
    }
}
class LandCreature: Creature { }
class HellCreature: Creature {
    func burn() {
        print("🔥")
    }
}

Sau đó chúng ta hãy tìm hiểu về HellCreature – một loại sinh vật có thể chạy, chiến đấu và thổi lửa:

A screenshot with a fragment of a code (hellcreature)

Vậy là xong, chúng ta đã hoàn thành để chơi.

Tại thời điểm này, sếp của bạn có thể sẽ nảy ra một ý tưởng tuyệt vời về việc thêm cấp độ cao cấp hơn vào trò chơi. Hãy thêm 1 loại sinh vật bay nổi loạn(pilot) vào trò chơi:

class SkyCreature: Creature {
    func fly() {
        print("🕊️")
    }
}

Hãy khởi tạo loài này như sau:

A screenshot with a fragment of a code (skycreature)

Ồ, điều ngạc nhiên là loài sinh vật này lại có thể chạy bộ. Điều quái quỷ gì thế này? Hãy sửa nó bằng cách viết lại 2 phương thức run() và walk() và thêm dòng này vào:

fatalError()

để nó phát sinh lỗi khi cố tình gọi. Hoặc chúng ta có thể di chuyển chúng về lớp LandCreature và HellCreature. Dù sao thì nó cũng là phần cuối của dự án. Một sự trùng lặp mã nhỏ cũng chả chết ai, phải không?

class Creature {
    let name: String
    init(name: String) {
        self.name = name
    }

    func fight() {
        print("👊")
    }
}
class LandCreature: Creature {
    func walk() {
        print("🚶🏻‍♀️")
    }

    func run() {
        print("🏃🏻")
    }
}
class HellCreature: Creature {
    func walk() {
        print("🚶🏻‍♀️")
    }
    func run() {
        print("🏃🏻")
    }
    func burn() {
        print("🔥")
    }
}

Cuối cùng, Kanimoor không thể chạy hoặc đi bộ được nữa.

A screenshot with a fragment of a code (skycreature)

Một lần nữa, trò chơi đã hoàn thành. Chà… gần như vậy. Trong phiên bản chào năm mới của trò chơi, sẽ có thêm một cấp độ nữa để chơi… đó là đấu với RỒNG! Tôi nghĩ bạn đã có thể nhìn thấy những rắc rối mà chúng ta sẽ gặp phải…

class Dragon: SkyCreature { }
A screenshot with a fragment of a code (dragon)

Tất nhiên 1 con Wyvern thì có thể đi bộ, chạy và phun lửa. Một lần nữa chúng ta có thể di chuyển walk() và run() vào lớp cha, hoặc sao chép code và dán vào lớp Dragon này.

Lập trình hướng giao thức – Protocol-oriented programming

Lần này, giả sử chúng ta có cơ hội bắt đầu lại toàn bộ dự án (đó là một viễn cảnh khá đẹp, phải không?) Và hãy xem cách chúng ta có thể sử dụng các giao thức để cấu trúc toàn bộ vũ trụ tốt hơn. Để làm được điều đó, trước tiên chúng ta phải tìm hiểu lập trình hướng giao thức(POP) với Swift là gì và nó cung cấp những gì. WWDC15 Apple nói rằng:

Protocols have all these advantages, and that’s why, when we made Swift, we made the first protocol-oriented programming language. So, yes, Swift is great for object-oriented programming, but from the way for loops and string literals work to the emphasis in the standard library on generics, at its heart, Swift is protocol-oriented.

protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality

…Một giao thức(protocol) xác định một bản thiết kế các phương thức, thuộc tính và các yêu cầu khác phù hợp với một nhiệm vụ hoặc một phần chức năng cụ thể. 

Protocols tương tự như interfaces -lớp giao diện ở các ngôn ngữ khác. Tuy nhiên với swift thì nó độc đáo và tiện ích hơn nhiều. Đầu tiên, ta có thể có một triển khai protocol mặc định và bắt buộc. Ví dụ:

protocol Running {
    func run()
}

extension Running {
    func run() {
        print("🏃🏻")
    }
}

Kể từ bây giờ loại protocol Running sẽ implementation phương thức run(). Tất nhiên, đôi khi bạn có thể muốn ghi đè nó bằng một cách triển khai hàm run() theo cách khác. Còn khi không muốn, giá trị mặc định phải đủ và hữu ích trong việc tránh trùng lặp mã. Các giá trị mặc định có thể cung cấp cho các lớp chỉ định. Trong ví dụ sau những loại có Persevering và Walking , giao thức Persevering sẽ triển khai phương thức stepByStep() như sau:

extension Persevering where Self: Walking {
    
    func stepByStep() {
        walk()
        walk()
    }
}
Screenshot with a fragment of a code (woolf).

Các kiểu khác nhau có thể áp dụng nhiều giao thức khác nhau, vì chúng có thể làm nhiều việc. Đồng thời, chúng chỉ có thể là một thứ (chỉ kế thừa một lớp cha). Một điều rất quan trọng khác là thực tế là các protocols có thể được chấp nhận bởi cả kiểu tham chiếu (Class) và kiểu tham trị (Struct và Enum), trong khi các lớp cha và con kế thừa bị giới hạn ở các kiểu tham chiếu. Chúng ta không đề cập đến sự khác biệt giữa tham trị và tham chiếu ở đây, nhưng tôi thực sự khuyên bạn nên tìm hiểu điều này. Đó là một tính năng chính khác trong Swift thực sự đáng để biết và sử dụng một cách khôn ngoan.

Extensions

Chúng ta hãy lập mô hình cấu trúc của một ứng dụng trước, thay vì buộc phải đưa ra tất cả các quyết định từ trước. Với các tiếp cận POP, chúng ta có thể bắt đầu triển khai hai cấp độ đầu tiên của trò chơi. Đầu tiên hãy xem xét lớp cơ sở Creature. Chúng ta hãy cho phép phương thức chiến đấu fight() qua protocol Strikeable:

class Creature {
    let name: String
    init(name: String) {
        self.name = name
    }
}

protocol Strikeable {
    func fight()
}

extension Strikeable {
    func fight() {
        print("👊")
    }
}

extension Creature: Strikeable { }

Định nghĩa hàm fight() riêng biệt trong protocol Strikeable, cung cấp khả năng tấn công cho Creatures. Cấp độ đầu tiên diễn ra trên mặt đất, nơi người chơi sẽ chiến đấu chống lại:

LandCreatures

Hành động cơ bản của chúng (ngoài chiến đấu) là đi bộ và chạy. Hai giao thức như sau:

protocol Walking {
    func walk()
}

extension Walking {
    func walk() {
        print("🚶🏻‍♀️")
    }
}
protocol Running {
    func run()
}

extension Running {
    func run() {
        print("🏃🏻")
    }
}

Với 2 protocols trên, LandCreature sẽ được thiết kế khá đơn giản như sau:

class LandCreature: Creature,
                    Walking,
                    Running { }

Các hành động của con Woolfie từ lớp LandCreature có thể được mô tả như sau:

A screenshot with a fragment of a code (landcreature – woolfie).

Hãy xuống địa ngục một lần nữa (?)! Bây giờ hãy thiết kế lớp cho loài HellCreature. Chúng ta sẽ thiết kế hành động đốt cháy burn() trong protocol Burning:

protocol Burning {
    func burn()
}

extension Burning {
    func burn() {
       print("🔥")
    }
}
Và đơn giản, HellCreature được thiết kế như sau:
class HellCreature: Creature,
                    Walking,
                    Running,
                    Burning { }
A screenshot with a fragment of a code (hellcreature)

Thật đơn giản phải không. Bây giờ hãy thiết kế cấp độ 3 cho trò chơi có tên là “Cuộc nổi dậy trên bầu trời” sẽ yêu cầu thêm các hành động fly() và fight() cho loài SkyCreature. Chúng ta cần sửa phương thức fight() mặc định để nó chiến đấu trên bầu trời theo cách của nó:

protocol Flying {
    func fly()
}

extension Flying {
    func fly() {
        print("🛩️")
    }
}
class SkyCreature: Creature, Flying {
    func fight() { 
        print(„🏹")
    }
}
A screenshot with a fragment of a code (skycreature)

Thật là đơn giản và nhanh chóng phải không? Bây giờ hãy mô tả lớp Rồng như sau:

class Dragon: Creature,
              Walking,
              Running,
              Flying,
              Burning { }
A screenshot with a fragment of a code (dragon)

Thật là một thành công lớn! Quá trình tạo các lớp theo hướng giao thức của chúng ta rất dễ dàng!

Bằng cách sử dụng protocol, chúng ta có thể mô tả tất cả các loại sinh vật 1 cách đơn giản, mà không cần qua cách tiếp cận hướng đối tượng phân cấp giữa các lớp cứng nhắc. Với các protocol, chúng ta có thể thêm các thuộc tính thuận tiện để kiểm tra xem một đối tượng nhất định có thể thực hiện một tác vụ cụ thể hay không:

extension Creature {
    
    var canFly: Bool { return self is Flying }
    var canBurn: Bool { return self is Burning }
    var canWalk: Bool { return self is Walking }
}

Điều tuyệt vời ở đây là bạn không cần phải cập nhật các giá trị này khi bất kỳ giá trị nào trong số chúng ngừng tuân thủ giao thức. Đây là các thuộc tính computed properties, vì vậy kết quả sẽ tự động thay đổi.

Rủi ro trong lập trình hướng giao thức POP

Cũng giống như lập trình hướng đối tượng có nguy cơ tạo ra một hệ thống phân cấp lớp rất phức tạp, lập trình hướng giao thức có thể khiến cấu trúc phát triển quá nhiều theo chiều ngang. Quá nhiều mở rộng do tạo ra nhiều giao thức nhỏ sẽ khiến việc duy trì và sử dụng ứng dụng trở nên khó khăn và phiền phức. Chắc chắn, protocols cũng yêu cầu phân lớp trong dự án để bạn không bị mất dấu về những protocol nào đã được thêm vào những class nào. Nó giống như bất kỳ kỹ thuật nào khác: cần phải tìm ra sự cân bằng khi chọn lựa giữa OOP và POP để giải quyết vấn đề chứ không phải là cứng nhắc khi chọn lựa.

Kết luận

Trong lập trình hướng đối tượng, chúng ta tập trung vào đối tượng là gì, trong khi phương pháp hướng giao thức cho phép chúng ta tập trung nhiều hơn vào những gì một đối tượng có thể làm, khả năng và hành vi của nó. Ví dụ trò chơi đơn giản của chúng ta nhằm nhấn mạnh sự khác biệt trong quá trình phát triển bằng cách sử dụng cả hai mô hình này. Lúc đầu, phần mở rộng giao thức(extension) và triển khai mặc định có vẻ giống với các lớp cơ sở hoặc lớp trừu tượng trong các ngôn ngữ khác, nhưng trong Swift, chúng đóng một vai trò lớn hơn. Tại sao?

  • Các kiểu class có thể phù hợp với nhiều hơn một giao thức.
  • Các protocol extension cũng có thể có các hành vi mặc định từ nhiều protocol khác nhau.
  • Không giống như đa kế thừa trong các ngôn ngữ lập trình khác, phần mở rộng giao thức không thêm bất kỳ trạng thái bổ sung nào.
  • Các giao thức có thể được chấp nhận bởi các lớp, struct và enum, trong khi các lớp base và lớp con kế thừa chỉ bị giới hạn ở các loại class.
  • Các giao thức cho phép lập mô hình hồi quy với các phần mở rộng được thêm vào các loại hiện có.

Vậy chúng ta có thể nói protocol là trái tim của swift. Vì vậy hãy học càng nhiều càng tốt từ thư viện tiêu chuẩn – đó là một nguồn kiến ​​thức tuyệt vời! Quan trọng nhất, hãy cho bản thân cơ hội lập trình theo hướng giao thức và tự tìm hiểu cách nó có thể cải thiện quy trình làm việc của bạn.

Bài viết gốc được đăng tải tại codetoanbug.comfanpage

Có thể bạn quan tâm:

Xem thêm Việc làm IT hấp dẫn trên TopDev