面向?qū)ο?/h1>
面向?qū)ο笫且环N對(duì)世界理解和抽象的方法。那么對(duì)象是什么呢?
對(duì)象是對(duì)世界的理解和抽象,世界又代稱為萬(wàn)物。理解世界是比較復(fù)雜的,但是世界又是由事物組成的。
正是這樣的一種關(guān)系,認(rèn)識(shí)事物是極其重要的。那什么是事物呢?
事物:由事和物兩個(gè)方面組成。事即事情,物即物體,那什么是事情?什么是物體呢?
- 意志的行為是為事。
- 存在的一切是為物,物體又是由屬性和行為組成的。
由于對(duì)象是對(duì)事物的理解和抽象,所以對(duì)象就是對(duì)一個(gè)事物的屬性和行為的理解和抽象。正是這樣的一種關(guān)系,面向?qū)ο?/strong>就是對(duì)一個(gè)事物的屬性和行為的理解和抽象的方法。
理解對(duì)象以及抽象“對(duì)象”就是在理解和抽象事物的屬性和行為。
屬性和操作
面向?qū)ο蟮暮诵氖?strong>對(duì)象,對(duì)象是由屬性和方法組合而成的。在使用面向?qū)ο筮M(jìn)行分析、設(shè)計(jì)、編碼的時(shí)候,你首先應(yīng)該想到的是屬性和方法組合形成的對(duì)象。在需要組合的時(shí)候就不應(yīng)該出現(xiàn)只包含屬性的對(duì)象或者只包含方法的對(duì)象。
- 何時(shí)需要屬性和方法組合的對(duì)象呢?
- 何時(shí)只需要包含屬性的對(duì)象呢?
- 何時(shí)只需要包含方法的對(duì)象呢?
事物由事情和物體組成。事情是行為,物體是屬性。
- 當(dāng)你需要抽象一個(gè)事物的事情和物體時(shí)就需要屬性和方法的組合。
- 當(dāng)你只需要抽象一個(gè)事物的物體時(shí)就只需要屬性。
- 當(dāng)你只需要抽象一個(gè)事物的事情時(shí)就只需要方法。
對(duì)象建模
在數(shù)據(jù)庫(kù)系統(tǒng)中,它們關(guān)心的是事物中的物體,所以在抽象事物時(shí)它們只抽象了事物中的屬性。在應(yīng)用系統(tǒng)中,它們關(guān)心的是表達(dá)事物的三種方式(屬性和方法的組合、只包含屬性、只包含方法),所以在抽象事物時(shí)需要思考你需要那種方式。
只要需要抽象事物(事情和物體)中的屬性,也就是物體的這部分,那有可能是需要持久化的。只要需要持久化,通常是保存到關(guān)系型數(shù)據(jù)庫(kù)中,在關(guān)系型數(shù)據(jù)庫(kù)中的表(Table)基本上是與面向?qū)ο笾械膶?duì)象(Object)的屬性是一一對(duì)應(yīng)的。
由于數(shù)據(jù)庫(kù)中的表只抽象了事物中的屬性,所以它有可能是不完整的。就抽象事物的屬性來(lái)說(shuō)依然有兩種:只抽象事物的屬性、抽象事物的屬性和方法的組合。
正是數(shù)據(jù)庫(kù)中表的這種抽象形成了數(shù)據(jù)模型,它對(duì)比對(duì)象模型是不完整,所以在面向?qū)ο蠓治觯∣OA)時(shí)一定要采用對(duì)象(事物)抽象而不是數(shù)據(jù)(屬性、物體)抽象。
舉個(gè)例子:
簡(jiǎn)單金融賬戶(Account)
屬性有:賬號(hào)(id)、余額(balance)、狀態(tài)(status)
操作有:開(kāi)戶(open)、注銷(close)、存錢(credit)、取錢(debit)。
數(shù)據(jù)模型的只需要設(shè)計(jì)字段(fields)和關(guān)聯(lián)關(guān)系,所以下面的 SQL 基本已完成。
create table account
(
id integer,
balance integer,
status integer
);
如果把上述 SQL 轉(zhuǎn)換成 Java 的對(duì)象的話,得到將是一個(gè)用面向?qū)ο笤O(shè)計(jì)的數(shù)據(jù)模型,而不是完整的對(duì)象模型。這種模型在 Java 開(kāi)發(fā)中非常普遍,這是數(shù)據(jù)模型思維所導(dǎo)致的結(jié)果。
@Getter
@Setter
public class Account {
private int id;
private int balance;
private AccountStatus status;
}
如果使用對(duì)象模型的思維來(lái)設(shè)計(jì)模型,從接口上來(lái)看,他應(yīng)該是這樣的:
public interface Account {
int getId();
int getBalance();
AccountStatus getStatus();
void open();
void close();
void credit(int amount);
void debit(int amount);
}
如果 Account 接口符合金融賬戶的設(shè)計(jì),那么 Account 最簡(jiǎn)單地實(shí)現(xiàn)應(yīng)該如下:
@Getter
public class Account {
private int id;
private int balance;
private AccountStatus status;
public void open() {
this.status = AccountStatus.OPENED;
}
public void close() {
this.status = AccountStatus.CLOSED;
}
public void credit(int amount) {
this.balance += amount;
}
public void debit(int amount) {
this.balance -= amount;
}
}
這是從兩個(gè)建模的角度來(lái)對(duì)比對(duì)象模型和數(shù)據(jù)模型的不同,下面我們還要從完整地執(zhí)行流程來(lái)對(duì)比。
Account Credit
首先是使用數(shù)據(jù)模型所設(shè)計(jì)的時(shí)序圖,因?yàn)閿?shù)據(jù)模型下的 Account 不包含業(yè)務(wù)邏輯,所有的業(yè)務(wù)邏輯都在 AccountService 中,所以通常稱為業(yè)務(wù)邏輯服務(wù)(層)或者事務(wù)腳本。如圖下:

使用 Java 代碼的實(shí)現(xiàn):
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public Account creditAccount(int accountId, int amount) {
var account = this.accountRepository.findById(accountId)
.orElseThrow(() -> new AccountException("The Account was not found"));
if (AccountStatus.OPENED != account.getStatus()) {
throw new AccountException("The Account is not open");
}
account.setBalance(account.getBalance() + amount);
return this.accountRepository.save(account);
}
}
現(xiàn)在我們要使用對(duì)象模型的思維進(jìn)行設(shè)計(jì)時(shí)序圖

使用 Java 代碼的實(shí)現(xiàn):
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public Account creditAccount(int accountId, int amount) {
var account = this.accountRepository.findById(accountId)
.orElseThrow(() -> new AccountException("The Account was not found"));
account.debit(amount);
return this.accountRepository.save(account);
}
}
在 AccountService 的 creditAccount 方法中已經(jīng)沒(méi)有了業(yè)務(wù)代碼,更多地是協(xié)調(diào)調(diào)用執(zhí)行流程。對(duì)于這種只用來(lái)實(shí)現(xiàn)執(zhí)行流程,不在包含業(yè)務(wù)邏輯的服務(wù)對(duì)象,將它們稱為應(yīng)用服務(wù)(Application Service)。
舉個(gè)家政服務(wù)公司與 AccountService 相似的例子:
比如你想請(qǐng)一位保潔阿姨給家里做一做清潔工作,首先是你打電話給家政服務(wù)公司說(shuō)你要給家里做一做清潔工作,然后家政公司安排一位保潔阿姨去你家?guī)兔ν瓿汕鍧嵐ぷ?,在這個(gè)過(guò)程中家政公司主要做了接待、協(xié)調(diào)、安排、最后可能包含一些保潔阿姨的績(jī)效等一系列工作。上面的 AccountService 也一樣是在做這樣的一件事情。所以在對(duì)象模型中,AccountService 只需要做像家政公司這樣協(xié)調(diào)工作,具體地工作由保潔阿姨來(lái)完成,這里的保潔阿姨就相當(dāng)于 Account 對(duì)象。
從兩處對(duì)比來(lái)看,采用數(shù)據(jù)模型建模配合業(yè)務(wù)邏輯服務(wù)的方式更像是過(guò)程化編程,只是在使用面向?qū)ο笳Z(yǔ)言來(lái)編寫(xiě)過(guò)程化代碼。而采用對(duì)象模型配合應(yīng)用服務(wù)的方式才是符合面向?qū)ο缶幊獭?/p>
組合與聚合
在多數(shù)的業(yè)務(wù)開(kāi)發(fā)中,普遍提到的是關(guān)聯(lián)關(guān)系(一對(duì)一、一對(duì)多、多對(duì)多)和繼承泛化,很少去關(guān)注組合與聚合,但是組合與聚合在面向?qū)ο笾惺窍喈?dāng)重要的。
組合與聚合是在探討整體與部分的關(guān)系,這種整體與部分的關(guān)系是一種比關(guān)聯(lián)關(guān)系更強(qiáng)的關(guān)系。比如:汽車與輪胎,汽車是一個(gè)整體,輪胎是汽車的一部分。如果汽車沒(méi)有輪胎,那么就無(wú)法構(gòu)成汽車的完整性,所以在討論整體與部分的關(guān)系時(shí),要特別注意整體對(duì)部分的依賴性而不是部分對(duì)整體的依賴。
首先通過(guò)一個(gè)人進(jìn)食過(guò)程的用例來(lái)考慮整體與部分的依賴關(guān)系,然后在例子中說(shuō)明組合與聚合區(qū)別和聯(lián)系。
這個(gè)進(jìn)食過(guò)程需要多個(gè)人體器官協(xié)作配合。首先是通過(guò)一種方式將食物送進(jìn)口腔,由牙齒的咀嚼和舌頭的攪拌,再由喉嚨吞咽,從食道進(jìn)入胃中,在通過(guò)胃里進(jìn)行初步消化,將飲食變成食糜,然后傳入小腸后,在脾的運(yùn)化作用下,精微物質(zhì)被吸收。
注意:這個(gè)從嘴到胃的執(zhí)行過(guò)程并不是一個(gè) Input/Output 方式,而是一個(gè) Stream 方式,后面還有連接。從這個(gè)角度來(lái)考慮嘴只是 Stream 的入口,但是這個(gè)用例主要是想說(shuō)明整體與部分的聯(lián)系,所以把這種連接的每一個(gè)部分修改成 Input/Output 調(diào)用方式。
為這次進(jìn)食過(guò)程來(lái)建模吧!首先確定關(guān)鍵的對(duì)象模型有:人(Person)、嘴(Mouth)、食道(Esophagus)、胃(Stomach)、腸道(Intestine)。代碼如下:
// 嘴
public class Mouth {
public Object chew(Object food) {
return food;
}
}
// 食道
public class Esophagus {
public Object transfer(Object paste) {
return paste;
}
}
// 胃
public class Stomach {
public Object fill(Object paste) {
return paste;
}
}
// 腸道
public class Intestine {
public void absorb(Object chyme) {
// absorbing...
}
}
public class Person {
private final Mouth mouth;
private final Esophagus esophagus;
private final Stomach stomach;
private final Intestine intestine;
public Person() {
this.mouth = new Mouth();
this.esophagus = new Esophagus();
this.stomach = new Stomach();
this.intestine = new Intestine();
}
public void eat(Object food) { // 進(jìn)食。
var paste = this.mouth.chew(food); // 咀嚼形成漿糊。
paste = this.esophagus.transfer(paste); // 通過(guò)食道傳送食物。
var chyme = this.stomach.fill(paste); // 填充到胃里形成食糜。
this.intestine.absorb(chyme); // 在腸道里吸收營(yíng)養(yǎng)。
// 便秘中...
}
}
public class PersonTests {
public static void main(String[] args) {
new Person().eat("chips");
}
}
在整個(gè)進(jìn)食流程中,是由人(Person)做的吃(eat)這個(gè)動(dòng)作開(kāi)始,然后由人體內(nèi)部的多個(gè)參與的部分對(duì)象協(xié)調(diào)完成的,這就是整體與部分的關(guān)系。Person 是個(gè)整體,Mouth, Esophagus, Stomach, Intestine 是整體內(nèi)的部分。然后在考慮一個(gè)事情,這些部分對(duì)象是不是依附在整體對(duì)象上,比如:嘴是不是獨(dú)立于人體不能存活,伴隨著人的存在而存在,消亡而消亡。這種部分對(duì)象的創(chuàng)建、存在和消亡都是和整體對(duì)象一起的就稱為組合。而聚合就不像組合的整體與部分的關(guān)系那么強(qiáng),比如:汽車與輪胎是一個(gè)整體與部分的關(guān)系,汽車沒(méi)有輪胎肯定跑不了。但是汽車可以更換輪胎,這種可以更換的關(guān)系就沒(méi)有組合關(guān)系那么強(qiáng)。除了更換還有缺少的,比如:螃蟹有八條腿,總的來(lái)說(shuō)螃蟹沒(méi)有腿肯定是無(wú)法行走的,但是缺少一個(gè)兩個(gè)還是能行走的,可能行走有一些困難。這樣的可以在初始化之后能夠更換的或者不需要強(qiáng)制完整的整體與部分的關(guān)系稱之為聚合。
隨著時(shí)間的向前和空間的擴(kuò)大,組合和聚合還是會(huì)存在轉(zhuǎn)換的情況,比如未來(lái)人可以換嘴、進(jìn)食流程不需要嘴的參與,再比如說(shuō)一次性轎車,出廠后就不能維修更換等等。所以在討論組合與聚合的關(guān)系時(shí),要在一定的限界下來(lái)討論。
開(kāi)源電商
Mallfoundry 是一個(gè)完全開(kāi)源的使用 Spring Boot 開(kāi)發(fā)的多商戶電商平臺(tái)。它可以嵌入到已有的 Java 程序中,或者作為服務(wù)器、集群、云中的服務(wù)運(yùn)行。
- 領(lǐng)域模型采用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)、接口化以及面向?qū)ο笤O(shè)計(jì)。
項(xiàng)目地址:https://gitee.com/mallfoundry/mall
總結(jié)
- 對(duì)象建模,通過(guò)對(duì)象模型與數(shù)據(jù)模型的對(duì)比來(lái)說(shuō)明需要一種對(duì)象模型的思維。
- 對(duì)象建模的應(yīng)用,通過(guò)賬戶存款的業(yè)務(wù)來(lái)簡(jiǎn)要說(shuō)明如何使用對(duì)象模型。
- 組合與聚合,通過(guò)重點(diǎn)說(shuō)明組合與聚合,讓其在對(duì)象模型的基礎(chǔ)上,討論整體與部分的關(guān)系。