mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-12-18 19:54:21 +08:00
回到简体
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
## 6.6. 封裝
|
||||
## 6.6. 封装
|
||||
|
||||
一個對象的變量或者方法如果對調用方是不可見的話,一般就被定義爲“封裝”。封裝有時候也被叫做信息隱藏,同時也是面向對象編程最關鍵的一個方面。
|
||||
一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装有时候也被叫做信息隐藏,同时也是面向对象编程最关键的一个方面。
|
||||
|
||||
Go語言隻有一種控製可見性的手段:大寫首字母的標識符會從定義它們的包中被導出,小寫字母的則不會。這種限製包內成員的方式同樣適用於struct或者一個類型的方法。因而如果我們想要封裝一個對象,我們必須將其定義爲一個struct。
|
||||
Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
|
||||
|
||||
這也就是前面的小節中IntSet被定義爲struct類型的原因,盡管它隻有一個字段:
|
||||
这也就是前面的小节中IntSet被定义为struct类型的原因,尽管它只有一个字段:
|
||||
|
||||
```go
|
||||
type IntSet struct {
|
||||
@@ -12,21 +12,21 @@ type IntSet struct {
|
||||
}
|
||||
```
|
||||
|
||||
當然,我們也可以把IntSet定義爲一個slice類型,盡管這樣我們就需要把代碼中所有方法里用到的s.words用*s替換掉了:
|
||||
当然,我们也可以把IntSet定义为一个slice类型,尽管这样我们就需要把代码中所有方法里用到的s.words用*s替换掉了:
|
||||
|
||||
```go
|
||||
type IntSet []uint64
|
||||
```
|
||||
|
||||
盡管這個版本的IntSet在本質上是一樣的,他也可以允許其它包中可以直接讀取併編輯這個slice。換句話説,相對*s這個表達式會出現在所有的包中,s.words隻需要在定義IntSet的包中出現(譯註:所以還是推薦後者吧的意思)。
|
||||
尽管这个版本的IntSet在本质上是一样的,他也可以允许其它包中可以直接读取并编辑这个slice。换句话说,相对*s这个表达式会出现在所有的包中,s.words只需要在定义IntSet的包中出现(译注:所以还是推荐后者吧的意思)。
|
||||
|
||||
這種基於名字的手段使得在語言中最小的封裝單元是package,而不是像其它語言一樣的類型。一個struct類型的字段對同一個包的所有代碼都有可見性,無論你的代碼是寫在一個函數還是一個方法里。
|
||||
这种基于名字的手段使得在语言中最小的封装单元是package,而不是像其它语言一样的类型。一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
|
||||
|
||||
封裝提供了三方面的優點。首先,因爲調用方不能直接脩改對象的變量值,其隻需要關註少量的語句併且隻要弄懂少量變量的可能的值卽可。
|
||||
封装提供了三方面的优点。首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
|
||||
|
||||
第二,隱藏實現的細節,可以防止調用方依賴那些可能變化的具體實現,這樣使設計包的程序員在不破壞對外的api情況下能得到更大的自由。
|
||||
第二,隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
|
||||
|
||||
把bytes.Buffer這個類型作爲例子來考慮。這個類型在做短字符串疊加的時候很常用,所以在設計的時候可以做一些預先的優化,比如提前預留一部分空間,來避免反複的內存分配。又因爲Buffer是一個struct類型,這些額外的空間可以用附加的字節數組來保存,且放在一個小寫字母開頭的字段中。這樣在外部的調用方隻能看到性能的提陞,但併不會得到這個附加變量。Buffer和其增長算法我們列在這里,爲了簡潔性稍微做了一些精簡:
|
||||
把bytes.Buffer这个类型作为例子来考虑。这个类型在做短字符串叠加的时候很常用,所以在设计的时候可以做一些预先的优化,比如提前预留一部分空间,来避免反复的内存分配。又因为Buffer是一个struct类型,这些额外的空间可以用附加的字节数组来保存,且放在一个小写字母开头的字段中。这样在外部的调用方只能看到性能的提升,但并不会得到这个附加变量。Buffer和其增长算法我们列在这里,为了简洁性稍微做了一些精简:
|
||||
|
||||
```go
|
||||
type Buffer struct {
|
||||
@@ -49,7 +49,7 @@ func (b *Buffer) Grow(n int) {
|
||||
}
|
||||
```
|
||||
|
||||
封裝的第三個優點也是最重要的優點,是阻止了外部調用方對對象內部的值任意地進行脩改。因爲對象內部變量隻可以被同一個包內的函數脩改,所以包的作者可以讓這些函數確保對象內部的一些值的不變性。比如下面的Counter類型允許調用方來增加counter變量的值,併且允許將這個值reset爲0,但是不允許隨便設置這個值(譯註:因爲壓根就訪問不到):
|
||||
封装的第三个优点也是最重要的优点,是阻止了外部调用方对对象内部的值任意地进行修改。因为对象内部变量只可以被同一个包内的函数修改,所以包的作者可以让这些函数确保对象内部的一些值的不变性。比如下面的Counter类型允许调用方来增加counter变量的值,并且允许将这个值reset为0,但是不允许随便设置这个值(译注:因为压根就访问不到):
|
||||
|
||||
```go
|
||||
type Counter struct { n int }
|
||||
@@ -58,7 +58,7 @@ func (c *Counter) Increment() { c.n++ }
|
||||
func (c *Counter) Reset() { c.n = 0 }
|
||||
```
|
||||
|
||||
隻用來訪問或脩改內部變量的函數被稱爲setter或者getter,例子如下,比如log包里的Logger類型對應的一些函數。在命名一個getter方法時,我們通常會省略掉前面的Get前綴。這種簡潔上的偏好也可以推廣到各種類型的前綴比如Fetch,Find或者Lookup。
|
||||
只用来访问或修改内部变量的函数被称为setter或者getter,例子如下,比如log包里的Logger类型对应的一些函数。在命名一个getter方法时,我们通常会省略掉前面的Get前缀。这种简洁上的偏好也可以推广到各种类型的前缀比如Fetch,Find或者Lookup。
|
||||
|
||||
```go
|
||||
package log
|
||||
@@ -73,18 +73,18 @@ func (l *Logger) Prefix() string
|
||||
func (l *Logger) SetPrefix(prefix string)
|
||||
```
|
||||
|
||||
Go的編碼風格不禁止直接導出字段。當然,一旦進行了導出,就沒有辦法在保證API兼容的情況下去除對其的導出,所以在一開始的選擇一定要經過深思熟慮併且要考慮到包內部的一些不變量的保證,未來可能的變化,以及調用方的代碼質量是否會因爲包的一點脩改而變差。
|
||||
Go的编码风格不禁止直接导出字段。当然,一旦进行了导出,就没有办法在保证API兼容的情况下去除对其的导出,所以在一开始的选择一定要经过深思熟虑并且要考虑到包内部的一些不变量的保证,未来可能的变化,以及调用方的代码质量是否会因为包的一点修改而变差。
|
||||
|
||||
封裝併不總是理想的。
|
||||
雖然封裝在有些情況是必要的,但有時候我們也需要暴露一些內部內容,比如:time.Duration將其表現暴露爲一個int64數字的納秒,使得我們可以用一般的數值操作來對時間進行對比,甚至可以定義這種類型的常量:
|
||||
封装并不总是理想的。
|
||||
虽然封装在有些情况是必要的,但有时候我们也需要暴露一些内部内容,比如:time.Duration将其表现暴露为一个int64数字的纳秒,使得我们可以用一般的数值操作来对时间进行对比,甚至可以定义这种类型的常量:
|
||||
|
||||
```go
|
||||
const day = 24 * time.Hour
|
||||
fmt.Println(day.Seconds()) // "86400"
|
||||
```
|
||||
|
||||
另一個例子,將IntSet和本章開頭的geometry.Path進行對比。Path被定義爲一個slice類型,這允許其調用slice的字面方法來對其內部的points用range進行迭代遍歷;在這一點上,IntSet是沒有辦法讓你這麽做的。
|
||||
另一个例子,将IntSet和本章开头的geometry.Path进行对比。Path被定义为一个slice类型,这允许其调用slice的字面方法来对其内部的points用range进行迭代遍历;在这一点上,IntSet是没有办法让你这么做的。
|
||||
|
||||
這兩種類型決定性的不同:geometry.Path的本質是一個坐標點的序列,不多也不少,我們可以預見到之後也併不會給他增加額外的字段,所以在geometry包中將Path暴露爲一個slice。相比之下,IntSet僅僅是在這里用了一個[]uint64的slice。這個類型還可以用[]uint類型來表示,或者我們甚至可以用其它完全不同的占用更小內存空間的東西來表示這個集合,所以我們可能還會需要額外的字段來在這個類型中記録元素的個數。也正是因爲這些原因,我們讓IntSet對調用方透明。
|
||||
这两种类型决定性的不同:geometry.Path的本质是一个坐标点的序列,不多也不少,我们可以预见到之后也并不会给他增加额外的字段,所以在geometry包中将Path暴露为一个slice。相比之下,IntSet仅仅是在这里用了一个[]uint64的slice。这个类型还可以用[]uint类型来表示,或者我们甚至可以用其它完全不同的占用更小内存空间的东西来表示这个集合,所以我们可能还会需要额外的字段来在这个类型中记录元素的个数。也正是因为这些原因,我们让IntSet对调用方透明。
|
||||
|
||||
在這章中,我們學到了如何將方法與命名類型進行組合,併且知道了如何調用這些方法。盡管方法對於OOP編程來説至關重要,但他們隻是OOP編程里的半邊天。爲了完成OOP,我們還需要接口。Go里的接口會在下一章中介紹。
|
||||
在这章中,我们学到了如何将方法与命名类型进行组合,并且知道了如何调用这些方法。尽管方法对于OOP编程来说至关重要,但他们只是OOP编程里的半边天。为了完成OOP,我们还需要接口。Go里的接口会在下一章中介绍。
|
||||
|
||||
Reference in New Issue
Block a user