美文网首页
领域驱动设计(DDD):对象属性(property)和 gett

领域驱动设计(DDD):对象属性(property)和 gett

作者: 不够具体 | 来源:发表于2020-12-08 15:05 被阅读0次

    对象属性(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)和 gett

          本文链接:https://www.haomeiwen.com/subject/oxzfgktx.html