Problem Statement

分層式的架構

我們常見的軟體設計方式為分層架構,由上到下一般分為: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 (Anemic domain model)

一種組織業務邏輯的模式,它將每個業務操作封裝成單一的過程,這個過程就是一個 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)

Solution

Package by feature (then layer)

Screening: 一眼就看得出來這是什麼服務。

Microservices: 將來容易分割成獨立的 service。