目前,面向对象的编程范式统治了编程语言的世界。除了纯粹支持结构式编程的C
、纯粹支持函数式编程的Haskell
、Erlang、Lisp、Clojure
等语言之外,绝大多数现代编程语言如Java、C#、C++、Swift、Kotlin、Go、Scala
等都是面向对象的。
1. 类、对象和值
面向对象的编程语言将**类(Class)和对象(Object)**作为程序的基本构建块,应用程序就是由相互调用的多个对象结合而成。
1.1 对象
Grady Booch在他的名著《Object-Oriented Analysis and Design with Applications》中对对象给出了这样的定义:
一个对象是一个具有状态、行为和标识符的实体。结构和行为类似的对象定义在它们共同的类中。
-
状态(State)
对象的状态包括这个对象的所有属性(
Property
,通常是静态的),以及每个属性的当前值(通常是动态的)。例如矩形(Rectangle)这个类具有*长(length)和宽(width)*两个属性。某一个长3米,宽2米的矩形是一个具体对象。这个矩形的长度和宽度就是它的当前状态。
对象所拥有的属性由它所属的**类(Class)**定义,通常是静态的,这意味着在对象的生命周期中一般不会动态添加新的属性(但是有些编程语言支持动态添加属性)。而对象的属性值由对象而不是类持有,在对象的生命周期中一般是可以改变的(例如员工的职位这个属性,在他的职业生涯中是会改变的),所以说属性值通常是动态的。当然也有一些属性值在对象的生命周期中不会改变,例如订单对象的订单号,这时它就是静态的。
系统中的所有对象都封装了某种状态,系统的所有状态都由对象所封装。
在
Java
中,属性以实例字段(Instance Field)的形式存在,一般是private
的,可以(但不必须)通过public
的getter/setter
方法暴露为外部可见的属性。如果只有setter
方法,就是只读属性;如果只有getter
方法,就是只写属性;两者都有,就是可读写属性。 -
行为(Behavior)
行为是对象在状态改变和消息传递方面的动作和反应的方式。换言之:对象的行为代表了它外部可见的活动。
对象的行为包括这个对象的所有操作(Operation)。一个操作代表了一个类提供给它的对象的一种服务。一个操作是某种动作,一个对象调用另一个对象定义的操作,目的是获得反应(获得操作的返回值或发生副作用——修改对象的内部状态)。
在
Java
中,操作用**方法(Method)**的形式来声明。方法是类的成员函数,可以包含0到多个形式参数。当一个对象调用另一个对象的方法时,我们说一个对象向另一个对象传递了一个消息,消息内容包括了方法名和参数列表。方法的执行结果由方法的参数和对象的状态共同决定。例如账户
Account
类的取款方法credit(int amount)
,其执行结果既受到方法参数取款金额amount
的影响,又受到Account
对象的当前余额属性balance
影响。因此可以说:**一个对象的行为是它的状态以及施加在它上面的操作的函数。**操作(方法)执行结果由操作的参数值和对象的当前状态(属性值)共同决定。
**一个对象的状态代表了它的行为的累积效果。**例如账户的余额和冻结状态代表了对该账户对象多次调用存款、取款、冻结和解冻操作之后的总结果。
-
标识符(Identity)
Khoshafina和Copeland提出了这样的标识符定义:
标识符是对象的一个属性,它区分这个对象与其他所有对象。
每个对象的唯一标识符(不一定是名称)是在整个对象的生命周期中都被保持的,即使对象的状态改变了,只要标识符不变,就还是原来的对象。
1.2 对象和值
对象的属性值可以是简单值(例如矩形Rectangle
对象的width
属性值是一个简单的数字),也可以是对另一个外部对象的引用(例如账户Account
对象的owner
属性,引用了另一个外部对象:一个Owner
类的实例)。数字、枚举、字符等等,都是简单值。字符串、枚举、日期等等数据类型,虽然在Java
中以对象的形式实现,但本质上可以视为简单值。
因此对象的状态可以人为划分为两部分:一部分是由值类型的所有属性组成的内部状态,另一部分是由对象类型的所有属性组成的外部引用。
1.3 类和对象
对象所拥有的属性和操作由对象所属的类定义。对象是存在于时间和空间中的具体实体,而类仅代表一种抽象,即一个对象的本质。**类是对象的模板,对象是类的实例。类定义所有同类对象的共同特征,包括它的对象实例所能够拥有的属性和操作,而具体的对象实例持有自己特有的属性值。**同一个类的所有对象拥有相同的属性集,但它们的属性值可以各不相同。
类之间可以存在继承关系。子类继承了超类的所有特征(属性和操作),同时可以再添加自己特有的属性和操作,还可以改变(不鼓励)或修饰超类的操作。所谓改变,是指在子类中完全重写操作的实现代码,将超类的代码覆盖掉。所谓修饰,是指子类的操作代码在调用超类操作代码之前/之后/前后,添加更多的代码。
有些编程语言允许子类继承多个超类,但Java
只允许继承一个超类。
下面是一个账户(Account)类的类图:
@startuml
class Owner
abstract class Account {
- boolean locked
- int balance
+ boolean isLocked()
+ int getBalance()
+ void credit(int amount)
+ void debit(int amount)
+ void lock()
+ void unlock()
}
Account --> Owner
@enduml
在账户Account
类中有三个私有的属性,代表它持有的两项数据:
locked
: 账户是否已经被冻结balance
:账户的当前余额owner
:账户的持有人,指向Owner
类的一个实例对象。
还有一些公开的操作:
isLocked()
:表明账户是否已被冻结getBalance()
:获取账户的当前余额credit()
:取出debit()
:存入lock()
:冻结账户unlock()
:解冻账户
这些方法或者修改对象的状态(字段值),或者执行结果受对象的状态的影响。举例来说,lock()
和unlock()
会修改locked
字段的值,debit()
和credit()
会修改balance
字段的值,而credit()
的执行结果受balance
的值的影响,debit()
和credit()
的执行结果都收到locked
的值的影响。
package yang.yu.tdd.bank;
//被测对象
public class Account {
private boolean locked = false;
private int balance = 0;
private Owner owner;
public Account(Owner owner) {
this.owner = owner;
}
public boolean isLocked() {
return locked;
}
public int getBalance() {
return balance;
}
public void debit(int amount) {
if (locked) {
throw new AccountLockedException();
}
if (amount <= 0) {
throw new InvalidAmountException();
}
balance += amount;
}
public void credit(int amount) {
if (locked) {
throw new AccountLockedException();
}
if (amount <= 0) {
throw new InvalidAmountException();
}
if (amount > balance) {
throw new BalanceInsufficientException();
}
balance -= amount;
}
public void lock() {
locked = true;
}
public void unlock() {
locked = false;
}
}
1.4 数据只是实现业务规则的辅助手段
从对象的用户的角度来说,我们真正关注的是对象的行为(方法),而不是它持有的数据(属性值)。与数据相比,行为更加重要。数据存在的目的只是影响方法的执行结果。从上面的例子来看:我们根本不在乎Account
对象中的balance
字段的值是多少,甚至不关心Account
类中是否存在balance
字段,**我们真正在乎的是:一个Account
对象,无论历经多少次存取,只要未被冻结且取款总额不大于存款总额,就可以取款成功,否则取款失败。**我们在Account
类中定义balance
字段的目的,只是为了实现这个业务规则的辅助手段。如果有其他方式可以达到同样的目标,我们完全可以不用在Account
类中定义balance
这个字段。可以说,**以数据库为中心的增删改查开发范式在大多数情况下都是不合适的,以领域模型为中心的领域驱动设计才是更合适的开发范式。**数据库是细枝末节,领域模型才是软件开发的核心。
详细内容请戳这里↓↓↓
原创 | 使用JPA实现DDD持久化- O:对象的世界(1/3)
这一节就讲到这里,下一节我们继续讲"O:对象的世界的第二部分"。
如果觉得有收获,右下角点个【在看】鼓励一下呗!