1. 程式人生 > 實用技巧 >領域驅動設計(DDD):物件屬性(property)和 getters , setters 方法

領域驅動設計(DDD):物件屬性(property)和 getters , setters 方法

物件屬性(property)和 getters , setters 方法

“需要為一個物件的屬性新增 Getters / Setters 方法”而提出為什麼?由此而進行深入思考。

它是欄位(field)

在 Java 中我們都知道如何在類(Class)中宣告一個成員屬性(field)。

public class HikariConfig {
    public long connectionTimeout;
    public long validationTimeout;
}

當我們需要設定物件的屬性值時,我們可以直接使用=賦值。

public class HikariConfigTests {
    public static void main(String[] args) {
        var config = new HikariConfig();
        config.connectionTimeout = 250;
        config.validationTimeout = 250;
    }
}

如果我們需要在設定connectionTimeout屬性時,做一些賦值校驗。比如: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)具有封裝性

面向物件有三大特性:繼承、封裝、多型。

我們應該已經發現校驗 connectionTimeout 的邏輯(程式碼)被放置在 HikariConfig 物件自身之外,但從面向物件的角度來說如校驗屬性的程式碼應該放在 connectionTimeout 上,但是欄位(field)不具備封裝性。

如果你發現了這個問題,那麼面向物件的設計者們也一樣會發現這個問題。

當聽到屬性這個詞時,你想到的是什麼呢?

  • 你可能想到的是欄位(field),因為 field 常常會被翻譯為成員屬性(field)。
  • field 真正要表達的意思是:一塊存放資料區域。

一個物件是由屬性和操作組成的。操作可以被封裝成一個方法:

public interface Runnable {
    void run();
}

如果操作可以被封裝成方法,那麼如何封裝屬性呢?

現代的程式語言為使用者提供了一些語法糖來封裝屬性,比如:C# , Kotlin , Typescript , Scala 等等。

在 Kotlin 中我們可以使用getset關鍵字來封裝屬性:

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關鍵字來封裝屬性:

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 中並沒有為屬性(property)提供getset關鍵字,而是將其設計成方法。 使用getXxx方法來模擬get關鍵字和使用setXxx方法來模擬set關鍵字。

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;
    }
}

本來在擁有getset關鍵字的程式語言裡,大家只是對 property 與 field 有些混淆,這樣的混淆還是可以很簡單的解釋清楚。但是在 Java 中由於直接使用方法(getXxx,setXxx)來封裝屬性(property)使得大家對 field , property 和 method 三者混淆起來。在有些時候大家不知道getXxxsetXxx方法是在做物件的屬性(property),所以很多人誤認為欄位(field)便是屬性(property)。尤其是在應用系統開發中許多模型的屬性不需要做多餘封裝,只是直白的存在。

當把欄位(field)誤認為是屬性(property)以後,在遇到需要為某一個物件的屬性進行封裝時,往往會使用其它方法來解決。比如:changeXxx方法。

在擁有getset關鍵字的程式語言裡,在使用get或者set關鍵字時,在編譯器在編譯程式碼時,依然會將get或者set關鍵字所做的對屬性(property)的封裝轉換成讀(read,get)方法或者寫(write,set)方法,所以getset關鍵字只是對getXxxsetXxx方法的一種語法糖。

在 Typescript 中,編譯器最終會將get或者set關鍵字最終編譯成這樣:

 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 中,編譯器最終會將get或者set關鍵字編譯成這樣:

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關鍵字的程式語言(C# , Scala)裡一樣會將其編譯成某種格式的方法來完成對屬性的封裝性。

屬性具有讀(read)和寫(write)許可權

在 Java 中提供了四個訪問控制修飾符( public , protected , default , private ),他們可以修飾類(class)、方法(method)以及欄位(field)。需要更深入的瞭解到它們只是在控制一定的範圍,比如在建立(new)一個物件時,是在控制可以在哪個包(package)內去建立這個物件。比如在使用方法時,也是在控制可以在哪個包內去使用這個方法。同樣在使用欄位(field)時也是在控制可以在哪個範圍內使用。

這些訪問(access)控制修飾符應該作用在被修飾的動作(動詞)上,而不是名稱(名詞)。比如:物件的建立,方法的呼叫,欄位的獲得與設定。建立(new)、呼叫(invoke)、獲得(get)、設定(set)這樣的動作都需要通過訪問修飾符做到精確控制。對於一個類(class)在使用時只有一個建立(new)的動作,同樣的使用方法時也只有一個呼叫(invoke)的動作,不需要再次精確細分。而對於欄位(field)在使用時有兩個動作:獲得(get)和設定(set),而欄位(field)本身在處理這兩個動作時並沒有辦法做到細分。

現在的問題是同一個訪問控制修飾符同時控制對某一個欄位(field)的兩個動作( get , set ),而這個問題需要發現者仔細思考:

class HikariConfig {
    connectionTimeout: number
}

const config = new HikariConfig()

config.connectionTimeout = 100 // Set

const timeout = config.connectionTimeout // Get

如果此時把public修改為protected,那麼操作connectionTimeout的兩個動作( get , set )的訪問控制將全部變成 protected 。

class HikariConfig {
    protected connectionTimeout: number
}

我們可以發現一個欄位的兩個動作( get , set )的訪問許可權被混合到了一起,這帶來了什麼問題呢?

  • 一個物件的屬性只是想對外提供公共讀(public read),對外不提供公共寫(private write)。
  • 一個物件的屬性只是想對外提供公共寫(public write),對外不提供公共讀(private read)。
  • ......

簡單來說就是:可讀、可寫、只讀、只寫、不可讀寫。

如果一個物件的屬性只是想對外提供只讀屬性(注意是對外,物件的內外有區別),而被public修飾的欄位將帶來的是getset都具有可讀寫的許可權,這就使得使用者可以設定(set)這個欄位。這將給物件帶來意向不當的後果,有可能是破壞性的後果。

因此為一個物件的屬性(get,set) 提供不同訪問控制是有必要的。

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
    }
}

屬性可以無讀(寫)操作

一個屬性(property)有兩個操作:讀(read)和寫(write)。可以將一個物件的屬性的讀寫許可權修改為 private 。私有的訪問控制權限並不意味著不存在,只是表明這個屬性只可以在這個物件內部使用,對外不可使用。同樣的屬性可以具有無讀(寫)操作。

無寫(write)操作:

class HikariConfig {

    #connectionTimeout: number

    public get connectionTimeout() {
        return this.#connectionTimeout
    }

    // 沒有 set 方法,無 set 與私有 set 的區別。
}

無讀(read)操作:

class HikariConfig {

    #connectionTimeout: number

    // 沒有 get 方法,無 get 與私有 get 的區別。

    public set connectionTimeout(connectionTimeout) {
        this.#connectionTimeout = connectionTimeout
    }
}

一個屬性不能同時沒有讀和寫方法(操作),如果同時沒有也就表明這個屬性不存在。

區分屬性(property)和欄位(field)

封裝性和讀寫訪問控制是屬性(property)和欄位(field)最根本的區別。在區分屬性和欄位的區別時,是依據他們的所具有的功能來判斷的。

尤其是在具有getset關鍵字以及對屬性(property)和欄位(field)沒有區分(如:命名規範、使用方式)的程式語言裡,區分屬性和欄位只需要簡單的通過是否具有封裝性和讀寫控制來區分。

屬性(property)欄位(field)
封裝性 具有封裝功能 不具有封裝功能
讀寫控制 可以細分控制 不能細分控制

實體物件屬性校驗方式

嘻嘻,過兩天再說。~~~~

開源電商

Mallfoundry 是一個完全開源的使用 Spring Boot 開發的多商戶電商平臺。它可以嵌入到已有的 Java 程式中,或者作為伺服器、叢集、雲中的服務執行。

  • 領域模型採用領域驅動設計(DDD)、介面化以及面向物件設計。

專案地址:https://gitee.com/mallfoundry/mall

總結

屬性(property)與欄位(field)有區別,總的來說是兩個方面:封裝性和訪問控制。

一個物件是由屬性和方法組成的,所以認識屬性時需要知道屬性是具有封裝性的。而不能只認識到只有方法需要封裝,屬性一樣需要封裝。、當屬性和方法都具有封裝性時,在使用具有屬性和方法的物件時才不會極化。

來源:呂梁SEO