
Most good programmers do programming not because they expect to get paid or adulation by the public, but because it is fun to program.
- Linus Torvalds
如果你是從頭開始閱讀本書,與我們一起進行所有專案的學習,那麼你已經向前邁進了一大步。到目前為止,你應該能夠使用 SwiftUI 來建立清單式的 iOS App 了,我們將繼續增強 FoodPin App,並加入更多的功能,但是在深入介紹 iOS App 開發和探索其他的 API 之前,我想向你介紹物件導向程式設計(Object Oriented Programming )的基礎知識, 並教導你如何編寫更佳的程式碼。
不要被「物件導向程式設計」或簡稱「OOP」的專有名詞所嚇到,它並不是一種新的程式語言,而是一種程式設計觀念。雖然坊間一些程式設計書籍一開始就介紹 OOP 的觀念,但我在規劃本書內容時,便打算在比較後面的章節才來介紹它,我想讓事情變得有趣,並向你介紹如何建立 App,我可不想讓一些技術術語或觀念嚇跑了你,不過我想是時候介紹 OOP 觀念了。如果讀完八章之後,你還在閱讀本書,我相信你已經下定決心要學好 iOS 程式設計,並希望提升自己的程式設計技能到更進階的水準,來成為一位專業的開發者。
好的,讓我們開始吧 !
自從建立第一個 App 以來,你一直在使用 struct,但是我還沒有解釋它是什麼。在我們深入研究 struct 之前,我先簡短介紹一下物件導向程式設計(OOP)。
和 Objective-C 及許多的其他程式語言類似,Swift 被認為是一種物件導向程式設計(OOP)語言,OOP 是一種使用物件建立應用軟體的方式。換句話說,在 App 中編寫的程式碼是以各種方式處理物件,你使用過的 View、Button 與 List 物件都是 SwiftUI 框架提供的範例物件。此外,在你的專案中你已建立了你自己的物件,如 RestaurantListView。
首先,為什麼 OOP 很重要?一個重要的理由是,它可讓我們將複雜的軟體分解成更小、更易於管理的部分或是建立模塊,這些較小的部分稱為「物件」,每個物件都有它的職責,物件間相互合作來讓軟體發揮作用,這就是OOP 背後的基本觀念。
在物件導向程式設計中,物件具有二個主要特徵:「屬性」(Property )與「功能」(Functionality )。為了說明,我們以一個真實世界汽車物體來說明,汽車具有顏色、型號、最高時速、製造商等屬性,這些屬性定義了汽車的特徵;在功能方面,汽車應該能夠執行加速、剎車及駕駛等基本操作,這些功能代表了與汽車相關的動作或行為。
軟體物件在概念上與真實世界物件很相似,我們回到 iOS 世界中,來看一下 Button 物件的屬性及功能:
在前面的章節中,你總是會碰到一個術語—「方法」(Method )。在Swift 中,我們會建立方法來提供物件的功能,通常一個方法對應一個物件的特定功能。
除了方法與物件之外,你也遇過「類別」(Class )與結構(Structure )等術語,這些都是物件導向程式設計(OOP)的常見術語,我將對每個術語進行簡要介紹。
「類別」是建立物件的藍圖或原型。基本上,類別是由屬性與方法所組成,我們以 Course 類別為例,Course 類別包含「課程名稱」、「課程代號」、「學生總數」等屬性。
這個類別代表課程的藍圖,我們可以用它來建立不同的課程,例如:iOS 程式設計課程(代號是 IPC101)、烹飪課程(代號是 CC101)等,這裡的 iOS 程式設計課程和烹飪課程就是 Course 類別的物件。我們通常將單一課程作為 Course 類別的實例(Instance )。為了簡單起見,「實例」與「物件」這兩個術詞有時可以交換使用。
設計房子的藍圖就像是一個類別敘述,根據該藍圖所建造的所有房屋都是該類別的物件,指定的房子就是一個實例。
出處: http://stackoverflow.com/questions/3323330/difference-between-object-and-instance
結構與類別是通用、靈活的結構,它們成為你的程式碼的構件。你可以使用和定義常數、變數、函數相同的語法來定義屬性與方法,以在你的結構與類別中加入功能。
- Apple 文件 (https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html)
除了類別以外,你可以在 Swift 中使用結構( structures 或 structs )來建立具有屬性與方法的你自己的型別。Swift 中的結構與類別有許多相似之處,並具有幾個共同的特徵,兩者皆可以定義用來儲存值的屬性以及用來提供功能的方法,兩者也可以建立自己的初始化來設定物件的初始狀態。
然而,在繼承(Inheritance )方面,Swift 中的類別和結構之間存在著重要差異,結構不支援繼承,這意味著你不能從一個結構繼承另一個結構,這是 Swift 中類別與結構之間的關鍵區別。
Swift 中的型別分為兩種類型:「實值型別」(Value Types )與「參考型別」(Reference Types )。Swift 中的所有結構都被視為實值型別,而類別則被視為參考型別,這種區別是類別與結構之間的另一個根本區別。對於結構,每個實例都有其資料的唯一副本;相反的,參考型別(類別)共享資料的單一副本。當你將類別的實例指定給另一個變數時,不是複製該實例的資料,而是使用該實例的參考。
為了說明實值型別(結構)與參考型別(類別)之間的差異,我利用一個範例來示範。在下列的程式碼片段中,我們定義一個名為「Car」的類別,其屬性名為「brand」。我們建立 Car 的實例,並將其指定給名為「car1」的變數,然後我們將 car1 的值指定給另一個名為「car2」的變數,最後我們修改 car1 的 brand 值。
class Car {
var brand = "Tesla"
}
var car1 = Car()
var car2 = car1
car1.brand = "Audi"
print(car2.brand)
你猜出 car2 的 brand 值嗎?是 Tesla 或 Audi 呢?答案是「Audi」,這就是參考型別的本質。car1 與 car2 都參考相同的實例,共享同一個資料副本。
相反的,如果你使用結構(即實值型別)來重寫同一段程式碼,你將觀察到不同的結果。
struct Car {
var brand = "Tesla"
}
var car1 = Car()
var car2 = car1
car1.brand = "Audi"
print(car2.brand)
在本例中,只有 car1 的 brand 值更新為 Audi,對 car2 來說,它的品牌還是 Tesla,因為每個實值型別的變數皆有自己的資料副本,圖 9.1 視覺化說明了類別與結構的區別。

由於類別與結構皆提供相似的功能,問題來了,則你應該使用哪一種呢?作為一般準則,建議在自己的型別時預設使用結構。這是 Apple 在其關於選擇結構和類別的文件中所推薦的方式( Choosing Between Structures and Classes | Apple Developer Documentation )但是如果你需要其他像是繼承的功能,則建議選擇類別而不是結構。
那麼,為什麼我們要在本章中介紹OOP 呢?沒有比用案例來解釋觀念的更棒方式了,讓我們再次以FoodPin 專案 ( http://www.appcoda.com/resources/swift59/swiftui-foodpin-list-selection-exercise.zip )來做說明。
在 RestaurantListView 結構中,我們建立多個陣列來儲存餐廳的名稱、型別、位置與圖片。
var restaurantNames = ["Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petite Oyster", "For Kee Restaurant", "Po's Atelier", "Bourke Street Bakery", "Haigh's Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats And Deli", "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donostia", "Royal Oak", "CASK Pub and Kitchen"]
var restaurantImages = ["cafedeadend", "homei", "teakha", "cafeloisl", "petiteoyster", "forkee", "posatelier", "bourkestreetbakery", "haigh", "palomino", "upstate", "traif", "graham", "waffleandwolf", "fiveleaves", "cafelore", "confessional", "barrafina", "donostia", "royaloak", "cask"]
var restaurantLocations = ["Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Sydney", "Sydney", "Sydney", "New York", "New York", "New York", "New York", "New York", "New York", "New York", "London", "London", "London", "London"]
var restaurantTypes = ["Coffee & Tea Shop", "Cafe", "Tea House", "Austrian / Causual Drink", "French", "Bakery", "Bakery", "Chocolate", "Cafe", "American / Seafood", "American", "American", "Breakfast & Brunch", "Coffee & Tea", "Coffee & Tea", "Latin American", "Spanish", "Spanish", "Spanish", "British", "Thai"]
@State var restaurantIsFavorites = Array(repeating: false, count: 21)
所有這些資料實際上都與餐廳清單有關,但為什麼我們需要將它們分成多個陣列呢? 你是否想過我們是否可以將這些資料群組在一起?
在物件導向程式設計中,這些資料可以被視為餐廳的屬性,我們可以建立一個 Restaurant 結構來表示一間餐廳,並將多間餐廳儲存在一個 Restaurant 物件的陣列中,而不是將這些資料儲存在個別的陣列中,如圖 9.2 所示。
現在,我們對 FoodPin 專案進行一些修改。我們將建立 Restaurant 結構,並將程式碼轉換為使用 Restaurant 物件清單。
首先,我們從建立 Restaurant 結構開始。為此,在專案導覽器中的「FoodPin」資料夾上按右鍵,並於選單中選擇「New File...」,這裡我們不延續使用 iOS SDK 所提供的 UI 物件,而是建立一個全新的結構,因此選取「Source」下的「Swift File」模板,並點選「Next」按鈕,然後將檔案命名為「Restaurant.swift」,並儲存至專案資料夾中,如圖 9.3 所示。

完成之後,在 Restaurant.swift 檔中使用下列的程式碼宣告 Restaurant 結構:
struct Restaurant {
var name: String
var type: String
var location: String
var image: String
var isFavorite: Bool
init(name: String, type: String, location: String, image: String, isFavorite: Bool) {
self.name = name
self.type = type
self.location = location
self.image = image
self.isFavorite = isFavorite
}
init() {
self.init(name: "", type: "", location: "", image: "", isFavorite: false)
}
}
要定義結構,你可以使用 struct 關鍵字。上列的程式碼導入一個具有 name、type、location、image、isFavorite 等五個屬性的 Restaurant 結構,除了 isFavorite 屬性是布林型別(Bool )之外,其餘屬性都是字串型別(String )。對於每個屬性,你可以選擇設定預設值或明確指定型別,在本例中我們選擇後面的作法。
「初始化」是一個結構(或類別)實例的準備程序。當你建立一個物件時,將呼叫初始化器,以在該實例準備好使用之前,為該實例上的每個儲存屬性設定初始值,並執行任何其他的設定。你可以使用 init 關鍵字來定義初始化器,其最簡單的形式如下所示:
init() {
}
你也可以自訂一個初始化器來接收輸入的參數,就如同我們在 Restaurant 結構中定義的那樣。我們的初始化器有五個參數,每一個參數都有其名稱,並明確指定一個型別。在初始化器中,它使用給定的值來初始化屬性的值。
要建立一個 Restaurant 結構的實例,語法如下:
Restaurant(name: "Thai Cafe", type: "Thai", location: "London", image: "thaicafe", isFavorite: false)
你可以定義多個能夠接收不同參數的初始化器。為了方便起見,在程式碼中我們建立另一個初始化器:
init() {
self.init(name: "", type: "", location: "", image: "", isFavorite: false)
}
沒有這個初始化器,你可以像這樣初始化一個空的 Restaurant 物件:
Restaurant(name: "", type: "", location: "", image: "", isFavorite: false)
現在使用便利的初始化器,你可以像這樣初始化相同的物件:
Restaurant()
這可以讓你省下每次初始化一個空的 Restaurant 物件時,需要輸入所有初始化參數的時間。
在 Swift 中,使用初始化器內的 self 關鍵字來區分屬性名稱與參數。由於初始化器中的參數與屬性具有相同的名稱,因此 self 是用來參照結構或類別的屬性,這有助於在初始化器範圍內闡明及區分兩者,如圖 9.4 所示。

你可以為每個屬性指定預設值並省略初始化器。這裡,Swift 將在背後自動產生預設的初始化器,因此 Restaurant 結構的簡化版本可以編寫如下:
struct Restaurant {
var name: String = ""
var type: String = ""
var location: String = ""
var image: String = ""
var isFavorite: Bool = false
}
對類別、結構與物件初始化有了基本的概念後,我們回到 FoodPin 專案,並將目前的陣列結合為一個 Restaurant 物件的陣列。首先,將 RestaurantListView 結構中與餐廳有關的陣列刪除:
var restaurantNames = ["Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petite Oyster", "For Kee Restaurant", "Po's Atelier", "Bourke Street Bakery", "Haigh's Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats And Deli", "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donostia", "Royal Oak", "CASK Pub and Kitchen"]
var restaurantImages = ["cafedeadend", "homei", "teakha", "cafeloisl", "petiteoyster", "forkee", "posatelier", "bourkestreetbakery", "haigh", "palomino", "upstate", "traif", "graham", "waffleandwolf", "fiveleaves", "cafelore", "confessional", "barrafina", "donostia", "royaloak", "cask"]
var restaurantLocations = ["Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Hong Kong", "Sydney", "Sydney", "Sydney", "New York", "New York", "New York", "New York", "New York", "New York", "New York", "London", "London", "London", "London"]
var restaurantTypes = ["Coffee & Tea Shop", "Cafe", "Tea House", "Austrian / Causual Drink", "French", "Bakery", "Bakery", "Chocolate", "Cafe", "American / Seafood", "American", "American", "Breakfast & Brunch", "Coffee & Tea", "Coffee & Tea", "Latin American", "Spanish", "Spanish", "Spanish", "British", "Thai"]
@State var restaurantIsFavorites = Array(repeating: false, count: 21)
將上列的陣列以新的 Restaurant 物件的陣列取代:
@State var restaurants = [
Restaurant(name: "Cafe Deadend", type: "Coffee & Tea Shop", location: "Hong Kong", image: "cafedeadend", isFavorite: false),
Restaurant(name: "Homei", type: "Cafe", location: "Hong Kong", image: "homei", isFavorite: false),
Restaurant(name: "Teakha", type: "Tea House", location: "Hong Kong", image: "teakha", isFavorite: false),
Restaurant(name: "Cafe loisl", type: "Austrian / Causual Drink", location: "Hong Kong", image: "cafeloisl", isFavorite: false),
Restaurant(name: "Petite Oyster", type: "French", location: "Hong Kong", image: "petiteoyster", isFavorite: false),
Restaurant(name: "For Kee Restaurant", type: "Bakery", location: "Hong Kong", image: "forkee", isFavorite: false),
Restaurant(name: "Po's Atelier", type: "Bakery", location: "Hong Kong", image: "posatelier", isFavorite: false),
Restaurant(name: "Bourke Street Backery", type: "Chocolate", location: "Sydney", image: "bourkestreetbakery", isFavorite: false),
Restaurant(name: "Haigh's Chocolate", type: "Cafe", location: "Sydney", image: "haigh", isFavorite: false),
Restaurant(name: "Palomino Espresso", type: "American / Seafood", location: "Sydney", image: "palomino", isFavorite: false),
Restaurant(name: "Upstate", type: "American", location: "New York", image: "upstate", isFavorite: false),
Restaurant(name: "Traif", type: "American", location: "New York", image: "traif", isFavorite: false),
Restaurant(name: "Graham Avenue Meats", type: "Breakfast & Brunch", location: "New York", image: "graham", isFavorite: false),
Restaurant(name: "Waffle & Wolf", type: "Coffee & Tea", location: "New York", image: "waffleandwolf", isFavorite: false),
Restaurant(name: "Five Leaves", type: "Coffee & Tea", location: "New York", image: "fiveleaves", isFavorite: false),
Restaurant(name: "Cafe Lore", type: "Latin American", location: "New York", image: "cafelore", isFavorite: false),
Restaurant(name: "Confessional", type: "Spanish", location: "New York", image: "confessional", isFavorite: false),
Restaurant(name: "Barrafina", type: "Spanish", location: "London", image: "barrafina", isFavorite: false),
Restaurant(name: "Donostia", type: "Spanish", location: "London", image: "donostia", isFavorite: false),
Restaurant(name: "Royal Oak", type: "British", location: "London", image: "royaloak", isFavorite: false),
Restaurant(name: "CASK Pub and Kitchen", type: "Thai", location: "London", image: "cask", isFavorite: false)
]
這個新陣列也使用 @State 屬性包裹器來標註,因為我們需要更新 isFavorite 的值。
當你用 restaurants 陣列取代原先的陣列之後,Xcode 中會出現一些錯誤,這是因為有些程式碼仍然參照舊陣列,如圖 9.5 所示。

為了修復這些錯誤,我們必須修改程式碼來使用新的 restaurants 陣列,如下所示:
ForEach(restaurants.indices, id: \.self) { index in
BasicTextImageRow(imageName: restaurants[index].image, name: restaurants[index].name, type: restaurants[index].type, location: restaurants[index].location, isFavorite: $restaurants[index].isFavorite)
}
現在我們使用 Restaurant 物件的陣列而不是餐廳名稱的陣列來顯示餐廳,我們可以透過存取 Restaurant 物件的屬性來存取餐廳資料。
實踐這些修改後,所有的錯誤都應該修正了,現在可以執行你的 App,該 App 的外觀和功能保持不變,但是我們已經重構程式碼來使用新的 Restaurant 結構。透過將多個陣列合併為一個,程式碼現在更為簡潔且可讀性更高。
我們可以更進一步重構程式碼。在 BasicTextImageRow 結構中,我們有五個參數:
var imageName: String
var name: String
var type: String
var location: String
@Binding var isFavorite: Bool
每一個參數實際上都是 Restaurant 結構的屬性,因此與其個別宣告這些參數,不如將它們合而為一,如下所示:
@Binding var restaurant: Restaurant
我們請求呼叫者向我們提供 Restaurant 物件的綁定,進行這些更改後,你可能會遇到一些錯誤,要解決它們,你必須更新程式碼,以使用 restaurant 物件,例如:你現在應該使用 restaurant.image 而不是 imageName。
如果你已正確修正這些錯誤,你的程式碼應如下所示:
struct BasicTextImageRow: View {
@Binding var restaurant: Restaurant
@State private var showOptions = false
@State private var showError = false
var body: some View {
HStack(alignment: .top, spacing: 20) {
Image(restaurant.image)
.resizable()
.frame(width: 120, height: 118)
.clipShape(RoundedRectangle(cornerRadius: 20))
VStack(alignment: .leading) {
Text(restaurant.name)
.font(.system(.title2, design: .rounded))
Text(restaurant.type)
.font(.system(.body, design: .rounded))
Text(restaurant.location)
.font(.system(.subheadline, design: .rounded))
.foregroundStyle(.gray)
}
if restaurant.isFavorite {
Spacer()
Image(systemName: "heart.fill")
.foregroundStyle(.yellow)
}
}
.onTapGesture {
showOptions.toggle()
}
.confirmationDialog("What do you want to do?", isPresented: $showOptions, titleVisibility: .visible) {
Button("Reserve a table") {
self.showError.toggle()
}
Button(restaurant.isFavorite ? "Remove from favorites" : "Mark as favorite") {
restaurant.isFavorite.toggle()
}
}
.alert("Not yet available", isPresented: $showError) {
Button("OK") {}
} message: {
Text("Sorry, this feature is not available yet. Please retry later.")
}
}
}
我們還沒有完成,還有幾個錯誤等待我們修正。首先,將 RestaurantListView 中的下列程式碼:
BasicTextImageRow(imageName: restaurants[index].image, name: restaurants[index].name, type: restaurants[index].type, location: restaurants[index].location, isFavorite: $restaurants[index].isFavorite)
改為:
BasicTextImageRow(restaurant: $restaurants[index])
我們不單獨傳送參數,而是將 BasicTextImageRow 綁定傳送給 Restaurant 物件。
對於 BasicTextImageRow 的預覽程式碼,我們還需要進行一些修改,如下所示:
#Preview("BasicTextImageRow", traits: .sizeThatFitsLayout) {
BasicTextImageRow(restaurant: .constant(Restaurant(name: "Cafe Deadend", type: "Cafe", location: "Hong Kong", image: "cafedeadend", isFavorite: true)))
}
如此,儘管 App 的外觀及感覺依然相同,但是程式碼現在看起來更簡潔了。
當我們繼續建立 App 時,我們將在專案資料夾中建立更多的檔案,因此我想要藉此機會向你展示一種更好組織專案的技術。
我們先來檢查專案導覽器,目前你建立的所有檔案都放置在 FoodPin 資料夾的最上層, 隨著你加入更多的檔案,要找到特定的檔案可能會變得越來越困難。為了加強專案檔的組織,Xcode 提供「群組」(Group )功能,可以讓你將檔案組織到群組或資料夾中。
有幾個群組檔案的方式,你可以將它們依照特點或功能來群組,較推薦的作法是依照職責來將它們分組,例如:視圖可以分組在 View 下,而像Restaurant 這樣的模型類別則可以分組在 Model 下。
要在專案導覽器中建立群組,則在「FoodPin」資料夾按右鍵,並選擇「New Group」, 然後命名為「View」,如圖 9.6 所示。

接下來,選取 RestaurantListView,並拖曳它到View 群組中。重複相同的步驟來分組到 Model 群組,並將 Restaurant 檔案拖曳到該群組中,如圖 9.7 所示。

如果你在 Finder 中開啟專案資料夾,你會發現所有檔案都整齊地組織到資料夾中(例如:Model 與 View ),每個資料夾對應到 Xcode 專案中的特定群組。
即使你已經將檔案移到不同資料夾,你仍然可以在不進行任何更改的情況下執行該專案,請一定要點擊「Run」按鈕並嘗試一下。
事實上,花在讀程式的時間對比寫程式的時間已經超過 10 比1。我們不斷地將讀舊程式當作在寫新程式工夫的一部分⋯。因此,讓程式更易於閱讀,才能更有利於寫程式。 ― Robert C. 《無瑕的程式碼:敏捷軟體開發技巧守則》
除了專案檔之外,還有一些更好組織原始碼的最佳作法,這裡我將向你展示一種強大的技術,可以將你的 Swift 程式碼組織成有用且易於閱讀的區塊。
如你所知,那些以「//」為開始的程式碼都是註解。「註解」是註記給自己或其他開發者(如果你是團隊開發的成員之一)的筆記來提供額外的資訊,例如:有關程式碼的意圖或解釋,其主要目的是讓程式碼更易於理解。
// 加入訂位動作
Swift 中還有另一種類型的註解,它以「// MARK: 」開頭,如以下的例子:
// MARK: - 綁定
MARK 是 Swift 中一種特殊註解的標記,讓你將程式碼組織成易於導覽的區塊。以 BasicTextImageRow 結構為例,有些變數是綁定,其他是狀態變數,我們可以利用 MARK 註解來將它們分成不同的區塊。
// MARK: - Binding
@Binding var restaurant: Restaurant
// MARK: - State variables
@State private var showOptions = false
@State private var showError = false
Now, when you click on the jump bar located at the top of your editor window, you will notice that the methods are organized into different meaningful sections. This allows for easier navigation and understanding of the code structure.

恭喜你又向前邁進一步了,我希望你對於本章內容不會覺得無趣。到目前為止,我們已經介紹了結構與物件導向程式設計( OOP)的基礎知識,此外我還向你展示了一些組織程式碼與專案檔的技術。
關於 OOP 的概念,還有許多需要學習,例如:多型( Polymorphism ),然而我們沒有足夠的時間在本書中深入研究它們,如果你想要成為專業的iOS 開發者,我建議你看看這裡提供的參考資料來進一步擴展你的知識。精通 OOP,需要大量的練習及實務經驗,儘管如此,若是你已經完成了本章,就有一個很好的開始。
在本章所準備的範例檔中,有最後完整的 Xcode 專案 https://www.appcoda.com/resources/swift59/swiftui-foodpin-oop.zip 可供你下載參考。在下一章中,根據我們所學到的知識,你將繼續調整 FoodPin App 的細節視圖畫面,這將會很有趣 !
Swift程式語言—類別與結構: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/classesandstructures/
Swift程式語言—初始化: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/
Swift程式語言—繼承: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/inheritance/
麻省理工學院(MIT)開放課程的物件導向程式設計: https://ocw.mit.edu/courses/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/resources/lecture-8-object-oriented-programming/