︿
Top

2015年1月18日 星期日

Oracle PL/SQL Dependency Injection for loose coupling


緣起


幾個月前, 有看到一篇針對 Oracle PL/SQL 進行重構的文章, 改用類似 Dependency Injection 的方式來進行設計, 直到最近才有時間撰寫網誌, 留下記錄.

該作者主要有2篇文章, 筆者在參考文件, 作了一些摘要說明. 真的很難想像 Oracle PL/SQL 可以實作一些 Design Pattern.

以下內容, 係筆者依目前工作上接觸到的產業進行情境設計.

完整程式範例, 筆者放在 GitHub, 請由此下載.





假設情境


以 證券後台系統 補單功能 的 現股賣出 交易為例, 其 交割金額 的邏輯如下:
(1) 價金(TradeAmt) = 成交價格 * 成交股數
(2) 交易稅(TradeTax) = 價金 * 千分之 3
==>這個是要給政府的
(3) 手續費(CommFee) = 價金 * 千分之 1.425    
==> 這個是券商的收入, 是券商可操控的部份,可能會依一些條件, 而有不同的費率
==> 本例只依現場單或電子單, 而有不同的費率
==> 現場單: 千分之1.425 (CommFeeRate_NonEC)
==> 電子單: 千分之1.325 (CommFeeRate_EC)
(4) 交割金額(SettleAmt) = 價金 - 交易稅 - 手續費
==> 這個才是真正進入客戶帳戶的錢

本文以手續費為例, 假設各家券商的手續費率的商業邏輯都相同, 只有費率不同.

第一版程式


1. 摘要說明: 參考架構圖
(1) 共有2個 Package (可以視為 C# 的物件):
* pkgDIMain_v01 (主程式模組) : 此 Package 為外部程式呼叫的入口, 有一個 plCalcSettleAmtForCashSell() 方法, 供外部呼叫
* pkgDIPayment_v01 (計費模組): 此 Package 主要負責交易稅及手續費的計算.
2. 問題點如下:
(1) 手續費率寫死了, 沒有彈性
3. 架構圖如下:
第一版程式架構圖

4. 程式碼如下:

CREATE OR REPLACE PACKAGE pkgDIMain_v01 
IS
  
/*******************************************************************************
   NAME:       plCalcSettleAmtForCashSell
   PURPOSE:    進行現股賣出交易的應付金額計算 (以券商角度而言, 是要支付金額給客戶)
   PARAMATER(S):
   現股賣出的交易可以分為3段
   (1) 價金(TradeAmt) = 成交價格 * 成交股數
   (2) 交易稅(TradeTax) = 價金 * 千分之 3         //這個是要給政府的
   (3) 手續費(CommFee) = 價金 * 千分之 1.425      //這個是券商的收入, 是券商可操控的部份, 
                                                  //可能會依一些條件, 而有不同的費率
                                                  //本例只依現場單或電子單, 而有不同的費率
                                                  //現場單: 千分之1.425 (CommFeeRate_NonEC)
                                                  //電子單: 千分之1.325 (CommFeeRate_EC)
   (4) 交割金額(SettleAmt) = 價金 - 交易稅 - 手續費          //這個才是真正進入客戶帳戶的錢
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   IN             pi_TradePrz    成交價格
   IN             pi_TradeQty    成交股數
   OUT   NOCOPY   po_ReturnMsg   交割金額
*******************************************************************************/
   PROCEDURE plCalcSettleAmtForCashSell   (  
      pi_ChannelKind IN          VARCHAR2,       --通路種類 ('1': 現場, '2':電子)
      pi_TradePrz    IN          NUMBER,     --成交價格
      pi_TradeQty    IN          NUMBER,     --成交股數
      po_SettleAmt   OUT NOCOPY  NUMBER      --交割金額
      );
      
END pkgDIMain_v01;
/
CREATE OR REPLACE PACKAGE BODY pkgDIMain_v01 
IS

/*******************************************************************************
   NAME:       plCalcSettleAmtForCashSell
   PURPOSE:    進行現股賣出交易的應付金額計算 (以券商角度而言, 是要支付金額給客戶)
*******************************************************************************/
   PROCEDURE plCalcSettleAmtForCashSell(
      pi_ChannelKind IN          VARCHAR2,       --通路種類 ('1': 現場, '2':電子)
      pi_TradePrz    IN          NUMBER,     --成交價格
      pi_TradeQty    IN          NUMBER,     --成交股數
      po_SettleAmt   OUT NOCOPY  NUMBER      --交割金額
      )
   IS
      lv_TradeAmt    NUMBER(16,2);     --價金
      lv_TradeTax    NUMBER(16,2);     --交易稅
      lv_CommFee     NUMBER(16,2);     --手續費
   BEGIN
      lv_TradeAmt := pi_TradeQty * pi_TradePrz;
      lv_TradeTax := pkgDIPayment_v01.fcGetTradeTax(lv_TradeAmt);
      lv_CommFee  := pkgDIPayment_v01.fcGetCommeFee(pi_ChannelKind, lv_TradeAmt);
      po_SettleAmt := lv_TradeAmt - lv_TradeTax - lv_CommFee;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         NULL;
      WHEN OTHERS THEN
         RAISE;
   END plCalcSettleAmtForCashSell;  

BEGIN
   -- Initialization
   NULL;
END pkgDIMain_v01;
/

CREATE OR REPLACE PACKAGE pkgDIPayment_v01 
IS

/*******************************************************************************
   NAME:       fcGetTradeTax
   PURPOSE:    取得交易稅
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   IN             pi_TradeAmt    價金
   RETURN         --             交易稅
*******************************************************************************/
   FUNCTION fcGetTradeTax(
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER;

/*******************************************************************************
   NAME:       fcGetCommFee
   PURPOSE:    取得手續費
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   IN             pi_ChannelKind 通路種類 ('1': 現場, '2':電子)
   IN             pi_TradeAmt    價金
   RETURN         --             手續費
*******************************************************************************/
   FUNCTION fcGetCommeFee(
      pi_ChannelKind IN          VARCHAR2,       --通路種類 ('1': 現場, '2':電子)
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER;

END pkgDIPayment_v01;
/
CREATE OR REPLACE PACKAGE BODY pkgDIPayment_v01 
IS

/*******************************************************************************
   NAME:       fcGetTradeTax
   PURPOSE:    取得交易稅
*******************************************************************************/
   FUNCTION fcGetTradeTax(
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER
   IS
      lv_Result   NUMBER(16,2);
   BEGIN
      lv_Result := pi_TradeAmt * 3 / 1000;      
      RETURN lv_Result;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         NULL;
      WHEN OTHERS THEN
         RAISE; 
   END fcGetTradeTax;
   

/*******************************************************************************
   NAME:       fcGetCommFee
   PURPOSE:    取得手續費
*******************************************************************************/
   FUNCTION fcGetCommeFee(
      pi_ChannelKind IN          VARCHAR2,       --通路種類 ('1': 現場, '2':電子)
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER
   IS
      lv_Result   NUMBER(16,2);
   BEGIN
      lv_Result := 0;
      IF pi_ChannelKind = '1' THEN
         lv_Result := pi_TradeAmt * 1.425 / 1000;      
      ELSE
         lv_Result := pi_TradeAmt * 1.325 / 1000;      
      END IF;
      RETURN lv_Result;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         NULL;
      WHEN OTHERS THEN
         RAISE; 
   END fcGetCommeFee;

BEGIN
   -- Initialization
   NULL;
END pkgDIPayment_v01;
/



第二版程式


1. 摘要說明:
或許各位有想到, 可以把手續費率建到檔案, 亦即多一個 config table; 或者增加一個 Package 存放常數 (suppose pkgCommon_v02), 但這樣會造成 pkgDIPayment 還是相依於那個 config table 或 pkgCommon ; 也就是說, 當要部署 pkgDIPayment_v02 時, 那個 config table 也要跟著一併部署.
2. 問題點如下:
(1) 難以給其它系統使用, 因為相依於某個 config table 或 Package
3. 架構圖如下: 將手續費率放在一個 pkgCommon_v02 的 Package 裡, 供 pkgDIPayment_v02 使用
第二版程式架構

4. 程式碼的部份, 由於不想佔用篇幅, 有興趣者, 可以自行下載 (本文的開頭, 有提供下載的連結)

第三版程式


1. 摘要說明:
換個方向, 假設我們可以由外部填值給 pkgDIPayment_v03, 而不是由 pkgPayment_v03 自己去取得, 那麼, 是否就可以增加 pkgDIPayment_v03 的可移植性了呢? 似乎有點 IOC (Inverse of Control) 的味道了?!
2. 架構圖如下:
第三版程式架構圖

3. 程式碼如下:
(1) 新增一個 CONFIGURATION_VALUES table, 並建立 2 筆資料.
-- 建立資料表
CREATE TABLE CONFIGURATION_VALUES
(
  PACKAGE_NAME VARCHAR2(30) not null,
  PROPERTY     VARCHAR2(50) not null,
  VALUE        VARCHAR2(4000)
)
;

-- 建立測試資料
INSERT INTO CONFIGURATION_VALUES (PACKAGE_NAME, PROPERTY, VALUE)
VALUES ('pkgDIPayment_v03', 'CommFeeRate_NonEC', '0.001425');
INSERT INTO CONFIGURATION_VALUES (PACKAGE_NAME, PROPERTY, VALUE)
VALUES ('pkgDIPayment_v03', 'CommFeeRate_EC', '0.001325');
COMMIT;

(2) 新增一個 pkgDI_Injector (相依注入模組): 負責在使用者登入時, 取得上述的參數資料, 將其注入到 pkgDIPayment. 此部份, 必須實作 2 段程式, 第1段建立該 Package, 第2段在登入的 trigger 呼叫該 Package 的 Inject() 方法
CREATE OR REPLACE PACKAGE pkgDI_Injector
IS
   /*******************************************************************************
   NAME:       plInject
   PURPOSE:    讀取設定的參數, 並注入各相關的模組
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
*******************************************************************************/
   PROCEDURE plInject;

END pkgDI_Injector;
/
CREATE OR REPLACE PACKAGE BODY pkgDI_Injector
IS

   /*******************************************************************************
   NAME:       plInject
   PURPOSE:    讀取設定的參數, 並注入各相關的模組
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   *******************************************************************************/
   PROCEDURE plInject
   IS
   BEGIN
      -- loop over all properties that are configured in table CONFIGURATION_VALUES
      FOR r IN (SELECT cve.package_name
                 ,      cve.property
                 ,      cve.value
                 FROM   CONFIGURATION_VALUES cve
                 )
      LOOP
         -- for each property
         EXECUTE IMMEDIATE 'begin '||r.package_name||'.plSet_'||r.property||'(:1); end;' USING r.value;
      END LOOP;
   END plInject;

END pkgDI_Injector;
/

CREATE OR REPLACE TRIGGER Dependency_Injection
AFTER LOGON  ON SCHEMA
BEGIN
   pkgDI_Injector.plInject;
END;

(3) 修改 pkgDIPayment 的程式
A. 加入 2 個私有欄位及 2個公開方法 (類似 C# 的 private field 及 set 程式區塊)
gv_CommFeeRate_NonEC NUMBER; --現場手續費率 (Private Field)
gv_CommFeeRate_EC NUMBER; --電子手續費率 (Private Field) >br /> plSet_CommFeeRate_NonEC(...) --設定現場交易的手續費率
plSet_CommFeeRate_EC(...) --設定電子交易的手續費率
CREATE OR REPLACE PACKAGE pkgDIPayment_v03 
IS

/*******************************************************************************
   NAME:       fcGetTradeTax
   PURPOSE:    取得交易稅
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   IN             pi_TradeAmt    價金
   RETURN         --             交易稅
*******************************************************************************/
   FUNCTION fcGetTradeTax(
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER;

/*******************************************************************************
   NAME:       fcGetCommFee
   PURPOSE:    取得手續費
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   IN             pi_ChannelKind 通路種類 ('1': 現場, '2':電子)
   IN             pi_TradeAmt    價金
   RETURN         --             手續費
*******************************************************************************/
   FUNCTION fcGetCommeFee(
      pi_ChannelKind IN          VARCHAR2,       --通路種類 ('1': 現場, '2':電子)
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER;
   
/*******************************************************************************
   NAME:       plSet_CommFeeRate_NonEC
   PURPOSE:    設定現場交易的手續費率
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   IN             pi_CommFeeRate_NonEC 現場交易的手續費率
*******************************************************************************/
   PROCEDURE plSet_CommFeeRate_NonEC(
      pi_CommFeeRate_NonEC    IN    NUMBER       --現場交易的手續費率
      ); 
   
/*******************************************************************************
   NAME:       plSet_CommFeeRate_EC
   PURPOSE:    設定電子交易的手續費率
   PARAMATER(S):
   Mode           Name           Description
   ---------      ------------   -----------------------------------------------
   IN             pi_CommFeeRate_EC 電子交易的手續費率
*******************************************************************************/
   PROCEDURE plSet_CommFeeRate_EC(
      pi_CommFeeRate_EC    IN    NUMBER       --電子交易的手續費率
      );  

END pkgDIPayment_v03;
/
CREATE OR REPLACE PACKAGE BODY pkgDIPayment_v03 
IS

   gv_CommFeeRate_NonEC    NUMBER;  --現場手續費率 (Private Field)
   gv_CommFeeRate_EC       NUMBER;  --電子手續費率 (Private Field)

/*******************************************************************************
   NAME:       fcGetTradeTax
   PURPOSE:    取得交易稅
 *******************************************************************************/
   FUNCTION fcGetTradeTax(
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER
   IS
      lv_Result   NUMBER(16,2);
   BEGIN
      lv_Result := pi_TradeAmt * 3 / 1000;      
      RETURN lv_Result;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         NULL;
      WHEN OTHERS THEN
         RAISE; 
   END fcGetTradeTax;
   

/*******************************************************************************
   NAME:       fcGetCommFee
   PURPOSE:    取得手續費
******************************************************************************/
   FUNCTION fcGetCommeFee(
      pi_ChannelKind IN          VARCHAR2,       --通路種類 ('1': 現場, '2':電子)
      pi_TradeAmt    IN          NUMBER          --價金
      )
   RETURN NUMBER
   IS
      lv_Result   NUMBER(16,2);
   BEGIN
      lv_Result := 0;
      IF pi_ChannelKind = '1' THEN
         lv_Result := pi_TradeAmt * gv_CommFeeRate_NonEC;      
      ELSE
         lv_Result := pi_TradeAmt * gv_CommFeeRate_EC;     
      END IF;
      RETURN lv_Result;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         NULL;
      WHEN OTHERS THEN
         RAISE; 
   END fcGetCommeFee;
   
/*******************************************************************************
   NAME:       plSet_CommFeeRate_NonEC
   PURPOSE:    設定現場交易的手續費率
*******************************************************************************/
   PROCEDURE plSet_CommFeeRate_NonEC(
      pi_CommFeeRate_NonEC    IN    NUMBER       --現場交易的手續費率
      )   
   IS
   BEGIN
      gv_CommFeeRate_NonEC := pi_CommFeeRate_NonEC;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         NULL;
      WHEN OTHERS THEN
         RAISE; 
   END plSet_CommFeeRate_NonEC;
   
/*******************************************************************************
   NAME:       plSet_CommFeeRate_EC
   PURPOSE:    設定電子交易的手續費率
*******************************************************************************/
   PROCEDURE plSet_CommFeeRate_EC(
      pi_CommFeeRate_EC    IN    NUMBER       --電子交易的手續費率
      )   
   IS
   BEGIN
      gv_CommFeeRate_EC := pi_CommFeeRate_EC;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         NULL;
      WHEN OTHERS THEN
         RAISE; 
   END plSet_CommFeeRate_EC;

BEGIN
   -- Initialization
   NULL;
END pkgDIPayment_v03;
/

呼叫範例


DECLARE 
   lv_SettleAmt   NUMBER(16,2);
BEGIN
   pkgDIMain_v03.plCalcSettleAmtForCashSell(
   '1', 
   10, 
   2000,
   lv_SettleAmt
   );
   
   DBMS_OUTPUT.PUT_LINE('交割金額=' || lv_SettleAmt);  
   
-- OUTPUT:
-- 交割金額=19911.5
END;



總結

本文的情境, 只是解決 pkgDIPayment_vxx 與手續費率設定的相依性, 將手續費率取得的方式, 由 pkgDIPayment 自行取得, 改為由外部傳入; 這樣有個好處, 就是其它系統 (例如: 證券前台系統) 要用到這個 Package, 只要複製過去即可, 不用再複製其它額外的設定檔.

關於 PL/SQL 如何實作各種 Design Pattern 的部份, 可以參考本文參考文件的前2篇文章.

筆者對於 DI (相依性注入) 這個議題, 只是略知皮毛, 還沒有深入 Study, 如果本文有什麼不足或錯誤之處, 尚請各位指點, 謝謝.

參考文件


沒有留言:

張貼留言