領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD):對(duì)象屬性(property)和 getters , setters 方法

對(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 中我們可以使用 getset 關(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 中我們可以使用 getset 關(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)提供 getset 關(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)在擁有 getset 關(guān)鍵字的編程語(yǔ)言里,大家只是對(duì) property 與 field 有些混淆,這樣的混淆還是可以很簡(jiǎn)單的解釋清楚。但是在 Java 中由于直接使用方法(getXxx , setXxx
)來(lái)封裝屬性(property)使得大家對(duì) field , property 和 method 三者混淆起來(lái)。在有些時(shí)候大家不知道 getXxxsetXxx 方法是在做對(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 方法。

在擁有 getset 關(guān)鍵字的編程語(yǔ)言里,在使用 get 或者 set 關(guān)鍵字時(shí),在編譯器在編譯代碼時(shí),依然會(huì)將 get 或者 set 關(guān)鍵字所做的對(duì)屬性(property)的封裝轉(zhuǎn)換成讀(read , get)方法或者寫(xiě)(write , set)方法,所以getset 關(guān)鍵字只是對(duì) getXxxsetXxx 方法的一種語(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)的是 getset
都具有可讀寫(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)判斷的。

尤其是在具有 getset 關(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ì)極化。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容