對(duì)象屬性(property)和 getters , setters 方法
“需要為一個(gè)對(duì)象的屬性添加 Getters / Setters 方法”而提出為什么?由此而進(jìn)行深入思考。
它是字段(field)
在 Java 中我們都知道如何在類(Class)中聲明一個(gè)成員屬性(field)。
public class HikariConfig {
public long connectionTimeout;
public long validationTimeout;
}
當(dāng)我們需要設(shè)置對(duì)象的屬性值時(shí),我們可以直接使用 = 賦值。
public class HikariConfigTests {
public static void main(String[] args) {
var config = new HikariConfig();
config.connectionTimeout = 250;
config.validationTimeout = 250;
}
}
如果我們需要在設(shè)置 connectionTimeout 屬性時(shí),做一些賦值校驗(yàn)。比如:connectionTimeout 不能小于 250ms 。
public class HikariConfigTests {
public static void main(String[] args) {
var config = new HikariConfig();
var connectionTimeoutMs = 250;
if (connectionTimeoutMs < 250) {
throw new IllegalArgumentException("connectionTimeout cannot be less than 250ms");
}
config.connectionTimeout = connectionTimeoutMs;
}
}
屬性(property)具有封裝性
面向?qū)ο笥腥筇匦裕豪^承、封裝、多態(tài)。
我們應(yīng)該已經(jīng)發(fā)現(xiàn)校驗(yàn) connectionTimeout 的邏輯(代碼)被放置在 HikariConfig 對(duì)象自身之外,但從面向?qū)ο蟮慕嵌葋?lái)說(shuō)如校驗(yàn)屬性的代碼應(yīng)該放在 connectionTimeout
上,但是字段(field)不具備封裝性。
如果你發(fā)現(xiàn)了這個(gè)問(wèn)題,那么面向?qū)ο蟮脑O(shè)計(jì)者們也一樣會(huì)發(fā)現(xiàn)這個(gè)問(wèn)題。
當(dāng)聽(tīng)到屬性這個(gè)詞時(shí),你想到的是什么呢?
- 你可能想到的是字段(field),因?yàn)?field 常常會(huì)被翻譯為成員屬性(field)。
- field 真正要表達(dá)的意思是:一塊存放數(shù)據(jù)區(qū)域。
一個(gè)對(duì)象是由屬性和操作組成的。操作可以被封裝成一個(gè)方法:
public interface Runnable {
void run();
}
如果操作可以被封裝成方法,那么如何封裝屬性呢?
現(xiàn)代的編程語(yǔ)言為使用者提供了一些語(yǔ)法糖來(lái)封裝屬性,比如:C# , Kotlin , Typescript , Scala 等等。
在 Kotlin 中我們可以使用 get 和 set 關(guān)鍵字來(lái)封裝屬性:
class HikariConfig {
var connectionTimeout: Long = 0
set(value) {
if (value < 250) {
throw IllegalArgumentException("connectionTimeout cannot be less than 250ms")
}
field = value
}
}
在 Kotlin 中使用屬性:
fun main() {
val config = HikariConfig()
config.connectionTimeout = 250
}
在 Typescript 中我們可以使用 get 和 set 關(guān)鍵字來(lái)封裝屬性:
class HikariConfig {
#connectionTimeout: number
public get connectionTimeout() {
return this.#connectionTimeout
}
public set connectionTimeout(connectionTimeout) {
if (connectionTimeout < 250) {
throw new Error("connectionTimeout cannot be less than 250ms");
}
this.#connectionTimeout = connectionTimeout
}
}
在 Typescript 中使用屬性:
const config = new HikariConfig()
config.connectionTimeout = 250
在 Java 中并沒(méi)有為屬性(property)提供 get 和 set 關(guān)鍵字,而是將其設(shè)計(jì)成方法。 使用 getXxx 方法來(lái)模擬 get 關(guān)鍵字和使用 setXxx 方法來(lái)模擬 set 關(guān)鍵字。
class HikariConfig {
private long connectionTimeout;
public long getConnectionTimeout() {
return this.connectionTimeout;
}
public void setConnectionTimeout(long value) {
if (value < 250) {
throw new IllegalArgumentException("connectionTimeout cannot be less than 250ms");
}
this.connectionTimeout = value;
}
}
本來(lái)在擁有 get 和 set 關(guān)鍵字的編程語(yǔ)言里,大家只是對(duì) property 與 field 有些混淆,這樣的混淆還是可以很簡(jiǎn)單的解釋清楚。但是在 Java 中由于直接使用方法(getXxx , setXxx
)來(lái)封裝屬性(property)使得大家對(duì) field , property 和 method 三者混淆起來(lái)。在有些時(shí)候大家不知道 getXxx 和 setXxx 方法是在做對(duì)象的屬性(property),所以很多人誤認(rèn)為字段(field)便是屬性(property)。尤其是在應(yīng)用系統(tǒng)開(kāi)發(fā)中許多模型的屬性不需要做多余封裝,只是直白的存在。
當(dāng)把字段(field)誤認(rèn)為是屬性(property)以后,在遇到需要為某一個(gè)對(duì)象的屬性進(jìn)行封裝時(shí),往往會(huì)使用其它方法來(lái)解決。比如:changeXxx 方法。
在擁有 get 和 set 關(guān)鍵字的編程語(yǔ)言里,在使用 get 或者 set 關(guān)鍵字時(shí),在編譯器在編譯代碼時(shí),依然會(huì)將 get 或者 set 關(guān)鍵字所做的對(duì)屬性(property)的封裝轉(zhuǎn)換成讀(read , get)方法或者寫(xiě)(write , set)方法,所以get 和 set 關(guān)鍵字只是對(duì) getXxx 和 setXxx 方法的一種語(yǔ)法糖。
在 Typescript 中,編譯器最終會(huì)將 get 或者 set 關(guān)鍵字最終編譯成這樣:
Object.defineProperty(config, "connectionTimeout", {
configurable: false,
enumerable: false,
set: function (connectionTimeout) {
if (connectionTimeout < 250) {
throw new Error("connectionTimeout cannot be less than 250ms");
}
this.#connectionTimeout = connectionTimeout;
},
get: function () {
return this.connectionTimeout;
}
})
在 Kotlin 中,編譯器最終會(huì)將 get 或者 set 關(guān)鍵字編譯成這樣:
class HikariConfig {
private long connectionTimeout;
public long getConnectionTimeout() {
return this.connectionTimeout;
}
public void setConnectionTimeout(long value) {
if (value < 250) {
throw IllegalArgumentException("connectionTimeout cannot be less than 250ms");
}
this.connectionTimeout = value;
}
}
在其它擁有 get 或者 set 關(guān)鍵字的編程語(yǔ)言(C# , Scala)里一樣會(huì)將其編譯成某種格式的方法來(lái)完成對(duì)屬性的封裝性。
屬性具有讀(read)和寫(xiě)(write)權(quán)限
在 Java 中提供了四個(gè)訪問(wèn)控制修飾符( public , protected , default , private ),他們可以修飾類(class)、方法(method)以及字段(field)。需要更深入的了解到它們只是在控制一定的范圍,比如在創(chuàng)建(new)一個(gè)對(duì)象時(shí),是在控制可以在哪個(gè)包(package)內(nèi)去創(chuàng)建這個(gè)對(duì)象。比如在使用方法時(shí),也是在控制可以在哪個(gè)包內(nèi)去使用這個(gè)方法。同樣在使用字段(field)時(shí)也是在控制可以在哪個(gè)范圍內(nèi)使用。
這些訪問(wèn)(access)控制修飾符應(yīng)該作用在被修飾的動(dòng)作(動(dòng)詞)上,而不是名稱(名詞)。比如:對(duì)象的創(chuàng)建,方法的調(diào)用,字段的獲得與設(shè)置。創(chuàng)建(new)、調(diào)用(invoke)、獲得(get)、設(shè)置(set)這樣的動(dòng)作都需要通過(guò)訪問(wèn)修飾符做到精確控制。對(duì)于一個(gè)類(class)在使用時(shí)只有一個(gè)創(chuàng)建(new)的動(dòng)作,同樣的使用方法時(shí)也只有一個(gè)調(diào)用(invoke)的動(dòng)作,不需要再次精確細(xì)分。而對(duì)于字段(field)在使用時(shí)有兩個(gè)動(dòng)作:獲得(get)和設(shè)置(set),而字段(field)本身在處理這兩個(gè)動(dòng)作時(shí)并沒(méi)有辦法做到細(xì)分。
現(xiàn)在的問(wèn)題是同一個(gè)訪問(wèn)控制修飾符同時(shí)控制對(duì)某一個(gè)字段(field)的兩個(gè)動(dòng)作( get , set ),而這個(gè)問(wèn)題需要發(fā)現(xiàn)者仔細(xì)思考:
class HikariConfig {
connectionTimeout: number
}
const config = new HikariConfig()
config.connectionTimeout = 100 // Set
const timeout = config.connectionTimeout // Get
如果此時(shí)把 public 修改為 protected ,那么操作 connectionTimeout 的兩個(gè)動(dòng)作( get , set )的訪問(wèn)控制將全部變成 protected 。
class HikariConfig {
protected connectionTimeout: number
}
我們可以發(fā)現(xiàn)一個(gè)字段的兩個(gè)動(dòng)作( get , set )的訪問(wèn)權(quán)限被混合到了一起,這帶來(lái)了什么問(wèn)題呢?
- 一個(gè)對(duì)象的屬性只是想對(duì)外提供公共讀(public read),對(duì)外不提供公共寫(xiě)(private write)。
- 一個(gè)對(duì)象的屬性只是想對(duì)外提供公共寫(xiě)(public write),對(duì)外不提供公共讀(private read)。
- ......
簡(jiǎn)單來(lái)說(shuō)就是:可讀、可寫(xiě)、只讀、只寫(xiě)、不可讀寫(xiě)。
如果一個(gè)對(duì)象的屬性只是想對(duì)外提供只讀屬性(注意是對(duì)外,對(duì)象的內(nèi)外有區(qū)別),而被 public 修飾的字段將帶來(lái)的是 get 和 set
都具有可讀寫(xiě)的權(quán)限,這就使得使用者可以設(shè)置(set)這個(gè)字段。這將給對(duì)象帶來(lái)意向不當(dāng)?shù)暮蠊?,有可能是破壞性的后果?/p>
因此為一個(gè)對(duì)象的屬性( get , set ) 提供不同訪問(wèn)控制是有必要的。
class HikariConfig {
#connectionTimeout: number // private
public get connectionTimeout() { // public , protected , default , private
return this.#connectionTimeout
}
public set connectionTimeout(connectionTimeout) { // public , protected , default , private
this.#connectionTimeout = connectionTimeout
}
}
屬性可以無(wú)讀(寫(xiě))操作
一個(gè)屬性(property)有兩個(gè)操作:讀(read)和寫(xiě)(write)??梢詫⒁粋€(gè)對(duì)象的屬性的讀寫(xiě)權(quán)限修改為 private
。私有的訪問(wèn)控制權(quán)限并不意味著不存在,只是表明這個(gè)屬性只可以在這個(gè)對(duì)象內(nèi)部使用,對(duì)外不可使用。同樣的屬性可以具有無(wú)讀(寫(xiě))操作。
無(wú)寫(xiě)(write)操作:
class HikariConfig {
#connectionTimeout: number
public get connectionTimeout() {
return this.#connectionTimeout
}
// 沒(méi)有 set 方法,無(wú) set 與私有 set 的區(qū)別。
}
無(wú)讀(read)操作:
class HikariConfig {
#connectionTimeout: number
// 沒(méi)有 get 方法,無(wú) get 與私有 get 的區(qū)別。
public set connectionTimeout(connectionTimeout) {
this.#connectionTimeout = connectionTimeout
}
}
一個(gè)屬性不能同時(shí)沒(méi)有讀和寫(xiě)方法(操作),如果同時(shí)沒(méi)有也就表明這個(gè)屬性不存在。
區(qū)分屬性(property)和字段(field)
封裝性和讀寫(xiě)訪問(wèn)控制是屬性(property)和字段(field)最根本的區(qū)別。在區(qū)分屬性和字段的區(qū)別時(shí),是依據(jù)他們的所具有的功能來(lái)判斷的。
尤其是在具有 get 和 set 關(guān)鍵字以及對(duì)屬性(property)和字段(field)沒(méi)有區(qū)分(如:命名規(guī)范、使用方式)的編程語(yǔ)言里,區(qū)分屬性和字段只需要簡(jiǎn)單的通過(guò)是否具有封裝性和讀寫(xiě)控制來(lái)區(qū)分。
| 屬性(property) | 字段(field) | |
|---|---|---|
| 封裝性 | 具有封裝功能 | 不具有封裝功能 |
| 讀寫(xiě)控制 | 可以細(xì)分控制 | 不能細(xì)分控制 |
實(shí)體對(duì)象屬性校驗(yàn)方式
嘻嘻,過(guò)兩天再說(shuō)。~~~~
開(kāi)源電商
Mallfoundry 是一個(gè)完全開(kāi)源的使用 Spring Boot 開(kāi)發(fā)的多商戶電商平臺(tái)。它可以嵌入到已有的 Java 程序中,或者作為服務(wù)器、集群、云中的服務(wù)運(yùn)行。
- 領(lǐng)域模型采用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)、接口化以及面向?qū)ο笤O(shè)計(jì)。
項(xiàng)目地址:https://gitee.com/mallfoundry/mall
總結(jié)
屬性(property)與字段(field)有區(qū)別,總的來(lái)說(shuō)是兩個(gè)方面:封裝性和訪問(wèn)控制。
一個(gè)對(duì)象是由屬性和方法組成的,所以認(rèn)識(shí)屬性時(shí)需要知道屬性是具有封裝性的。而不能只認(rèn)識(shí)到只有方法需要封裝,屬性一樣需要封裝。、當(dāng)屬性和方法都具有封裝性時(shí),在使用具有屬性和方法的對(duì)象時(shí)才不會(huì)極化。