一、读书笔记
3.2 对象和属性
昨天我们学到KaraokeSong是Song的子类,但是我们并没有指定Song类本身的父类是什么?如果你在定义一个类时没有指定其父类,Ruby默认以Object类作为其父类,这意味着所有类的始祖都是object,并且Object的实例方法对Ruby的所有对象都可用。
我们现在创建的Song对象有内部的状态(例如歌曲名称和演唱者),这些状态是对象私有的——其他对象无法访问一个对象的实例变量,总体来说,这是一件好事,这意味着只有对象才有责任维护其自身的一致性。
不过,完全密封的对象实在没什么用处——你可以创建它,但接下来你对他什么都不能做,你通常定义一些访问及操作对象的状态,让外部世界得以与之交互,一个对象的外部可见部分被称为其属性(attribute)。
对我们的Song对象来说,需要做的第一件事情是找出歌曲名称和演唱者(这样我们可以在歌曲播放时显示它们)以及时长(这样我们可以显示某种进度条)。
某些面向对象语言(例如C++)支持多继承,在这些语言中,一个类可以有多于一个的直接父类,并继承每个的功能,虽然强大,但这种技术有危险,会使继承结构便的混乱,其他语言,例如Java和C#,仅支持单继承,其中,一个类只能有一个直接的父类,虽然清爽(并且容易实现),但是单继承也有缺点——在现实世界中,对象通常从多个源继承属性(例如,足球是一个弹性的、圆球状的东西)。
Ruby提供了一种有趣且强大的折中,给了你单继承的简单性以及多继承的强大功能,Ruby类只有一个直接的父类,因此Ruby是一门单继承语言,不过,Ruby类可以从任何数量的mixin(类似于一种部分的类定义)中引入(include)功能,这提供了可控的、类似多继承的能力,而没有多继承的缺点。
举个例子:
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个实例变量的值,举例来说,方法name()返回实例变量@name的值,因为这是一个常见的惯用手法,Ruby提供了一种方便的快捷方式:attr_reader为你创建这些访问方法。
class song
attr_reader :name, :artist, :duration
end
注意,attr_reader实际上是Ruby的一个方法,而:name等符号(Symbol)是其参数,它会通过代码求解(code evaluation)动态地在Song类中加入实例方法体,这也是Ruby meta-programming的一个示例。
song = Song.new("Bicyclops", "Fleck", 260)
song.artist # "Fleck"
song.name # "Bicyclops"
song.duration
这个示例引入了一些新的内容,构成体:artist是一个表达式,它返回对应artist的一个Symbol对象,你可以将:artist看作是变量artist的名字,而普通的artist是变量的值,在这个例子中,我们将访问方法命名为name、artist和duration。对应的实例变量@name、@artist和@duration会自动被创建。这些访问方法和我们早先手写的那些是等同的。
3.2.1 可写的属性
有些时候,你需要能够在一个对象外部设置它的属性,例如,让我们假定某一首歌的时长最初是预估得来的(可能从CD或MP3数据的信息中产生而来)。当我们第一次播放这首歌时,会得到它实际的长度,并将这个新的值保存回Song对象中。
在类如C++和Java等语言中,你需要用setter方法来完成这个任务。
class JavaSong {
private Duration _duration;
public void setDuration(Duration newDuration) {
_duration = newDuration
}
}
s = new JavaSong(...);
s.setDuration(length);
在Ruby中,访问对象属性就像访问其他变量一样,我们在前面已经看到,例如song.name语句,因此,当你想要设置某个属性的值时,对这些变量直接赋值似乎更自然些,在Ruby中,你可以通过创建一个名字以等号结尾的方法来达成这一目标,这些方法可以作为赋值操作的目标。
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 调用了song对象中的duration=方法,并将257作为参数传入,实际上,定义一个以等号结尾的方法名,便使其能够出现在复制操作的左侧。
同样,Ruby又提供了一种快捷方式创建这类简单的属性设置方法。
class Song
attr_writer :duration
end
song = Song.new("Bicyclops", "Fleck", 260)
song.duration = 257
3.2.2 虚拟属性
这类属性访问的方法并不必须是对象实例变量的简单包装(wrapper),例如,你可能希望访问以分钟为单位,而不是我们已经实现的以秒为单位的时长。
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
这里我们使用属性方法创建了一个虚拟的实例变量,对外部世界来说,duration_in_minutes就像其他属性一样,然而,在内部它没有对应的实例变量。
这远非出于讨巧,在Bertrand Meyer的划时代著作Object-Oriented Software Construction中,他将它称为统一访问原则,通过隐藏实例变量与计算的值得差异,你可以向外部世界疲敝类的实现,将来你可以自由地更改对象如何运作,而不会影响使用了你的类的、数以百万行计的代码。这是一种很大的成功。
3.2.2 属性、实例变量及方法
这种对属性的描述,可能会让你认为它们无外乎就是方法——为什么我们要为它们发明一个新奇的名字呢?从某种程度上,这绝对是正确的,属性就是一个方法,某些时候,属性简单的返回实例变量的值,某些时候,属性返回计算后的结果,并且某些时候,那些名字以等号结尾的古怪方法,被用来更新对象的状态,那么问题是,哪里是属性结束而一般方法开始的地方呢?是什么让它变成属性的呢?
当你设计一个类的时候,你决定其具有什么样的内部状态,并决定这内部状态对外界(类的用户)的表现形式。内部状态保存在实例变量中,通过方法暴露出来的外部状态,我们称之为属性。
你的类可以执行的其他动作,就是一般方法,这并非一个非常重要的区别,但是通过把一个对象的外部状态称为属性,可以帮助人们了解你所编写的类。
3.3 类变量和类方法
到目前为止,所有我们创建的类都包括有实例变量和实例方法:变量被关联到类的某个特定实例,以及操作这些变量的方法。有时候,类本身需要它们自己的状态。这是类变量的领地、
3.3.1 类变量
类变量被类的所有对象共享,它与我们稍后描述的类方法相关联。对一个给定的类来说,类变量只存在一份拷贝。类变量由两个@符开头,例如@@count。与全局变量和实例变量不同,类变量在使用之前必须被初始化,通常,初始化就是在类定义中简单赋值。
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
出于调试的目的,我们还让Song#play返回一个字符串,其中包括该歌曲被播放的次数,以及所有歌曲播放的总次数,我们可以很容易测试它。
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."
类变量对类及其实例都是私有的,如果你想要让它们能够被外部世界访问,你需要编写访问方法,这个方法要么是一个实例方法,或者是紧接着在下一节要介绍的类方法。
3.3.2 类方法
有时,类需要提供不束缚于任何特定对象的方法。我们已经见过一个这样的方法,new方法创建一个新的Song对象,但是new方法本身并不与一个特定的歌曲对象相关联。
song = Song.new(....)
我们会发现类方法遍布于Ruby库中。例如,File类的对象用来表示在底层文件系统中打开的一个文件。不过,File类还提供了几个类方法来操作文件,而它们并未打开文件,因此也没有相应的File对象,如果你想要删除一个文件,你可以调用类方法File.delete,传入文件名作为参数。
File.delete("domend.txt")
类方法和实例方法是通过它们的定义区别开来的,通过在方法名之前放置类名以及一个句点,来定义方法。
class Example
def instance_method
end
def Example.class_method
end
end
点唱机按播放的每首歌曲收费,而非按分钟收费,这使得短歌曲比长歌更有助于盈收。我们可能希望避免那些过长的歌曲出现在SongList中,我们可以在SongList中定义一个类方法检查,是否某一首歌超过了时限。我们使用一个类的常量设置时限,就是在一个类代码中初始化的常量(记得常量吗?它们使用一个大写字母开头)。
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默认创建对象的方式。举例来说,看看我们的点唱机。因为我们将有许多点唱机分布在全国各地,我们希望让维护工作尽可能简单,其中一个需求是记录点唱机发生的所有事情:播放歌曲、收到的点歌费、倾倒进去的各种奇怪液体等等。
因为我们希望为音乐保留网络宽带,因此我们将日志文件保存本地。这意味着我们需要一个类来处理日志。不过,我们希望每个点唱机只有一个记录日志的对象,并且我们希望其他所有对象都共享同一个日志对象。
这里适用Singleton模式,在《Design Patterns(设计模式)》一书中记载,我们将进行一些调整,使得只有一种方式创建日志对象,那就是调用MyLogger.create,并且我们还保存只有一个日志对象被创建。
class MyLogger
private_class_method :new
@@logger = nil
def MyLogger.create
@@logger = new unless @@logger
@@logger
end
end
通过将MyLogger的new方法标记为private(私有),我们阻止所有人使用传统的构造函数来创建日志对象。相反,我们提供了一个类方法,MyLogger.crate。这个方法使用了类变量@@logger来保存唯一一个日志对象实例的引用,并在每次被调用时返回。
实际上,你可以用很多方式来定义类方法,但是理解这些方式为什么有效,我们需要等到第24章。
下面是在类Demo内定义类方法。
class Demo
def Demo.meth1
# ....
end
def self.meth2
# ...
end
class <<self
def meth3
# ...
end
end
end
这个实例,我们可以通过查看方法返回对象的标识符来检验。
MyLogger.create.object_id
MyLogger.create.object_id
使用类方法作为伪构造函数(pseudo-constructors),可以让使用类的用户更轻松些。举个简单的例子,让我们看一个表示等边多边形的Shape类。通过将所需的边数和周长传入构造函数来创建Shape的实例。
class Shape
def initialize(num_sides, perimeter)
# ...
end
end
注意:我们这里演示的singleton实现并非是线程安全的;如果多个线程在运行,有可能会创建多个日志对象。与其我们自己添加线程安全,不如使用Ruby提供的Singleton mixin。
不过,几年之后,另一个不同的应用使用了这个类,其中的程序员习惯于通过名字、并指定每条边的长度而不是周长来创建图形,只需要在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
类方法有许多有趣并强大的用途,为了使我们的点唱机产品尽可能快地完成,这里就不再继续深入探究了,让我们继续前进吧!
3.4 访问控制(Access Controller)
当我们设计类的接口时,需要着重考虑你的类想要向外部世界暴露何种程度的访问。如果你的类允许过度的访问,会增加应用中耦合的风险——类的用户可能会依赖于类实现的细节,而非逻辑性的接口。好消息是,在Ruby中改变一个对象的状态,唯一的简单方式是调用它的方法。
控制对方法的访问,你就需要空控制对对象的访问,一个经验法则是,永远不要暴露会导致对象出于无效状态的方法,Ruby为你提供了三中级别的保护。
- Public 没有限制,方法默认是public的(initialize除外,它是private的)
- Protected 只能被定义了该方法的类或其他子类的对象调用。整个家族均可访问。
- Private 不能被明确的接受者调用,其接受者只能是self,这意味着私有方法只能在当前对象的上下文中被调用,你不能调用另一个对象的私有方法。
“protected”和“private”之前的区别很微妙,并且和其他大多数普通的面相对象语言不同,如果方法是保护的,他可以被定义了该方法的类或者子类的实例所调用,如果方法是私有的,它只能在当前对象的上下文中被调用——不可能直接访问其他对象的私有方法,即便它与调用者都属同一个类的对象。
Ruby和其他面向对象语言的差异,还体现在另一个重要的方面,访问控制是在程序运行时动态判定的,而非静态判定。只有当代码试图执行受限的方法,你才会得到一个访问违规。
3.4.1 访问控制(Specifying Access Control)
你可以使用public、protected和private3个函数(function),来为类或模块定义内的方法(method)指定访问级别,你可以以两种不同的方式使用每个函数。
如果使用时没有参数,这3个函数设置后续定义方法的默认访问控制,如果你是C++或者Java程序员,这应该是你熟悉的行文,你同样使用类似public的关键字来取得相同的效果。
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
另外,你可以通过将方法名作为参数列表传入访问控制函数来设置它们的访问级别。
class MyClass
def method
end
public :method1, :method4
private :method2
end
举个例子,假设我们要为一个会计系统建立模型,其中每个借方都有对应的信用,因为我们希望确保没有人破坏这个规则,所以我们让有关借方和信用的方法均称为私有。并定义外部的交易接口。
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节
- 继续看application.rb剩余部分
- admin/logs
今天收获了什么?
- logs_controller.rb文件
class Admin...Controller
# Admin...Controller继承A...Controller
def index
@records = model.includes(:or)
# 引用父类ApplicationController的model方法,把日志的每一条数据都查询出来
super
end
end
- 日志视图index
ruby:
.. = {
collections: %i[ search .. ], # 搜索集,%i生成一个Symbol数组
...
} #这是一个hash数据流,返回给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.. #模型的关联是单向绑定。当一个关联为a:belongs_to时,inverse_of选项基本上给我们双向内存绑定。https://www.viget.com/articles/exploring-the-inverse-of-option-on-rails-model-associations
has_many :.., -> { enabled }, through: :..是什么意思
has_many :.., through: :..
- “::”是什么意思? 命名空间的分隔符
- module Ruby中的module有两个作用,一个是标识命名空间,一个是mixin 具体参考http://phrogz.net/programmingruby/tut_modules.html