对象属性(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 中我们可以使用 get
和 set
关键字来封装属性:
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
关键字来封装属性:
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)提供 get
和 set
关键字,而是将其设计成方法。 使用 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;
}
}
本来在拥有 get
和 set
关键字的编程语言里,大家只是对 property 与 field 有些混淆,这样的混淆还是可以很简单的解释清楚。但是在 Java 中由于直接使用方法(getXxx
, setXxx
)来封装属性(property)使得大家对 field , property 和 method 三者混淆起来。在有些时候大家不知道 getXxx
和 setXxx
方法是在做对象的属性(property),所以很多人误认为字段(field)便是属性(property)。尤其是在应用系统开发中许多模型的属性不需要做多余封装,只是直白的存在。
当把字段(field)误认为是属性(property)以后,在遇到需要为某一个对象的属性进行封装时,往往会使用其它方法来解决。比如:changeXxx
方法。
在拥有 get
和 set
关键字的编程语言里,在使用 get
或者 set
关键字时,在编译器在编译代码时,依然会将 get
或者 set
关键字所做的对属性(property)的封装转换成读(read
, get
)方法或者写(write
, set
)方法,所以get
和 set
关键字只是对 getXxx
和 setXxx
方法的一种语法糖。
在 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
修饰的字段将带来的是 get
和 set
都具有可读写的权限,这就使得使用者可以设置(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)最根本的区别。在区分属性和字段的区别时,是依据他们的所具有的功能来判断的。
尤其是在具有 get
和 set
关键字以及对属性(property)和字段(field)没有区分(如:命名规范、使用方式)的编程语言里,区分属性和字段只需要简单的通过是否具有封装性和读写控制来区分。
属性(property) | 字段(field) | |
---|---|---|
封装性 | 具有封装功能 | 不具有封装功能 |
读写控制 | 可以细分控制 | 不能细分控制 |
实体对象属性校验方式
嘻嘻,过两天再说。~~~~
开源电商
Mallfoundry 是一个完全开源的使用 Spring Boot 开发的多商户电商平台。它可以嵌入到已有的 Java 程序中,或者作为服务器、集群、云中的服务运行。
- 领域模型采用领域驱动设计(DDD)、接口化以及面向对象设计。
项目地址:https://gitee.com/mallfoundry/mall
总结
属性(property)与字段(field)有区别,总的来说是两个方面:封装性和访问控制。
一个对象是由属性和方法组成的,所以认识属性时需要知道属性是具有封装性的。而不能只认识到只有方法需要封装,属性一样需要封装。、当属性和方法都具有封装性时,在使用具有属性和方法的对象时才不会极化。