Raccoon 採用 Controller-Service-Repository 架構

Posted on Thu, Jan 18, 2024 Tech DICOM MongoDB Node.JS

原本 Raccoon 使用的是像是 MVC (Model View Controller) 架構,有 api 資料夾放置 controllers,models 放置資料處理相關的檔案。在軍中放假的期間,有嘗試重構,加入 service,讓 raccoon 比較容易地去更改資料庫依賴( MongoDB 轉換到 SQL ),雖然重構後在實作上變的簡單一些,但其實最好的狀況是更改 repository 的使用,這樣 controller 跟 service 理論上可以不更改,達到更加容易抽換資料庫的作用。

Controller - Service - Repository

🚨

以下使用 perplexity 搜尋

Controller-Service-Repository 設計模式可以幫助解耦程式內不同的內容,以提高模組化、可維護性和可測試性

Raccoon 更改前各層程式碼

以下就用最簡單的 create patient 做範例

Controller

const { Controller } = require("@root/api/controller.class");
const { ApiLogger } = require("@root/utils/logs/api-logger");
const {
  CreatePatientService,
} = require("@api/dicom-web/controller/PAM-RS/service/create-patient.service");
const { ApiErrorArrayHandler } = require("@error/api-errors.handler");

class CreatePatientController extends Controller {
  constructor(req, res) {
    super(req, res);
    this.apiLogger = new ApiLogger(this.request, "PAM-RS");
    this.apiLogger.addTokenValue();
  }
  async mainProcess() {
    this.apiLogger.logger.info("Create Patient");

    let createPatientService = new CreatePatientService(
      this.request,
      this.response
    );
    try {
      let createPatientID = await createPatientService.create();
      return this.response
        .set("content-type", "application/dicom+json")
        .status(201)
        .json(createPatientID);
    } catch (e) {
      let apiErrorArrayHandler = new ApiErrorArrayHandler(
        this.response,
        this.apiLogger,
        e
      );
      return apiErrorArrayHandler.doErrorResponse();
    }
  }
}

module.exports = async function (req, res) {
  let controller = new CreatePatientController(req, res);

  await controller.doPipeline();
};
目前 controller 算是還 ok,都是直接呼叫 service 做相對應操作,最後進行回傳

Service

const { PatientModel } = require("@dbModels/patient.model");
const { set, get } = require("lodash");
const shortHash = require("shorthash2");
const { v4: uuidV4 } = require("uuid");

class CreatePatientService {
  /**
   *
   * @param {import("express").Request} req
   * @param {import("express").Response} res
   */
  constructor(req, res) {
    this.request = req;
    this.response = res;
  }

  async create() {
    let incomingPatient = this.request.body;
    let patientID = shortHash(uuidV4());
    set(incomingPatient, "patientID", patientID);
    set(incomingPatient, "00100020.Value", [patientID]);
    const patient = new PatientModel(incomingPatient);
    await patient.save();
    return {
      patientID,
    };
  }
}

module.exports.CreatePatientService = CreatePatientService;
可以看到 service 的 create function 確實是新增 patient,但有一個問題是,呼叫的內容其實是 MongoDB 專屬的,這邊就必須更改,在 MongoDB 的功能上在 wrap 一層創建 patient 的 fcuntion

Repository

const mongoose = require("mongoose");
const _ = require("lodash");
const { tagsNeedStore } = require("../../DICOM/dicom-tags-mapping");
const { getVRSchema } = require("../schema/dicomJsonAttribute");
const { tagsOfRequiredMatching } = require("../../DICOM/dicom-tags-mapping");
const { raccoonConfig } = require("@root/config-class");
const {
  DicomSchemaOptionsFactory,
  PatientDocDicomJsonHandler,
} = require("../schema/dicom.schema");
const { dictionary } = require("@models/DICOM/dicom-tags-dic");

let patientSchemaOptions = _.merge(
  DicomSchemaOptionsFactory.get("patient", PatientDocDicomJsonHandler),
  {
    methods: {
      toDicomJson: function () {
        let obj = this.toObject();
        delete obj._id;
        delete obj.id;
        delete obj.patientID;
        delete obj.studyPaths;
        delete obj.deleteStatus;
        delete obj.createdAt;
        delete obj.updatedAt;

        return obj;
      },
    },
    statics: {
      getPathGroupQuery: function (iParam) {
        let { patientID } = iParam;
        return {
          $match: {
            "00100020.Value": patientID,
          },
        };
      },
      /**
       *
       * @param {import("../../../utils/typeDef/dicom").DicomJsonMongoQueryOptions} queryOptions
       * @returns
       */
      getDicomJsonProjection: function (queryOptions) {
        let fields = {};
        for (let tag in tagsOfRequiredMatching.Patient) {
          fields[tag] = 1;
        }
        return fields;
      },
      /**
       *
       * @param {string} patientId
       * @param {any} patient
       */
      findOneOrCreatePatient: async function (patientId, patient) {
        /** @type {PatientModel | null} */
        let foundPatient = await mongoose.model("patient").findOne({
          "00100020.Value": patientId,
        });

        if (!foundPatient) {
          /** @type {PatientModel} */
          let patientObj = new mongoose.model("patient")(patient);
          patient = await patientObj.save();
        }

        return patient;
      },
    },
  }
);

let patientSchema = new mongoose.Schema(
  {
    patientID: {
      type: String,
      default: void 0,
      index: true,
      required: true,
    },
    studyPaths: {
      type: [String],
      default: void 0,
    },
    deleteStatus: {
      type: Number,
      default: 0,
    },
  },
  patientSchemaOptions
);

// Index patient id
patientSchema.index({
  patientID: 1,
});
patientSchema.index({
  "00100020": 1,
});

let patientModel = mongoose.model("patient", patientSchema, "patient");

module.exports = patientModel;

module.exports.PatientModel = patientModel;
repository 是使用 mongoose 來實作,好在 mongoose 有 statics 和 methods 讓我們可以 wrap CRUD 的功能

Raccoon 更改後各層程式碼

Controller

Service

const { PatientModel } = require("@dbModels/patient.model");
const { set, get } = require("lodash");

class CreatePatientService {
  /**
   *
   * @param {import("express").Request} req
   * @param {import("express").Response} res
   */
  constructor(req, res) {
    this.request = req;
    this.response = res;
  }

  async create() {
    let incomingPatient = this.request.body;
    let patientID = get(incomingPatient, "00100020.Value.0");
    set(incomingPatient, "patientID", patientID);
    let patient = await PatientModel.createOrUpdatePatient(
      patientID,
      incomingPatient
    );
    return patient.toGeneralDicomJson();
  }
}

module.exports.CreatePatientService = CreatePatientService;

Repository

...
{
	methods: {
		toGeneralDicomJson: async function () {
                let obj = this.toObject();
                delete obj._id;
                delete obj.id;
                delete obj.patientID;
                delete obj.studyPaths;
                delete obj.deleteStatus;
                delete obj.createdAt;
                delete obj.updatedAt;

                return obj;
    }
	},
	statics: {
		/**
     * 
     * @param {string} patientID 
     * @param {any} patient patient general dicom json
     */
    createOrUpdatePatient: async function(patientID, patient) {
        return await mongoose.model("patient").findOneAndUpdate({
            patientID
        }, patient, { upsert: true, new: true });
    }
	}
}
...

如何抽換成 SQL?

當我們真正抽換資料庫換成 SQL 時,其實我們應該需要更改 require 的路徑,不過 Raccoon 在後面有使用 module-alias 這款好工具,直接去設定檔改一下,create patietn 程式碼當中的 const { PatientModel } = require("@dbModels/patient.model"); 就可以引入不同資料庫實作的檔案囉!!

參考資料