Lập trình hướng đối tượng (OOP) với Swift
iOS & Swift . TutorialsContents
Chào mừng bạn đến với Fx Studio. Chủ đề của bài viết này là về Lập trình hướng đối tượng, hay còn gọi là Object Oriented Programming – OOP). Đây là một chủ đề siêu kinh điển, hiểu & áp dụng được nó thì bạn đã nắm trọn thiên hạ này rồi. Và trong phạm vi kiến thức, mình sẽ trình bày Lập trình hướng đối tượng với ngôn ngữ Swift thần thánh của chúng ta.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
OOP là một trong những thứ cơ bản nhất và nó được tích hợp vào tất cả các ngôn ngữ lập trình hiện đại ngày nay. Do đó, bạn không cần phải chuẩn bị gì nhiều về mặt công cụ và phần mềm nhóe. Thay vào đó, bạn nên chuẩn bị …
Một tâm hồn đẹp!
Nếu bạn chưa biết gì về lập trình hoặc ngôn ngữ Swift, thì tốt nhất dành ra 10 phút cuộc đời để đọc bài viết này:
Và nếu bạn muốn áp dụng OOP với Swift nhanh nhất, thì hãy tiếp tục với series Lập trình iOS cho mọi người thêm nhóe. Hứa uy tín luôn!
OOP – Lập trình hướng đối tượng là gì?
Khái niệm & lịch sử
Về mặt định nghĩa cụ thể và bài bản nhất, bạn có thể tìm trên mạng bằng vài đường Google nhóe. Hoặc ghé qua Wikipedia để đọc thêm.
Lập trình hướng đối tượng (Object Oriented Programming – OOP) là một trong những kỹ thuật lập trình rất quan trọng và sử dụng nhiều hiện nay. Hầu hết các ngôn ngữ lập trình hiện nay như Java, PHP, .NET, Ruby, Python… đều hỗ trợ OOP. Và Swift thì không ngoại lệ. Nhưng ta lại có những cái đặc thù riêng của Swift có mà các ngôn ngữ khác không có.
Điều tạo nên khác biệt chính là sự kế thừa từ người tiền nhiệm của Swift, là ngôn ngữ Objevtive-C. Hiểu đơn giản là từ thời điểm Objective-C ra đời, thì thế giới OOP chia thành 2 trường phái: C++ & Objective-C. Sau này, C++ với con cháu của nó là Java, C# … còn Objective-C là Swift. (Tạm chấp nhận như vậy trước nhóe!)
Tại sao sử dụng hướng đối tượng?
Thay vì trả lời câu hỏi là gì, thì bạn nên tìm hiểu câu hỏi tại sao để hiểu hơn việc bạn sẽ sử dụng nó tốt hơn nữa.
Với tư tưởng chính, OOP xem như mọi thành phần trong chương trình đều là các thực thể (object). Các đối tượng sẽ tương tác với nhau, dẫn tới sự biến đổi về tính chất (property) của chúng. Sự tương tác đó là một cách mô phỏng lại hành vi (method) của đối tượng.
Nhiệm vụ của chúng ta trong lập trình hướng đối tượng rất đơn giản, cụ thể như sau:
- Mô phỏng lại thực tế bằng các đối tượng trong chương trình.
- Biến đổi chúng bằng cách thực hiện các hành vi.
- Cho các đối tượng tương tác với nhau.
Ví dụ: bạn viết một chương trình điều kiểu xe hơi. Bạn phải tạo ra được đối tượng xe. Và muốn nó di chuyển thì bạn sẽ gọi hành vi chạy. EZ!
Và câu trả lời cho câu hỏi trên là:
Dễ sử dụng
Đó là một phép ánh xạ, giữ thực tế và lập trình. Bạn sẽ dễ hình dùng và dễ sử dụng hơn. Giúp cho nhiều bạn mới bắt đầu học lập trình có thể tiếp cận được. Và tới bây giờ, OOP vẫn là cách tiếp cận tốt nhất khi mới bước vào thế giới lập trình.
Bên cạnh đó, một tính chất rất quan trọng để OOP đến với mọi người. Chính là tính kế thừa. Các đối tượng con sẽ mang trong mình tất cả các tính chất của đối tượng cha. Giúp bạn tiết kiệm đi khá nhiều công sức khi không xây dựng lại từ đầu với mỗi chương trình.
Các tính chất của hướng đối tượng
Tính đóng gói (Encapsulation)
- Các dữ liệu liên quan sẽ được đóng gói thành một lớp
- Giúp che giấu dữ liệu với các đối tượng bên ngoài
- Do đó, sẽ chia thành các thuộc tính & phương thức thông qua các cách truy cập: riêng tư (private), công khai (public)
Tính kế thừa (Inheritance)
- Xây dựng lớp mới đơn giản hơn, dựa trên các lớp có sẵn
- Lớp con (sub-class) sẽ mang trong mình đầy đủ các phương thức & thuộc tính của lớp cha (super-class)
- Các lớp cơ sở (base-class) được xem là các lớp đầu tiên và nó không kế thừa lại từ lớp nào cả
Tính đa hình (Polymorphism)
- Một hành động, hay phương thức (method) sẽ được thực hiện bằng nhiều cách khác nhau.
- Đây là sức mạng của hướng đối tượng.
- Đa hình là khái niệm mà hai hoặc nhiều lớp có những phương thức giống nhau nhưng có thể thực thi theo những cách thức khác nhau.
- Ví dụ: chó & méo đều là 2 đối tượng, chúng có cùng thuộc tính là kêu. Nhưng mỗi con lại phát ra tiếng khác nhau.
Tính trừu tượng (Abstraction)
- Giúp cho bạn khái quát hóa một lớp. Mang tính chất thiết kế hơn là triển khai các lớp.
- Vì chỉ có khai báo các thuộc tính & phương thức. Chứ không định nghĩa cụ thể từng cái ra.
- Lớp trừu tượng sẽ không tạo được thể hiện của nó. Tức là không có đối tượng.
- Các lớp con của lớp trừu tượng thì phải định nghĩa hết tất cả các phương thức & thuộc tính trừu tượng.
Class và Object
Định nghĩa
Okay, chúng ta vào phần chính thôi nào. Đầu tiên, ta cần biết 2 khái niệm cơ bản chính của OOP nhóe. Đó chính là lớp (class) và đối tượng (object). Tóm tắt mối quan hệ của chúng nó thì như sau:
- Class là một tập hợp các đối tượng (object) có cùng thuộc tính và phương thức
- Class định nghĩa ra các thuộc tính (property) và các phương thức (method) mà đối tượng sở hữu
- Object (đối tượng) là thể hiện (instance) của lớp
Tới đây, nhiều bạn mới học sẽ bắt đầu rối rồi. Vì bạn có thể chưa hình dung ra được cái nào có trước, cái nào có sau giữa class & object. Do đó, ta đi tiếp vào phần so sánh sau đây.
Trong bài viết, mình sẽ sử dụng hỗn hợp giữa tiếng Việt và tiếng Anh để gọi các khái niệm. Vì không phải là dịch toàn bộ ra tiếng Việt không được. Mà là muốn cho các bạn có không bỡ ngỡ với phần còn lại của thế giới lập trình, khi mọi tài liệu tiếng Việt đều ghi là “dịch không rõ nghĩa“.
Hiện thực & Lập trình
Hiện thực
- Tập hợp các đối tượng có cùng chung một hoặc 1 vài tính chất thì tạo thành 1 lớp
- Từ những thuộc tính và hành vi của đối tượng mà mô tả nên các tính chất của và đặc tính của một lớp
- Lớp
- Là 1 tập hợp
- Không đồng nhất về tất cả cho toàn bộ các đối tượng của lớp
- Đối tượng là một thực thể xác định
- Đối tượng có trước và lớp có sau
Lập trình
- Một lớp bao gồm các thuộc tính và tính chất sẽ được định nghĩa trước. Sau đó, các đối tượng là các thể hiện của các lớp & được tạo ra sau.
- Các đối tượng cùng một lớp, thì có những thuộc tính và phương thức giống nhau
- Lớp
- Là 1 khuôn mẫu
- Đối tượng thì đồng nhất về tất cả
- Đối tượng là thể hiện của lớp
- Lớp có trước và đối tượng có sau
Khai báo & sử dụng
Định nghĩa như vậy thì xem tạm ổn rồi. Chúng ta tiếp tục với việc coding nó thôi. Đầu tiên, chính là khai báo một lớp thì cú pháp như sau:
class <ClassName> { // class’s definition }
Trong đó:
class
là từ khóa để khai báo một lớp- Bạn tự đặt tên cho lớp của bạn nhóe. Có một quy tắt nhỏ thì tên của lớp là 1 danh từ và viết theo quy tắt lạc đà.
- Nội dung hay thân (body) của lớp, thì được định nghĩa trong cặp dấu
{ }
nhóe
Cú pháp để tạo một thể hiện (instance) như sau:
var <instanceName> = <ClassName>()
Cũng khá đơn giản, giống như cách bạn tạo một biến với kiểu dữ liệu chính là lớp vừa khai báo ở trên. Xem ví dụ code cho dễ hiểu hơn nhóe!
class Point { var x = 0.0 var y = 0.0 } var point = Point() point.x = 100 point.y = 200 print("point (\(point.x), \(point.y))") // point (100.0, 200.0)
Trong đó:
- Khai báo class Point có 2 thuộc tính
x
vày
- Cấp giá trị mặc định cho
x
vày
- Khởi tạo đối tượng
point
là thể hiện của class Point - Truy cập và thay đổi giá trị của
x
vày
- In các giá trị
x
vày
ra màn hình
Structure
Định nghĩa
Structure hay còn gọi là cấu trúc. Một dạng sơ khai của lớp. Với các ngỗn ngữ lập trình khác, Struct không có ý nghĩa nhiều. Vì nó chỉ là nhóm các kiểu dữ liệu cơ bản thành một kiểu dữ liệu mới.
Ví dụ: bạn muốn có 1 kiểu dữ liệu vừa có 2 tính chất là Int & String, thì bạn kết hợp Int & String cho 2 thuộc tính (property) của Struct. Quan trọng Struct sẽ không có các phương thức (method).
Tuy nhiên, với OOP trong Swift thì mọi thứ điên rồ hơn. Bạn sẽ có một phiên bản Struct cũng bá như là Class. Tức là, bạn sẽ có đầy đủ các tính chất sau cho một Struct:
- Thuộc tính
- Phương thức
- Thể hiện
Khai báo một Struct với cú pháp sau:
struct <StructureName>{ // structure’s definition }
Tạo một thể hiện của Struct với cú pháp sau:
var <instanceName> = <StructureName>()
Rất giống với khai báo một Class nhóe, bạn chỉ cần thay từ khóa class
thành struct
mà thôi. EZ Game!
Sau đây, là ví dụ code cho bạn dễ hình dung nhóe!
struct Cat { var name: String var age: Int } var kitty = Cat(name: "Kitty", age: 5) print("Cat: \(kitty.name) - \(kitty.age)")
Đặc trưng đầu tiên của Struct là:
Tất cả structure đều tự động khởi tạo memberwise initializer, dùng để khởi tạo tất cả giá trị ban đầu của structure.
Sự giống nhau giữa Class & Structure
- Các thuộc thính (properties) để chứa giá trị
- Các phương thức (methods) để xử lý các chức năng
- Khai báo subscripts để truy cập vào giá trị thông qua cú pháp
subscript
(phần này nâng cáo nhóe) - Khai báo initializers để cài đặt trạng thái ban đầu
- Tương thích với protocol
Sự khác nhau giữa Class & Structure
Class
- Có thể kế thừa từ class khác
- Có thể ép kiểu (type casting) để kiểm tra và chuyển đổi kiểu của instance tại runtime
- Kiểu tham chiếu
Giá trị của kiểu dữ liệu tham chiếu được tham chiếu tới cùng một instance khi nó được gán cho một biến/hằng số, hoặc khi nó được truyền qua một function.
Structure
- Không có tính chất kế thừa
- Không thể ép kiểu
- Kiểu tham trị
Giá trị của kiểu dữ liệu tham trị được copy khi nó được gán cho một biến/hằng số, hoặc khi nó được truyền qua một function.
Enumeration
Định nghĩa
Enumeration hay Enum còn được gọi là kiểu liệt kê. Về định nghĩa, khai báo một nhóm các giá trị liên quan với nhau, nhằm tạo sự tường minh trong lập trình cũng như tính toán và xử lý. Về đặc trưng, thì
- Là kiểu tham trị
- Không có tính thừa kế.
Cú pháp:
- Khai báo enumeration bằng từ khoá
enum
- Mỗi trường hợp (case) trong enumeration được khai báo bằng từ khoá
case
Xem ví dụ code nhóe!
enum CompassPoint { case north case south case east case west }
Đó là khai báo một Enum, ngoài ra bạn còn có dạng thu gọn như sau:
enum CompassPoint { case north, south, east, west }
Về cách sử dụng để tạo thể hiện, bạn cũng có 2 cách.
- Cách đầy đủ
var directionToHead = CompassPoint.east
- Cách thu gọn
directionToHead = .west
Property & Method
Với OOP trong Swift lại khá điên rồ hơn nữa. Khi bạn có thể tạo thêm các thuộc tính (property) & phương thức (method) cho các khai báo Enum. Và nó cũng tương tự như Class và Struct.
Xem ví dụ sau nhóe!
enum Suit { case spades, hearts, diamonds, clubs func simpleDirection() -> String { switch self { case .spades: return "Speades" case .hearts: return "Hearts" case.diamonds: return "Diamonds" case .clubs: return "Clubs" } } } let hearts = Suit.hearts print(hearts.simpleDirection())
Không có gì khó ở đây hết. Enumeration có method và properties … cách sử dụng tương tự như ở class và structure. Ngoài ra, bạn nên sử dụng switch case
trong Enum khi muốn tương tác với các thuộc tính của chúng.
Associated values
Sự tiến hóa của Swift, còn nâng cấp thêm sức mạnh của Enum lên rất là nhiều. Cụ thể:
- Enumeration case có thể chứa giá trị liên kết (associated values). Mục đích là chứa thêm thông tin của enum
case
. - Những giá trị này có thể thuộc bất kỳ kiểu dữ liệu nào và khác nhau trong mỗi enum
case
nếu cần thiết.
Ví dụ code nhóe!
enum Barcode { case upc(Int, Int, Int, Int) case qrCode(String) } var productBarcode = Barcode.upc(8, 85909, 51226, 3) productBarcode = .qrCode("ABCD")
Cùng là một thể hiện từ 1 Enum đã khai báo. Nhưng bạn có 2 cách tạo chúng với 2 kiểu dữ liệu khác nhau truyền vào.
Ví dụ: bạn giải một phương trình bậc 2 cơ bản. Kết quả sẽ là nhiều trường hợp (có 2 nghiệm, có 1 nghiệm, vô nghiệm …). Khi đó, bạn chỉ cần khai báo 1 Enum với các case
tương tứng:
- Có 2 nghiệm thì sẽ là
case
với Float & Float - 1 nghiệm, thì
case
sẽ là Float - Vô nghiệm, thì không cần gì hết
Giúp cho việc diễn đạt suy nghĩ logic của bạn trong các bài toán được rõ ràng hơn nhiều. Đây là đặc trưng riêng biệt của Swift. Và sau này bạn sẽ sử dụng nó rất là nhiều.
Raw values
Một phần không thể thiếu trong Enum, chính là dữ liệu thô (raw value). Với Swift, chúng ta lại có khá nhiều cách sử dụng raw value.
- Raw value là giá trị mặc định được đưa ra của enumeration case, có cùng kiểu dữ liệu với enumeration.
- Đối với kiểu string hoặc integer, Swift tự động gán giá trị raw value cho enum case nếu case đó không được gán giá trị mặc định.
Xem ví dụ nhóe!
// #1 enum Suit1: String { case spades = "Spades" case hearts = "Hearts" case diamonds = "Diamonds" case clubs = "Clubs" } print(Suit1.spades.rawValue) // #2 enum Suit2: String { case spades case hearts case diamonds case clubs } print(Suit2.spades.rawValue) // #3 enum Suit3: Int { case spades = 1 case hearts case diamonds case clubs } print(Suit3.spades.rawValue)
Ở trên, ta có 3 cách sử dụng raw value cơ bản. Bạn thử thực thi đoạn code trên và cảm nhận kết quả nhóe.
Cuối cùng, chính là việc khởi tạo một một thể hiện của Enum từ một raw value nào đó. Xem tiếp ví dụ code nhóe!
let someSuit = Suit3(rawValue: 4) // clubs let someAnotherSuit = Suit3(rawValue: 5) //nil
Và khi, giá trị cung cấp cho hàm khởi tạo với raw value không tồn tại. Bạn sẽ nhận được một đối tượng optional (nil).
Tóm tắt 1: Class, Structure và Enumeration
Với 3 phần lớn ở trên, ta tìm hiểu qua được các kiểu dữ liệu phức tạp (custom type) mà bạn sử dụng trong quá trình tương tác OOP với Swift. Trong ngôn ngữ Swift, mọi thứ không còn bị giới hạn bởi các khái niệm thông thường ở các ngôn ngữ khác. Các tính chất trước đây khi bạn học OOP thì bây giờ chỉ còn là …
Tương đối mà thôi!
Sự giống nhau của chúng trong Swift:
- Cách khai báo & cách sử dụng tương tự nhau
- Đều có phương thức & thuộc tính
- Cách tạo các thể hiện cũng tương tự nhau
Sự khác nhau của chúng:
- Dựa vào đặc tính của loại.
- Kiểu dữ liệu là tham chiếu hay tham trị
- Mục đích sử dụng của từng loại.
- Khả năng kế thừa của từng loại.
Hi vọng tới được đây thì bạn vẫn còn đủ minh mẫn. Vì chúng ta mới bước được 1/3 chặn đường mà thôi. Các phần dưới sẽ tập trung vào cấu trúc bên trong của chúng. Chính là các thuộc tính & phương thức.
Bạn hãy ngồi xuống, ăn miếng trà & uống miếng bánh. Chúng ta lại tiếp tục nào!
Properties
Định nghĩa
Thành phần đầu tiên trong phần thân (body) của Class/Struct/Enum mà chúng ta cần khám phá, chính là thuộc tính (property). Về định nghĩa, thuộc tính liên kết các giá trị với cấu trúc dữ liệu: class, structure và enumeration, và được mô tả bên trong các cấu trúc dữ liệu đó.
Ví dụ: Một lớp xe hơi với các đặc tính là: dòng xe, màu xe, công suất … Các đặc tính đó sẽ được biểu diễn thành thuộc tính trong khai báo lớp xe hơi. Ta có ví dụ code đơn giản như sau:
class Car { var name: String = "" var color: String = "" }
Trong đó:
- Lớp Car có 2 thuộc tính
name
đại diện cho tên xecolor
đại diện cho màu xe
Về cơ bản, bạn sẽ cần cung cấp một giá trị ban đầu cho thuộc tính mà bạn khai báo. Hoặc bạn phải cung cấp giá trị tại hàm khởi tạo của lớp (tìm hiểu ở phần sau nhóe).
Phân loại
Về mặt phân loại các thuộc tính trong một Class/Struct/Enum lại khá phức tạp. Vì theo mỗi tiêu chí, chúng ta lại có một cách phân biệt khác nhau.
Phân loại theo kiểu dữ liệu:
- Instance properties:
- Các propeties làm việc cùng với thể hiện (instance) của lớp.
- Dễ hiểu là các thuộc tính
non-static
- Type properties:
- Các properties làm việc cùng với kiểu dữ liệu lớp
- Dễ hiểu là các thuộc tính
static
Trong 2 loại trên, chúng ta lại chia tiếp theo tính chất của chúng.
- Stored properties:
- Là nơi chứa các giá trị hằng/biến
- Với instance thì là Stored properties
- Với type thì là Stored type properties
- Computed properties:
- Giá trị của nó được tính toán ra
- Với instance thì là Computed properties
- Với type thì là Computed type properties
Bênh cạnh đó:
- Instance của class và structure có stored properties và computed properties, trong khi đó enumeration chỉ có computed properties.
- Ngoài ra, có thể khai báo property observers để theo dõi sự thay đổi giá trị của một property.
Tới đây, bạn đã nổ não chưa!
Stored properties
Kiểu thuộc tính này là kiểu thuộc tính phổ biến nhất trong giới lập trình OOP. Nó có vài đặc điểm cơ bản sau:
- Là biến/hằng được chứa trong thể hiện (instance) của một class hoặc structure.
- Khai báo bằng:
let
cho hằng hoặcvar
cho biến. - Có thể cung cấp giá trị khởi tạo khi khai báo.
- Có thể thiết lập hoặc sửa đổi giá trị của chúng trong hàm khởi tạo.
Chú ý: đối với structure, khi khai báo instance là một hằng. Thì bạn không thể thay đổi giá trị của các properties bên trong, dù cho các properties đó là biến. Xem ví dụ là hiểu liền.
struct Rectange { var width: Double var height: Double } let rect = Rectange(width: 10, height: 20) rect.width = 11 //error: Cannot assign to property: 'rect' is a 'let' constant
Đoạn code sẽ bị báo lỗi, vì chúng ta không thay đổi các thuộc tính của một thế hiện structure được. Bạn thử làm điều đó với class
xem được không nhóe.
Lazy properties
Đây là một kiểu ở trong Stored Properties. Khi đó, nó là thuộc tính mà giá trị của nó sẽ được khởi tạo vào lần đầu tiên sử dụng. Bạn chỉ cần thêm từ khóa lazy
trước khai báo các thuộc tính mà thôi. Ngoài ra, bạn cần lưu ý một số điểm sau:
- Các lazy stored properties bắt buộc phải khai báo là biến (
var
). - Hữu ích đối với những thuộc tính gây tốn tài nguyên, khi nào cần sử dụng thì giá trị của nó mới được khởi tạo.
- Việc sử dụng với nhiều luồng truy cập không đảm bảo việc khởi tạo giá trị đầu tiên cho nó là duy nhất. Tức là chú ý khi lập trình đa luồng.
Xem ví dụ code sau nhóe!
class DataImporter { var fileName = "data.txt" } class DataManager { lazy var importer = DataImporter() var data = [String]() } let manager = DataManager() manager.data.append("Some data") manager.data.append("Some more data") print(manager.importer.fileName)
Trong đó:
- Ví dụ mô phỏng một lớp quản lý dữ liệu, có nhập xuất từ file. Đây là một tác vụ tốn tài nguyên và thời gian của hệ thống.
- Lớp DataManager có khai báo thuộc tính
lazy
làimporter
- Khi tiến hành tạo thể hiện của DataManager, thì thuộc tính
importer
vẫn chưa được khởi tạo - Cho tới lúc bạn sử dụng truy/xuất dữ liệu từ
importer
, thì nó mới được khởi tạo
Computed properties
Kiểu thuộc tính thứ 2, đó là các thuộc tính mà giá trị của chúng sẽ phải được tính toán ra. Nó không lưu trữ dữ liệu. Gọi là các Computed Properties.
Vì không lưu trữ dữ liệu. Do đó, bạn cần phải thêm các function là get
& set
để dùng nhận giá trị và thiết lập giá trị cho các thuộc tính này một cách gián tiếp. Trong đó:
- Getter là bắt buộc. Từ khóa là
get
- Setter là tùy chọn (không có vẫn không sao). Từ khóa là
set
- Nếu hàm setter không khai báo tên parameter thì tên mặc định là
newValue
Xem ví dụ nhóe!
struct Point { var x = 0.0, y = 0.0 } struct Size { var width = 0.0, height = 0.0 } struct Rect { var origin = Point() var size = Size() var center: Point { get { let centerX = origin.x + (size.width / 2) let centerY = origin.y + (size.height / 2) return Point(x: centerX, y: centerY) } set(newCenter) { origin.x = newCenter.x - (size.width / 2) origin.y = newCenter.y - (size.height / 2) } } } var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0)) let initialSquareCenter = square.center // initialSquareCenter is at (5.0, 5.0) square.center = Point(x: 15.0, y: 15.0) print("square.origin is now at (\(square.origin.x), \(square.origin.y))") // Prints "square.origin is now at (10.0, 10.0)"
Trong ví dụ trên, bạn chỉ cần tập trung vào thuộc tính center
của lớp Rect. Đó là một Computed Properties, giá trị của nó cần phải được tính toán ra từ 2 thuộc tính origin
& size
. EZ Game!
Read-only computed properties
Đây là một biến thể của Computed Properties khi chúng thiếu đi hàm Setter và chỉ có mỗi Getter mà thôi. Ta không thể gán giá trị cho thuộc tính kiểu này.
struct Cuboid { var width = 0.0, height = 0.0, depth = 0.0 var volume: Double { return width * height * depth } } let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0) print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)") // Prints "the volume of fourByFiveByTwo is 40.0"
Và khi, bạn lượt bỏ đi set
của thuộc tính volume
, thì thuộc tính này được gọi là Read-only computed properties.
Type properties
Đây chính là các thuộc tính static trong các ngôn ngữ lập trình khác. Bạn sẽ không cần phải tạo các đối tượng để sử dụng. Mà sử dụng bằng chính tên kiểu dữ liệu (Type name) của nó.
Khi khai báo, bạn có thể sử dụng 2 từ khóa sau:
static
class
(cho phép các lớp con có thể ghi đè lại – override)
Và bạn sẽ thêm các tử khóa đó vào phía trước khai báo của một thuộc tính nhóe. Bạn vẫn có thể dùng được cho cả let
& var
. Xem ví dụ code sau nhóe!
class SomeClass { static var storedTypeProperty = "Some value." static var computedTypeProperty: Int { return 27 } class var overrideableComputedTypeProperty: Int { return 107 } } print(SomeClass.computedTypeProperty) // Prints "27"
Trong đó:
- Các thuộc tính với
static
thì bạn không thể ghi đè lại ở các lớp con - Còn với các thuộc tính với
class
bạn có thể tiến hành ghi đè lại ở các lớp con - Sử dụng thuộc tính thì dùng chính tên class để gọi,
SomeClass.computedTypeProperty
Properties Observers
Các Properties Observers không phải là các thuộc tính, mà là các function. Nó được thêm vào để lắng nghe các sự kiện thay đổi giá trị của thuộc tính. Với mục đích là phản hồi lại sự thay đổi giá trị của chúng. Bao gồm 2 function sau:
willSet
: được gọi trước khi các giá trị mới được lưu trữ vào thuộc tínhdidSet
: được gọi khi hoàn tất việc thay đổi giá trị của thuộc tính
Chúng được gọi bất kỳ khi nào giá trị của thuộc tính bị thay đổi, bao gồm việc thay đổi giá trị mới giống như giá trị hiện tại của nó. Xem ví dụ code sau nhóe!
class StepCounter { var totalSteps: Int = 0 { willSet(newTotalSteps) { print("About to set totalSteps to \(newTotalSteps)") } didSet { if totalSteps > oldValue { print("Added \(totalSteps - oldValue) steps") } } } } let stepCounter = StepCounter() stepCounter.totalSteps = 200 // About to set totalSteps to 200 // Added 200 steps stepCounter.totalSteps = 360 // About to set totalSteps to 360 // Added 160 steps stepCounter.totalSteps = 896 // About to set totalSteps to 896 // Added 536 steps
Với tham số newTotalSteps
dùng trong willSet
. Nhưng nến bạn không sử dụng thì mặc định sẽ là newValue
. Còn tham số trong didSet
là oldValue
. Thực thi đoạn code và cảm nhận kết quả nhóe.
Global & local variables
Đây là 2 khái niệm khác nhau khi đề cập tới một biến/đối tượng. Tuy nhiên sự khác nhau được phân biệt bởi phạm vi khai báo của chúng mà thôi. Còn về bản chất thì nó giống như các biến/đối tượng bình thường khác. Ta có:
- Global variables: Khai báo bên ngoài của bất cứ một function, method, closure, hoặc kiểu dữ liệu.
- Local variables: Khai báo bên trong function, method, hoặc closure.
Về bản chất thì sự giống nhau giữa chúng với các biến/đối tượng khác:
- Có khả năng chứa giá trị như stored properties
- Có khả năng tính toán như computed properties
- Khả năng theo dõi và phản hồi sự thay đổi giá trị như observer properties
Methods
Định nghĩa
Thành phần quan trọng thứ 2 có trong thân của một Class/Struct/Enum là các phương thức (methods). Về định nghĩa, Method là function có liên quan đến một kiểu dữ liệu nhất định nào đó. Cũng như thuộc tính, ta có thể phân loại chúng như sau:
- Instance method: bao đóng những công việc và chức năng dùng để làm việc với một instance của một kiểu dữ liệu nhất định.
- Type method: bao đóng những công việc và chức năng dùng để làm việc trực tiếp tới kiểu dữ liệu của chính nó.
Ví dụ code nhóe:
class SomeClass { func someInstanceMethod() { // function body } class func someTypeMethod() { // function body } } let instance = SomeClass() instance.someInstanceMethod() SomeClass.someTypeMethod()
Trong đó:
- Instance Method là
someInstanceMethod
- Type Method là
someTypeMethod
- Hai cách gọi khác nhau đối với từng loại method thông qua instance & Type name
Instance method
Ta tiếp tục đi vào khám phá từng loại method. Đầu tiên với Instance method thì khá là đơn giản và quen thuộc trong lập trình OOP. Chính là các function mà bạn thêm vào các Class và sử dụng chúng.
Tuy nhiên, ta sẽ có vấn đề chính khi làm việc với các method, chính là truy cập và sửa đổi dữ liệu của chính đối tượng. Trong trương hợp này, Swift cung cấp cho bạn một thuộc tính là self
(hay là con trỏ this
trong các ngôn ngữ khác).
- Thuộc tính self: Mọi instance của một kiểu dữ liệu đều sở hữu thuộc tính được đặt tên là seft, mà giá trị của nó tương đương với chính instance đó.
- Sử dụng thuộc tính self trong instance method mục đích để trỏ tới chính instance hiện tại đang làm việc.
- Nhằm tạo ra sự tường mình khi các biến local của các hàm trùng với tên của các property.
Ví dụ code nhóe!
struct Point { var x = 0.0, y = 0.0 func isToTheRightOf(x: Double) -> Bool { return self.x > x } }
Trong đó:
- Tại function
isToTheRightOf
, có biến local làx
. Nó trùng tên với thuộc tínhx
của lớp - Sử dụng
self
để xác định đúng thuộc tính & đúng phạm vi của biến local hay thuộc tính của lớp
Vấn đề tiếp theo, chính là kiểu tham trị (tức là các instance method của Struct).
- Thuộc tính của kiểu tham trị (như structure hoặc enumeration) mặc định không được thay đổi từ bên trong của instance method
- Muốn thay đổi thì instance method đó phải được khai báo bằng từ khoá
mutating
Ta cập nhật lại ví dụ trên, với sự thay đổi giá trị thuộc tính từ phương thức.
struct Point { var x = 0.0, y = 0.0 mutating func moveBy(x deltaX: Double, y deltaY: Double) { x += deltaX y += deltaY } }
Sử dụng từ khóa mutating
, lúc này sẽ hiểu là tạo ra một phiên bản mới (khác với instance đang thực thi). Sau khi tính toán thì ghi đè lên lại.
Type method
Các phương thức này cũng tương tự với Type Properties. Ta gọi là các Type Methods. Là các function mà được gọi bằng chính tên kiểu dữ liệu của chúng. Về khai báo, bạn vẫn có 2 cách với 2 từ khóa sau:
static func
: Không cho phép các lớp con override lại functionclass func
: Cho phép lớp con override lại function
Với Swift, bạn sẽ rất là loạn não, khi mà tất cả Class, Struct và Enum để có thể khai báo Type Method. Còn bây giờ, ta xem sơ qua ví dụ của Type Method nhóe!
class SomeClass { class func someTypeMethod() { // type method implementation goes here } } SomeClass.someTypeMethod()
Tóm tắt 2: Properties & Methods
Bạn đã đi qua được 2/3 bài viết rồi. Với 2 phần ở trên, bạn sẽ hệ thống hóa lại thuộc tính & phương thức có trong lập trình OOP với Swift. Ngoài ra, bạn cũng phân loại được chúng, mà cụ thể xoay quanh tính chất cơ bản là:
- Instance
- Type name
Phần tiếp theo, bạn sẽ ôn lại các tính chất cơ bản của OOP như:
- Abstract
- Encapsulate
- Inheritance
- Polymorphism
Thông qua các kiến thức Swift liên quan tới:
- Initializers
- Extension
- Access control
Ăn miếng nước & uống miếng bánh. Hít thở và tiếp tục nào!
Inheritance
Định nghĩa
Tính thừa kế (Inheritance) là một trong những tính chất quan trọng nhất của OOP. Và trong ngôn ngữ lập trình hiện đại nào thì cũng đều có nó hết. Việc thừa kế chỉ có trong các kiểu dữ liệu là Class mà thôi.
Thông qua việc kế thừa, một class có thể sử dụng các properties, methods, subscripts của 1 class khác và có thể override lại chúng. Trong đó:
- Super-class: Class cha được kế thừa
- Sub-class: Class con kế thừa properties, methods, subcripts của class cha
Và kế thừa trong Swift là đơn kế thừa. Nghĩa là 1 class chỉ được phép kế thừa từ 1 super-class mà thôi. Ta có cú pháp như sau:
<sub-class-name> : <super-class-name>
Dấu :
được sử dụng làm từ khóa cho việc kế thừa. Xem qua ví dụ sau nhóe!
class AClass { func doSomeThing() { print("Hello from AClass") } } class SubClass: AClass { } let baseObject = AClass() baseObject.doSomeThing() let enhancedObject = SubClass() enhancedObject.doSomeThing()
Khá đơn giản phải không nào. SubClass là lớp con của AClass, nên ta có thể gọi phương thức doSomeThing
từ thể hiện của SubClass. EZ Game!
Base Class
- Là class không kế thừa từ bất kỳ class nào
- Các class của Swift mà kế thừa từ NSObject
- Tương tự như các class của Objective-C
- Để gọi các method sử dụng: objc_msgSend()
- Các metadata sẽ được cung cấp lúc runtime
- Các class của swift mà không kế thừ từ NSObject
- Là các class của Objective-C, chỉ implement một số ít các phương thức để tương thích
- Không sử dụng objc_msgSend()
- không được cung cấp các metadata lúc runtime
- Để cải thiện hiệu suất trong swift thì khuyến cáo không nên kế thừa từ NSObject
Hơi khó hiểu phải không nào. Nhưng thực tế nó là vậy. Khi Swift là người kế nhiệm của Objective-C, tức là nó mang trong mình đầy đủ các tính chất của Objective-C để lại.
Ví dụ: với kiểu dữ liệu là chuỗi, bạn có thể sử dụng NSString & String trong ngôn ngữ Swift. Với NSString là của Objective-C và String là của Swift.
Overriding
Định nghĩa
Ghi đè (override) là một kỹ thuật phổ biến trong lập trình OOP nói chung. Với Overriding trong Swift thì như sau:
- Subclass kế thừa những tính chất từ superclass, bên cạnh đó subclass có thể thay đổi những tính chất đó hoặc thêm những tính chất mới.
- Subclass có thể custom những instance method, type method, instance property, type property hay subscript mà nó thừa kế từ superclass thành của riêng nó. Việc này được xem là overriding.
- Overriding được tuỳ chỉnh sao cho phù hợp với ý đồ sử dụng trong subclass.
Khai báo
Các từ khóa mà bạn sẽ sử dụng là:
override
đặt phía trước tính chất cần overriding. Ví dụ:override func someMethod() { }
supper
để trỏ tới tính chất của lớp cha. Nó xem như là con trỏ của lớp cha trong lớp con.
Ta xem lại ví dụ trên với một chút cập nhật ghi đè nhóe.
class AClass { func doSomeThing() { print("Hello from AClass") } } class SubClass: AClass { override func doSomeThing() { super.doSomeThing() print("Hello from Subclass") } } let baseObject = AClass() baseObject.doSomeThing() let enhancedObject = SubClass() enhancedObject.doSomeThing()
Trong đó:
- Tại SubClass, ta tiến hành ghi đè lại
doSomeThing()
từ AClass - Trong function
doSomeThing()
ở lớp con, ta lại sử dụngsuper
để gọi tớidoSomeThing()
của lớp cha - Kết quả thực thi với instance của lớp con sẽ in ra 2 lần
Đây cũng được xem là một thể hiện của tính Đa hình (Polymorphism) trong lập trình OOP.
Prevent overriding
Đôi khi có trường hợp thèn con mất dạy quá, là cha của nó mà bạn không muốn nó kế thừa lại một vài tính chất. Lúc này, chúng ta sẽ sử dụng từ khóa final
. Với final
có nghĩa thông báo cho lớp con biết các thuộc tính hay phương thức đó là không ghi đè được.
Ví dụ:
final var someVariance = 0 final func someFuntion() { ... }
Khi bạn quá lười, bạn có thể sử dụng từ khóa final
cho việc khai báo class. Đánh dấu đây là lớp cuối cùng rồi, hoàn toàn không thể tạo nên các lớp con từ nó được nữa.
final class SomeClass { func someMethod() { print("Hello") } }
Điều này cũng góp phần tăng tốc độ biên dịch chương trình của bạn. Nhất là khi Swift là một ngôn ngữ với khả năng nội suy kiểu dữ liệu rất phức tạp.
Initialization
Định nghĩa
Hàm khởi tạo (Initialization) là một phần quan trong trong Class/Struct/Enume, cũng như trong lập trình OOP với Swift. Đó là quá trình chuẩn bị cho việc sử dụng một instance của một Class/Struct/Enume.
Quá trình này bao gồm:
- Cài đặt những giá trị ban đầu cho mỗi một thuộc tính của instance
- Thực hiện những setup hay những initialization khác mà cần thiết phải làm trước khi instance được sử dụng.
Đơn giản là việc cung cấp đầy đủ các giá trị cho toàn bộ thuộc tính.
Quá trình này được khai báo bởi initializer, là một method đặc biệt. Dùng để khởi tạo một instance mới của một kiểu dữ liệu nhất định. Không có giá trị trả về như ở Objective-C. Hay còn gọi là:
init()
Khai báo
Tương tự như function khác trong Swift, initializer cũng có argument label và parameter name. Ví dụ:
init(argumentLabel parameterName: Int) { // Initialization’s body }
Bạn cần chú ý:
- Class và structure phải set tất cả các giá trị mặc định thích hợp cho các thuộc tính trước khi instance của class hay structure được khởi tạo.
- Đối với những thuộc tính có kiểu optional, có thể không khởi tạo giá trị cho nó.
- Đối với những class hoặc structure mà đã cung cấp giá trị ban đầu cho tất cả các thuộc tính và không có hàm khởi tạo, Swift sẽ cung cấp hàm
init()
mặc định cho những class và structure trên. - Đặc biệt ở structure có auto-generated memberwise initializer.
Ví dụ sử dụng
Đoạn code sau sẽ có 2 lỗi, mặc dù nó có 4 dòng.
class Person { var name: String var phoneNumber: String? }
Trong đó:
- Thuộc tính
name
không có giá trị mặc định lúc khai báo - Không có hàm khởi tạo để
set
giá trị mặc định cho thuộc tínhname
Cách fix lỗi thứ 1:
- Hàm khởi tạo với từ khoá
init
set giá trị mặc định thích hợp cho các thuộc tính. - Vì class Person là lớp cơ sở, cho nên không xuất hiện từ khoá
super
trong hàm khởi tạo.
class Person { var name: String var phoneNumber: String? init(name: String) { self.name = name } }
Cách fix lỗi thứ 2:
- Với các thuộc tính optional, không cần thiết phải gán giá trị mặc định cho nó.
class Person { var name: String? var phoneNumber: String? }
Về kinh nghiệm, mình không khuyến kích bạn thực hiện cách fix lỗi theo kiểu thứ 2 này.
Class Inheritance & Initialization
Đây là phần rối não nhất mà Swift mang lại cho bạn. Vì hàm khởi tạo cũng có hàm khởi tạo this, hàm khởi tạo that … tạo nên sự phức tạp không đáng có của Swift. Và chúng đến từ việc xử lý kế thừa khá rườm ra. Tuy nhiên, nó đem lại sức mạnh cho bạn rất nhiều.
Trước tiên chúng ta cần phần loại các loại initializer trong Swift trước nhé!
Phân loại
Designated initializers:
- Hàm khởi tạo cơ sở của class.
- Khởi tạo đầy đủ các thuộc tính của class.
- Gọi hàm khởi tạo thích hợp của lớp cha để tiếp tục chuỗi khởi tạo
- Mỗi class có ít nhất một designated initializer
- Cú pháp như hình dưới
Convenience initializers:
- Hàm khởi tạo phụ trợ của class.
- Gọi hàm designated initializer trong cùng class.
- Thường được dùng để khởi tạo instance đối với một use case phổ biến hoặc với giá trị đầu vào đặc biệt…
- Không bắt buộc phải có convenience initializer trong class
- Cú pháp với từ khóa như sau
(Đọc thêm về Convenience initializers tại đây.)
Ví dụ sử dụng
Đầu tiên, ta có một lớp cơ sở là BaseClass như sau:
class BaseClass { var a: String init(a: String) { self.a = a } }
Tại BaseClass, thì Designated initializer phải đảm bảo tất cả properties phải được set giá trị trước khi instance được khởi tạo.
Ta lại tiếp tục với ví dụ một lớp con là SubClass kế thừa lại BaseClass.
class SubClass: BaseClass { var b: String init(a: String, b: String) { self.b = b super.init(a: a) } }
Vì phải theo nguyên tắc là phải cung cấp đầy đủ các giá trị cho toàn bộ các thuộc tính. Nên Designated initializer của SubClass phải gọi Designated initializer của class cha (hay là BaseClass trong ví dụ) gần nhất với nó.
Độ phức tạp của ví dụ sẽ tăng lên khi ta thêm thuộc tính mới nữa như sau:
class SubClass: BaseClass { var b: String var c: String = "" init(a: String, b: String) { self.b = b super.init(a: a) } convenience init(a: String, b: String, c: String) { self.init(a: a, b: b) self.c = c } }
Với mục đích, bạn không muốn thay đổi quá nhiều cấu trúc của lớp có sẵn. Thì Convenience initializer phụ trợ cho designated initializer. Convenience initializer phải gọi initializer của cùng một class và phải gọi designated initializer.
So sánh
Tại sao trời sinh class có designated init và convenience init làm chi cho đau khổ?
- Structure hay enumeration không có tính kế thừa, thì việc khởi tạo khá đơn giản
- Sử dụng hàm
init
do chúng tự cung cấp - Nếu có thì chỉ cần gọi hàm
init
lồng trong hàminit
- Sử dụng hàm
- Class có tính kế thừa, việc khởi tạo phức tạp hơn, trước khi instance được khởi tạo:
- Các thuộc tính của lớp con đều có giá trị. Designated init phải đảm bảo điều này
- Các thuộc tính của lớp cha đều có giá trị. Gọi Designated init của lớp cha gần nhất
- Phụ trợ lớp cha cho việc khởi tạo những pattern phổ biến, thì sử dụng convenience init
Extension
Đây thực sự là một phần mình khá thích trong Swift. Nó giúp trả lời câu hỏi:
Có cách nào để thêm các function cho một lớp có sẵn mà không phải tạo các lớp kế thừa lại nó?
Với từ khóa là extension
, bạn thêm các chức năng mới cho một class, structure, enumeration hoặc protocol. Extension có thể:
- Thêm các properties và methods
- Khai báo initializers, subscripts mới
- Làm cho kiểu dữ liệu phù hợp với protocol khác
Cú pháp như sau:
extension SomeType { // new functionality to add to SomeType goes here } extension SomeType: SomeProtocol, AnotherProtocol { // implementation of protocol requirements goes here }
Và Extension hữu ích khi:
- Nhóm các function hay implement các function của các protocol cần đường định nghĩa lại trong file class
- Thêm các function khi muốn can thiệp các class của hệ thống
Xem ví dụ nhóe!
protocol ExampleProtocol { var simpleDescription: String { get } mutating func adjust() } extension Int: ExampleProtocol { var simpleDescription: String { return "The number \(self)" } mutating func adjust() { self + 42 } } print(7.simpleDescription)
Trong đó:
- Bạn có một protocol là ExampleProtocol với các thuộc tính & phương thức khai báo
- Sau đó bạn tiếp tục mở rộng lớp Int, với việc
extension
và conform ExampleProtocol - Lúc này, một thể hiện của lớp Int sẽ có thêm các thuộc tính & phương thức mới
Với Extension cũng được xem là cách bạn biến Swift thành Đa thừa kế.
Access control
Access control chính là các cấp độ cho phép truy cập trong Swift. Nó áp dụng cho tất cả những gì mà Swift có, hay những gì của bạn tạo ra. Chúng ta có 5 mức truy cập tương ứng với 5 từ khóa như sau.
open
- Có thể truy cập từ bên ngoài, cho sử dụng ngoài module hay project
- Có thể kế thừa và sử dụng lại toàn bộ các thuộc tính cũng như phương thức
public
- Tương tự như open nhưng việc kế thừa và override lại bị giới hạn
- Chỉ có các thành viên cùng module thì mới có thể kế thừa
internal
- Cho phép sử dụng bất kì các file source code….trong cùng module
- Đây là mức truy cập mặc định
fileprivate
- Chỉ cho phép sử dụng bên trong file source code
- Có thể sử dụng từ các extension
private
- Hạn chế tất cả các quyền truy cập
Ngoài ra, với final thì cũng được xem là 1 mức truy cập đặc biệt
- Ngăn chặn việc override các phương thức cũng như kế thừa class
- Có thể truy cập các phương thức và thuộc tính với khai báo (public và open). Tuy nhiên không thể kế thừa lại chúng
- Khuyến cáo nên sử dụng để tăng tốc độ biên dịch của Swift
Tạm kết
Bài viết cũng khá là dài. Tuy nhiên, tất cả ở trên chỉ là những gì cơ bản nhất của lập trình hướng đối tượng (OOP) với Swift mà thôi. Vẫn còn nhiều phần mới, hoặc các phần mình chưa bổ sung thêm vào (như: protocol, subscript, deintit, wrapper property, actor …). Nhưng cũng cũng an tâm, vì các phần đó là phần bổ sung. Còn về bài viết này sẽ giúp bạn nắm được:
- Các khái niệm cơ bản trong lập trình hướng đối tượng
- Ba khái niệm lớn nhất là Class, Struct và Enum trong Swift
- Hai thành phần quan trọng là thuộc tính & phương thức có trong OOP của Swift
- Các tính chất của lập trình hướng đối tượng được thể hiện trong Swift
Okay! Tới đây, mình xin kết thúc bài viết về Lập trình hướng đối tượng với Swift . 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!
Related Posts:
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!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- 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)