我們常見的軟體設計方式為分層架構,由上到下一般分為:controller、service 以及 repository 三層,採由上往下相依的方式建構。
由此方式設計的程式碼很容易得到依照分層的方式。
com.app
└── controller
├── CompanyController
├── ProductController
└── UserController
└── service
├── CompanyService
├── ProductService
└── UserService
└── api
├── PartnerApiClient
├── MemberApiClient
└──
└── repository
├── CompanyRepository
├── ProductRepository
└── UserRepository
技術 (layer) 導向的區分方式,無法一眼看出核心業務邏輯 (screening)
。
不利於將來採用 micro-services 的拆分,容易導致大泥球 (a big ball of mud) 的混亂程式碼。
一種組織業務邏輯的模式,它將每個業務操作封裝成單一的過程,這個過程就是一個 transaction script。在這種模式中,每個腳本負責從呼叫者接收輸入,執行所有必要的處理,並返回結果或更新數據存儲。
範例:
async composeTransferPageUrl({
userId,
partnerCode,
productUrl,
deepLinkUrl,
partnerProductId,
partnerProductTitle,
}) {
try {
logger.info('composeTransferPageUrl');
let cidKey = '';
let cid = '';
let tagKey = '';
const userTrackCode = await this.getTrackingCode({ currentUserId });
const tag = userTrackCode.trackingCode;
let otaCode = partnerProductTitle;
let partner = null;
let partnerProduct = null;
if (deepLinkUrl) {
logger.info(`hashedDeeplink: ${deepLinkUrl}`);
deepLinkUrl = decodeUri(deepLinkUrl).toString().replace(/\\s/g, '+');
url = this.encryptService.decrypt(deepLinkUrl);
url = new URL(url);
const params = new UrlParams(url.search.slice(1));
const name = params.get('name');
if (name) {
otaCode = name;
params.delete('name');
}
url.search = params.toString();
}
url = decodeUri(url);
if (partnerCode) {
partner = await this.models.Partner.findOne({
code: partnerCode,
});
} else if (partnerProductId) {
partnerProduct = await this.models.PartnerProduct.findOne({ _id: partnerProductId }).populate('partner_id').populate('type_id');
partner = partnerProduct ? partnerProduct.partner_id : null;
} else if (otaCode) {
const ota = await this.models.ota.findOne({ ota_code: otaCode });
partnerProduct = await this.models.Product.findOne({ _id: ota.product_id }).populate('partner_id');
partnerProductId = partnerProduct._id.toString();
partner = partnerProduct ? partnerProduct.partner_id : null;
} else {
const partners = await this.models.Partner.find({});
const matches = url.match(/^https?:\\/\\/([^/?#]+)(?:[/?#]|$)/i);
const domain = matches && matches[1];
logger.info(domain);
partners.forEach((o) => {
if (domain.indexOf(o.partner_url_name) > -1) {
partner = o;
}
});
}
if (_.isEmpty(partner)) throw new MsgBadRequestResponse('找不到該 partner');
keyUrl = partner.url_key;
urlValue = partner.url_value;
trackingCode = partner.tracking_code;
if (!keyUrl) throw new MsgBadRequestResponse('找不到該 partner');
if (!urlValue) throw new MsgBadRequestResponse('找不到該 partner');
if (!trackingCode) throw new MsgBadRequestResponse('找不到該 partner');
switch (partner.partner_url_name) {
case 'partner1':
partnerProductId = config.get('partner1');
break;
case 'partner2':
partnerProductId = config.get('partner2');
break;
case 'partner3':
partnerProductId = config.get('partner3');
break;
default:
break;
}
const { rewardRate, extraPointReward } = await this.getPointReward({
partnerId: partner._id,
partnerProductId,
});
const user = await this.models.Customer.findOne({
_id: currentUserId,
});
const imageUrl = _.get(partnerProduct, 'image_url') || _.get(partner, 'image_url') || _.get(partnerProduct, 'category_id.image_url') || '';
return {
partnerName: otaCode || partner.partner_url_name,
tag,
point: rewardRate,
extraPointReward,
imageUrl,
hasAgreeTerms: user.has_agree_terms,
url,
};
} catch (error) {
logger.error(error);
throw error;
}
}
不適合複雜的商業情境。由上面的程式範例來說,缺乏適當的業務抽象,程式難以理解與維護。開發的過程中應該逐步的重構,凸顯商業意圖,讓商業邏輯(domain entity)逐一浮現。
Example 1: anemic domain model (transaction script)
Screening: 一眼就看得出來這是什麼服務。
Microservices: 將來容易分割成獨立的 service。