緣起
幾個月前, 有看到一篇針對 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. 架構圖如下:
![]() |
| 第一版程式架構圖 |
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, 如果本文有什麼不足或錯誤之處, 尚請各位指點, 謝謝.
參考文件
- Dependency Injection in PL/SQL – Remove Hard Coded Dependencies from your code
- 此為本篇網誌的主要參考文件, 它以一個 Email 的功能作範例
- Design Patterns in PL/SQL – Interface Injection for even looser coupling
- 此篇以 Logger 為範例, 描述如可由 Logon Trigger (Dependency Injector) 將實際的 Logger Package 傳入到 HRM_SALARY_RULES 這個 Package 裡, 供其使用; 其實, 就是採用類似 Setter Property 的方式, 將真正的 Logger Package 及 Procedure 名稱傳入.
- PL/SQL Packages
- Oracle 官方網站, 內含 PL/SQL Package 的語法結構及範例
- {Huan-Lin 學習筆記} Dependency Injection 筆記 (1)
- 內含一系列的 Dependency Injection 的介紹,



沒有留言:
張貼留言