一、讀書筆記
3.2 對象和屬性
昨天我們學(xué)到KaraokeSong是Song的子類,但是我們并沒有指定Song類本身的父類是什么?如果你在定義一個類時沒有指定其父類,Ruby默認(rèn)以O(shè)bject類作為其父類,這意味著所有類的始祖都是object,并且Object的實(shí)例方法對Ruby的所有對象都可用。
我們現(xiàn)在創(chuàng)建的Song對象有內(nèi)部的狀態(tài)(例如歌曲名稱和演唱者),這些狀態(tài)是對象私有的——其他對象無法訪問一個對象的實(shí)例變量,總體來說,這是一件好事,這意味著只有對象才有責(zé)任維護(hù)其自身的一致性。
不過,完全密封的對象實(shí)在沒什么用處——你可以創(chuàng)建它,但接下來你對他什么都不能做,你通常定義一些訪問及操作對象的狀態(tài),讓外部世界得以與之交互,一個對象的外部可見部分被稱為其屬性(attribute)。
對我們的Song對象來說,需要做的第一件事情是找出歌曲名稱和演唱者(這樣我們可以在歌曲播放時顯示它們)以及時長(這樣我們可以顯示某種進(jìn)度條)。
某些面向?qū)ο笳Z言(例如C++)支持多繼承,在這些語言中,一個類可以有多于一個的直接父類,并繼承每個的功能,雖然強(qiáng)大,但這種技術(shù)有危險(xiǎn),會使繼承結(jié)構(gòu)便的混亂,其他語言,例如Java和C#,僅支持單繼承,其中,一個類只能有一個直接的父類,雖然清爽(并且容易實(shí)現(xiàn)),但是單繼承也有缺點(diǎn)——在現(xiàn)實(shí)世界中,對象通常從多個源繼承屬性(例如,足球是一個彈性的、圓球狀的東西)。
Ruby提供了一種有趣且強(qiáng)大的折中,給了你單繼承的簡單性以及多繼承的強(qiáng)大功能,Ruby類只有一個直接的父類,因此Ruby是一門單繼承語言,不過,Ruby類可以從任何數(shù)量的mixin(類似于一種部分的類定義)中引入(include)功能,這提供了可控的、類似多繼承的能力,而沒有多繼承的缺點(diǎn)。
舉個例子:
class Song
def name
@name
end
def artist
@artist
end
def duration
@duration
end
end
song = Song.new("Bicyclops", "Fleck", 260)
song.artist
song.name
song.duration
返回
"Fleck"
"B..."
260
這里,我們定義了3個方法得到這3個實(shí)例變量的值,舉例來說,方法name()返回實(shí)例變量@name的值,因?yàn)檫@是一個常見的慣用手法,Ruby提供了一種方便的快捷方式:attr_reader為你創(chuàng)建這些訪問方法。
class song
attr_reader :name, :artist, :duration
end
注意,attr_reader實(shí)際上是Ruby的一個方法,而:name等符號(Symbol)是其參數(shù),它會通過代碼求解(code evaluation)動態(tài)地在Song類中加入實(shí)例方法體,這也是Ruby meta-programming的一個示例。
song = Song.new("Bicyclops", "Fleck", 260)
song.artist # "Fleck"
song.name # "Bicyclops"
song.duration
這個示例引入了一些新的內(nèi)容,構(gòu)成體:artist是一個表達(dá)式,它返回對應(yīng)artist的一個Symbol對象,你可以將:artist看作是變量artist的名字,而普通的artist是變量的值,在這個例子中,我們將訪問方法命名為name、artist和duration。對應(yīng)的實(shí)例變量@name、@artist和@duration會自動被創(chuàng)建。這些訪問方法和我們早先手寫的那些是等同的。
3.2.1 可寫的屬性
有些時候,你需要能夠在一個對象外部設(shè)置它的屬性,例如,讓我們假定某一首歌的時長最初是預(yù)估得來的(可能從CD或MP3數(shù)據(jù)的信息中產(chǎn)生而來)。當(dāng)我們第一次播放這首歌時,會得到它實(shí)際的長度,并將這個新的值保存回Song對象中。
在類如C++和Java等語言中,你需要用setter方法來完成這個任務(wù)。
class JavaSong {
private Duration _duration;
public void setDuration(Duration newDuration) {
_duration = newDuration
}
}
s = new JavaSong(...);
s.setDuration(length);
在Ruby中,訪問對象屬性就像訪問其他變量一樣,我們在前面已經(jīng)看到,例如song.name語句,因此,當(dāng)你想要設(shè)置某個屬性的值時,對這些變量直接賦值似乎更自然些,在Ruby中,你可以通過創(chuàng)建一個名字以等號結(jié)尾的方法來達(dá)成這一目標(biāo),這些方法可以作為賦值操作的目標(biāo)。
class Song
def duration = (new_duration)
@duration = new_duration
end
end
song = Song.new("Bicyclops", "Fleck", 260)
song.duration # 260
song.duration = 257
song.duration # 257
賦值操作song.duration = 257 調(diào)用了song對象中的duration=方法,并將257作為參數(shù)傳入,實(shí)際上,定義一個以等號結(jié)尾的方法名,便使其能夠出現(xiàn)在復(fù)制操作的左側(cè)。
同樣,Ruby又提供了一種快捷方式創(chuàng)建這類簡單的屬性設(shè)置方法。
class Song
attr_writer :duration
end
song = Song.new("Bicyclops", "Fleck", 260)
song.duration = 257
3.2.2 虛擬屬性
這類屬性訪問的方法并不必須是對象實(shí)例變量的簡單包裝(wrapper),例如,你可能希望訪問以分鐘為單位,而不是我們已經(jīng)實(shí)現(xiàn)的以秒為單位的時長。
class Song
def duration_in_minutes
@duration/60.0
end
def duration_in_minutes=(new_duration)
@duration = (new_duration*60).to_i
end
end
song = Song.new("Bicyclops", "Fleck", 260)
song.duration_in_minutes # 4.33333333333333
song.duration_in_minutes = 4.2
song.duration # 252
這里我們使用屬性方法創(chuàng)建了一個虛擬的實(shí)例變量,對外部世界來說,duration_in_minutes就像其他屬性一樣,然而,在內(nèi)部它沒有對應(yīng)的實(shí)例變量。
這遠(yuǎn)非出于討巧,在Bertrand Meyer的劃時代著作Object-Oriented Software Construction中,他將它稱為統(tǒng)一訪問原則,通過隱藏實(shí)例變量與計(jì)算的值得差異,你可以向外部世界疲敝類的實(shí)現(xiàn),將來你可以自由地更改對象如何運(yùn)作,而不會影響使用了你的類的、數(shù)以百萬行計(jì)的代碼。這是一種很大的成功。
3.2.2 屬性、實(shí)例變量及方法
這種對屬性的描述,可能會讓你認(rèn)為它們無外乎就是方法——為什么我們要為它們發(fā)明一個新奇的名字呢?從某種程度上,這絕對是正確的,屬性就是一個方法,某些時候,屬性簡單的返回實(shí)例變量的值,某些時候,屬性返回計(jì)算后的結(jié)果,并且某些時候,那些名字以等號結(jié)尾的古怪方法,被用來更新對象的狀態(tài),那么問題是,哪里是屬性結(jié)束而一般方法開始的地方呢?是什么讓它變成屬性的呢?
當(dāng)你設(shè)計(jì)一個類的時候,你決定其具有什么樣的內(nèi)部狀態(tài),并決定這內(nèi)部狀態(tài)對外界(類的用戶)的表現(xiàn)形式。內(nèi)部狀態(tài)保存在實(shí)例變量中,通過方法暴露出來的外部狀態(tài),我們稱之為屬性。
你的類可以執(zhí)行的其他動作,就是一般方法,這并非一個非常重要的區(qū)別,但是通過把一個對象的外部狀態(tài)稱為屬性,可以幫助人們了解你所編寫的類。
3.3 類變量和類方法
到目前為止,所有我們創(chuàng)建的類都包括有實(shí)例變量和實(shí)例方法:變量被關(guān)聯(lián)到類的某個特定實(shí)例,以及操作這些變量的方法。有時候,類本身需要它們自己的狀態(tài)。這是類變量的領(lǐng)地、
3.3.1 類變量
類變量被類的所有對象共享,它與我們稍后描述的類方法相關(guān)聯(lián)。對一個給定的類來說,類變量只存在一份拷貝。類變量由兩個@符開頭,例如@@count。與全局變量和實(shí)例變量不同,類變量在使用之前必須被初始化,通常,初始化就是在類定義中簡單賦值。
class Song
@@play = 0
def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
@plays = 0
end
def play
@plays += 1
@@plays += 1
"This song: #@plays plays. Total #@@plays plays."
end
end
出于調(diào)試的目的,我們還讓Song#play返回一個字符串,其中包括該歌曲被播放的次數(shù),以及所有歌曲播放的總次數(shù),我們可以很容易測試它。
s1 = Song.new("Song1", "Artist1", 234)
s2 = Song.new("Song2", "Artist2", 345)
s1.play # "This song : 1 plays. Total 1 plays."
s2.play # "This song : 1 plays. Total 2 plays."
s1.play # "This song : 2 plays. Total 3 plays."
s2.play # "This song : 3 plays. Total 4 plays."
類變量對類及其實(shí)例都是私有的,如果你想要讓它們能夠被外部世界訪問,你需要編寫訪問方法,這個方法要么是一個實(shí)例方法,或者是緊接著在下一節(jié)要介紹的類方法。
3.3.2 類方法
有時,類需要提供不束縛于任何特定對象的方法。我們已經(jīng)見過一個這樣的方法,new方法創(chuàng)建一個新的Song對象,但是new方法本身并不與一個特定的歌曲對象相關(guān)聯(lián)。
song = Song.new(....)
我們會發(fā)現(xiàn)類方法遍布于Ruby庫中。例如,F(xiàn)ile類的對象用來表示在底層文件系統(tǒng)中打開的一個文件。不過,F(xiàn)ile類還提供了幾個類方法來操作文件,而它們并未打開文件,因此也沒有相應(yīng)的File對象,如果你想要刪除一個文件,你可以調(diào)用類方法File.delete,傳入文件名作為參數(shù)。
File.delete("domend.txt")
類方法和實(shí)例方法是通過它們的定義區(qū)別開來的,通過在方法名之前放置類名以及一個句點(diǎn),來定義方法。
class Example
def instance_method
end
def Example.class_method
end
end
點(diǎn)唱機(jī)按播放的每首歌曲收費(fèi),而非按分鐘收費(fèi),這使得短歌曲比長歌更有助于盈收。我們可能希望避免那些過長的歌曲出現(xiàn)在SongList中,我們可以在SongList中定義一個類方法檢查,是否某一首歌超過了時限。我們使用一個類的常量設(shè)置時限,就是在一個類代碼中初始化的常量(記得常量嗎?它們使用一個大寫字母開頭)。
class SongList
MAX_TIME = 5*60
def SongList.is_too_long(song)
return song.duration > MAX_TIME
end
end
song1 = Song.new("Bicyclops", "Fleck", 260)
SongList.is_too_long(song1) # false
song2 = Song.new("The Calling", "Santana" 468)
SongList.is_too_long(song2) # true
有些時候,你希望覆寫(override)Ruby默認(rèn)創(chuàng)建對象的方式。舉例來說,看看我們的點(diǎn)唱機(jī)。因?yàn)槲覀儗⒂性S多點(diǎn)唱機(jī)分布在全國各地,我們希望讓維護(hù)工作盡可能簡單,其中一個需求是記錄點(diǎn)唱機(jī)發(fā)生的所有事情:播放歌曲、收到的點(diǎn)歌費(fèi)、傾倒進(jìn)去的各種奇怪液體等等。
因?yàn)槲覀兿M麨橐魳繁A艟W(wǎng)絡(luò)寬帶,因此我們將日志文件保存本地。這意味著我們需要一個類來處理日志。不過,我們希望每個點(diǎn)唱機(jī)只有一個記錄日志的對象,并且我們希望其他所有對象都共享同一個日志對象。
這里適用Singleton模式,在《Design Patterns(設(shè)計(jì)模式)》一書中記載,我們將進(jìn)行一些調(diào)整,使得只有一種方式創(chuàng)建日志對象,那就是調(diào)用MyLogger.create,并且我們還保存只有一個日志對象被創(chuàng)建。
class MyLogger
private_class_method :new
@@logger = nil
def MyLogger.create
@@logger = new unless @@logger
@@logger
end
end
通過將MyLogger的new方法標(biāo)記為private(私有),我們阻止所有人使用傳統(tǒng)的構(gòu)造函數(shù)來創(chuàng)建日志對象。相反,我們提供了一個類方法,MyLogger.crate。這個方法使用了類變量@@logger來保存唯一一個日志對象實(shí)例的引用,并在每次被調(diào)用時返回。
實(shí)際上,你可以用很多方式來定義類方法,但是理解這些方式為什么有效,我們需要等到第24章。
下面是在類Demo內(nèi)定義類方法。
class Demo
def Demo.meth1
# ....
end
def self.meth2
# ...
end
class <<self
def meth3
# ...
end
end
end
這個實(shí)例,我們可以通過查看方法返回對象的標(biāo)識符來檢驗(yàn)。
MyLogger.create.object_id
MyLogger.create.object_id
使用類方法作為偽構(gòu)造函數(shù)(pseudo-constructors),可以讓使用類的用戶更輕松些。舉個簡單的例子,讓我們看一個表示等邊多邊形的Shape類。通過將所需的邊數(shù)和周長傳入構(gòu)造函數(shù)來創(chuàng)建Shape的實(shí)例。
class Shape
def initialize(num_sides, perimeter)
# ...
end
end
注意:我們這里演示的singleton實(shí)現(xiàn)并非是線程安全的;如果多個線程在運(yùn)行,有可能會創(chuàng)建多個日志對象。與其我們自己添加線程安全,不如使用Ruby提供的Singleton mixin。
不過,幾年之后,另一個不同的應(yīng)用使用了這個類,其中的程序員習(xí)慣于通過名字、并指定每條邊的長度而不是周長來創(chuàng)建圖形,只需要在Shape中添加若干類方法。
class Shape
def Shape.triangle(side_lenght)
Shape.new(3, side_lenght*3)
end
def Shape.square(side_lenght)
Shape.new(4, side_length*4)
end
end
類方法有許多有趣并強(qiáng)大的用途,為了使我們的點(diǎn)唱機(jī)產(chǎn)品盡可能快地完成,這里就不再繼續(xù)深入探究了,讓我們繼續(xù)前進(jìn)吧!
3.4 訪問控制(Access Controller)
當(dāng)我們設(shè)計(jì)類的接口時,需要著重考慮你的類想要向外部世界暴露何種程度的訪問。如果你的類允許過度的訪問,會增加應(yīng)用中耦合的風(fēng)險(xiǎn)——類的用戶可能會依賴于類實(shí)現(xiàn)的細(xì)節(jié),而非邏輯性的接口。好消息是,在Ruby中改變一個對象的狀態(tài),唯一的簡單方式是調(diào)用它的方法。
控制對方法的訪問,你就需要空控制對對象的訪問,一個經(jīng)驗(yàn)法則是,永遠(yuǎn)不要暴露會導(dǎo)致對象出于無效狀態(tài)的方法,Ruby為你提供了三中級別的保護(hù)。
- Public 沒有限制,方法默認(rèn)是public的(initialize除外,它是private的)
- Protected 只能被定義了該方法的類或其他子類的對象調(diào)用。整個家族均可訪問。
- Private 不能被明確的接受者調(diào)用,其接受者只能是self,這意味著私有方法只能在當(dāng)前對象的上下文中被調(diào)用,你不能調(diào)用另一個對象的私有方法。
“protected”和“private”之前的區(qū)別很微妙,并且和其他大多數(shù)普通的面相對象語言不同,如果方法是保護(hù)的,他可以被定義了該方法的類或者子類的實(shí)例所調(diào)用,如果方法是私有的,它只能在當(dāng)前對象的上下文中被調(diào)用——不可能直接訪問其他對象的私有方法,即便它與調(diào)用者都屬同一個類的對象。
Ruby和其他面向?qū)ο笳Z言的差異,還體現(xiàn)在另一個重要的方面,訪問控制是在程序運(yùn)行時動態(tài)判定的,而非靜態(tài)判定。只有當(dāng)代碼試圖執(zhí)行受限的方法,你才會得到一個訪問違規(guī)。
3.4.1 訪問控制(Specifying Access Control)
你可以使用public、protected和private3個函數(shù)(function),來為類或模塊定義內(nèi)的方法(method)指定訪問級別,你可以以兩種不同的方式使用每個函數(shù)。
如果使用時沒有參數(shù),這3個函數(shù)設(shè)置后續(xù)定義方法的默認(rèn)訪問控制,如果你是C++或者Java程序員,這應(yīng)該是你熟悉的行文,你同樣使用類似public的關(guān)鍵字來取得相同的效果。
class MyClass
def method
# ..
end
protected # subsequent method will be 'protected'
def method2 # will be 'protected'
# ..
end
private
def method3 # will be private
# ..
end
end
另外,你可以通過將方法名作為參數(shù)列表傳入訪問控制函數(shù)來設(shè)置它們的訪問級別。
class MyClass
def method
end
public :method1, :method4
private :method2
end
舉個例子,假設(shè)我們要為一個會計(jì)系統(tǒng)建立模型,其中每個借方都有對應(yīng)的信用,因?yàn)槲覀兿M_保沒有人破壞這個規(guī)則,所以我們讓有關(guān)借方和信用的方法均稱為私有。并定義外部的交易接口。
class Accounts
def initialize(checking, saving)
@checking = checking
@saving = savings
end
private
def debit(account, amount)
account.balance -=amount
end
def credit(account, amount)
account.balance +=amount
end
public
def transfer_to_savings(amount)
debit(@checking, amount)
credit(@savings, amount)
end
end
二、心得體會
今天完成了什么?
- 花了兩個小時看了《Programmin Ruby》3.2、3.3節(jié)
- 繼續(xù)看application.rb剩余部分
- admin/logs
今天收獲了什么?
- logs_controller.rb文件
class Admin...Controller
# Admin...Controller繼承A...Controller
def index
@records = model.includes(:or)
# 引用父類ApplicationController的model方法,把日志的每一條數(shù)據(jù)都查詢出來
super
end
end
- 日志視圖index
ruby:
.. = {
collections: %i[ search .. ], # 搜索集,%i生成一個Symbol數(shù)組
...
} #這是一個hash數(shù)據(jù)流,返回給table顯示
= render 'ad...ble', ..
- 日志視圖show
ruby:
fields = { # 返回每條日志的具體信息
o..d: {},
..
}
= render 'ad..m', fields: fields
- operator.rb
belongs_to ...n_key: :id #引用外鍵
has_many ...inverse_of ::op.. #模型的關(guān)聯(lián)是單向綁定。當(dāng)一個關(guān)聯(lián)為a:belongs_to時,inverse_of選項(xiàng)基??本上給我們雙向內(nèi)存綁定。https://www.viget.com/articles/exploring-the-inverse-of-option-on-rails-model-associations
has_many :.., -> { enabled }, through: :..是什么意思
has_many :.., through: :..
- “::”是什么意思? 命名空間的分隔符
- module Ruby中的module有兩個作用,一個是標(biāo)識命名空間,一個是mixin 具體參考http://phrogz.net/programmingruby/tut_modules.html