领域驱动设计(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)有区别,总的来说是两个方面:封装性和访问控制。

一个对象是由属性和方法组成的,所以认识属性时需要知道属性是具有封装性的。而不能只认识到只有方法需要封装,属性一样需要封装。、当属性和方法都具有封装性时,在使用具有属性和方法的对象时才不会极化。

你可能感兴趣的:(领域驱动设计(DDD):对象属性(property)和 getters , setters 方法)