搜尋此網誌

顯示具有 物件導向 標籤的文章。 顯示所有文章
顯示具有 物件導向 標籤的文章。 顯示所有文章

2014/01/17

開發 SAP RFC 專案的架構技巧

開發 SAP RFC 專案的架構技巧

情境:使用 DotNET Client + SAP RFC 進行庫存管理

要測試 SAP RFC (Remote Function Call) 似乎真的要有 SAP 系統以及其 SAPGui 工具(很龐大),
就算把 SAP 架起來了也不是隨時有環境可進行測試,這一定會影響功能面的開發進度。
因為大部分的功能看來都與 SAP RFC 無關,所以被此卡住就不划算了。

先介紹一個常見的 SA/SD 技巧:虛擬倉 + 虛擬儲位:
  • 仔細想想就會發現所有的庫存交易都是Movement(移動)
  • Movement 一定要有的資料是:品項、來源、目的、與數量。
  • 若把系統範圍之外的地方想像成虛擬倉/虛擬儲位的話,
    則所有的
    庫存交易都可用一致的的 Movement 邏輯處理掉,不必再裡廠內或廠外的問題。

舒緩此類系統邊界問題的架構技巧如下(此技巧也常用於 module 邊界):
  • 建立一個 SapRfc 父類別並定義出要使用的函式介面。
  • 再定義兩個 SapRfc 的子類別例如:SapRfcMock 與 SapRfcReal:
    • SapRfcMock 是模擬介面,裡頭要用一般 DB 或 只用 Collection 隨你的便,這部分很單純。
    • SapRfcReal 是真正去做 RFC 呼叫的類別,將 RFC 的 code 都放在這裡。
    • 這樣功能面與 RFC 的工作就可平行地進展。
  • 若使用 DotNET,建議不要再用 DataTable 之類的東西(因為也不會有DB),
    因為靈活的 DataSource 屬性
    才是 DotNET的強項。
    • 最好是建立 Model 物件(EX: Warehouse, Locator, Inventory, Product,...)
    • 將上述的 Model 物件放到 List 之類的東西就可與控制項 binding 了。
    • 若一鄧要用 DataTable 將面臨更多的轉換動作。
事實上 Model + SapRfcMock 定義完成後,一至三天內馬上就可開始功能面的開發。
而且這個定義可以用遞增的方式進行,不用一次全部做完,所以只需要很短的時間就做可出第一版。

UI(client端) 必須使用 SapRfc 呼叫以read/write需要的資料。
千萬不要一開始就去想真正的 RFC 呼叫,因為研究 RFC 會卡住功能的開發。
只需用一個 flag 決定傳回 SapRfcMock 或 SapRfcReal 即可,整個感覺如下:
C# 檔案見:
https://drive.google.com/file/d/0B9exai2F1cufMHRPLWlpVUVXWGs/edit?usp=sharing

01 public sealed class SapRfcFactory {
02     private static SapRfc singleton = null;
03 
04     public static SapRfc GetInstance() {
05         // 在此處決定要使用 real 或 mock 物件
06         if (singleton = null)
07             singleton = new SapRfcMock();
08         //singleton = new SapRfcReal();
09 
10         return singleton;
11     }
12 
13     private SapRfcFactory() {}
14 
15     // other codes...
16 }
17 
18 // 定義 RFC 呼叫介面
19 public abstract class SapRfc {
20     // 可以在此成員存放共用參數
21     Dictionary<string, object> dicParams = new Dictionary<string, object>();
22 
23     public abstract bool Logon();
24     public abstract void Logout();
25 }
26 
27 public class SapRfcMock: SapRfc {
28     public override bool Logon() {
29         // 直接回傳 true
30         return true;
31     }
32 
33     public override void Logout() {
34         // do nothing.
35     }
36 }
37 
38 public class SapRfcReal: SapRfc {
39     public override bool Logon() {
40         // 進行實際的 RFC 登入...
41     }
42 
43     public override void Logout() {
44         // 進行實際的 RFC 登出...
45     }
46 }


2013/08/20

一個簡單的 Delphi O/R Mapping 工具

一個簡單的 Delphi O/R Mapping 工具。
這裡好像無法上傳檔案,需要者可與我聯絡:

  albertdongtw@gmail.com

或由此下載:

Download

Entity Builder主要的功能在於產生Delphi類別來對映(mapping)既有的資料表。基本上,資料表的欄位會對映到Delphi類別的屬性(property)。但是手動做這些工作是非常辛苦的,所以我們可以使用這個工具程式來建立基本的類別骨架。
執行程式後的啟始畫面。


按下「建立ADO連線」按鈕設定ADO連線。

接著如同使用TADOConnection元件般設定ConnectionString。



此時會顯示資料庫與資料表(不含系統資料表)清單。

由資料表清單選擇一個資料表。


按「預覽」按鈕即可顯示產生的程式碼。可按滑鼠右鍵儲存到檔案或是複製到剪貼簿。




  
產生出來的完整程式碼如下:
{-------------------------------------------------------------------------------
This file is generated by EntityBuilder v1.2
provided by Albert Dong, albertdongtw@gmail.com

The Template Engine is from:
    http://sourceforge.net/projects/delphi-templeng/
--------------------------------------------------------------------------------

Class: User
Table: user
Author: XXX
Date: 2013/8/20 下午 07:06:39
Description:
-------------------------------------------------------------------------------}

unit uUser;

interface

uses
  Windows, Messages, SysUtils, Variants, Contnrs, Classes, DB, uEntity;

const
  TBL_User = 'user';

type
  {**
   * Class description.
   *
   * @author XXX, 2013/08/20 19:06:39. Created.
   *}
  TUser = class(TEntity)
  private
    // ------ Fields ------
    FNo: string;
    FName: string;
    FNameEng: string;
    FEmail: string;
    FRecvCalNotify: integer;
    FRecvCalOverdue: integer;
    FIsAdmin: integer;

  protected
    // ------ Property Accessors ------
    function GetNo: string;
    procedure SetNo(value: string);

    function GetName: string;
    procedure SetName(value: string);

    function GetNameEng: string;
    procedure SetNameEng(value: string);

    function GetEmail: string;
    procedure SetEmail(value: string);

    function GetRecvCalNotify: integer;
    procedure SetRecvCalNotify(value: integer);

    function GetRecvCalOverdue: integer;
    procedure SetRecvCalOverdue(value: integer);

    function GetIsAdmin: integer;
    procedure SetIsAdmin(value: integer);


    procedure FillData(DataSet: TDataSet); override;

  public
    // ------ Utility Class Methods ------
    class function ListAll: TList;
    class procedure DeleteAll;

    constructor Create; override;
    destructor Destroy; override;

    function ToString: string; override;

    procedure Reload;
    procedure Insert;
    procedure Update;
    procedure Delete;

  published
    // ------ Properties ------
    property No: string read GetNo write SetNo;
    property Name: string read GetName write SetName;
    property NameEng: string read GetNameEng write SetNameEng;
    property Email: string read GetEmail write SetEmail;
    property RecvCalNotify: integer read GetRecvCalNotify write SetRecvCalNotify;
    property RecvCalOverdue: integer read GetRecvCalOverdue write SetRecvCalOverdue;
    property IsAdmin: integer read GetIsAdmin write SetIsAdmin;
  end;

implementation

uses
  uToString;

{ TUser }

class function TUser.ListAll: TList;
var
  stmt: string;
begin
  stmt := 'SELECT * FROM ' + TBL_User;
  result := TEntity.ExecQueryForList(stmt, TUser);
end;

class procedure TUser.DeleteAll;
begin
  ExecUpdate('DELETE FROM ' + TBL_User);
end;

constructor TUser.Create;
begin
  inherited Create;
  // TODO: Constructor...
end;

destructor TUser.Destroy;
begin
  // TODO: Destructor...
  inherited Destroy;
end;

(**
 * Fill object properties with the specified dataset.
 * Every subclass of TEntity inherits AsXXX() methods.
 *
 * @param DataSet the specified dataset.
 *)
procedure TUser.FillData(DataSet: TDataSet);
begin
  if DataSet = nil then
    raise Exception.Create('Nil DataSet!');

  if DataSet.IsEmpty or DataSet.Eof then
    Exit;

    FNo := AsString(DataSet, 'no');
    FName := AsString(DataSet, 'name');
    FNameEng := AsString(DataSet, 'name_eng');
    FEmail := AsString(DataSet, 'email');
    FRecvCalNotify := AsInteger(DataSet, 'recv_cal_notify');
    FRecvCalOverdue := AsInteger(DataSet, 'recv_cal_overdue');
    FIsAdmin := AsInteger(DataSet, 'is_admin');
end;

procedure TUser.Reload;
var
  stmt: string;
begin
  stmt := 'SELECT * FROM ' + TBL_User
        + ' WHERE 1 = 1 '
        + '   AND no = ' + QuotedStr(No)
    ;

  TEntity.Load(stmt, self);
end;

procedure TUser.Insert;
var
  stmt: string;
begin
  stmt := 'INSERT INTO ' + TBL_User + ' ('

        + 'no'

        + ', name'

        + ', name_eng'

        + ', email'

        + ', recv_cal_notify'

        + ', recv_cal_overdue'

        + ', is_admin'

        + ') VALUES ('

        + QuotedStr(No)

        + ', ' + QuotedStr(Name)

        + ', ' + QuotedStr(NameEng)

        + ', ' + QuotedStr(Email)

        + ', ' + IntToStr(RecvCalNotify)

        + ', ' + IntToStr(RecvCalOverdue)

        + ', ' + IntToStr(IsAdmin)
        + ') '
    ;

  ExecUpdate(stmt);
end;

procedure TUser.Update;
var
  stmt: string;
begin
  stmt := 'UPDATE ' + TBL_User + ' SET '

        + 'no = ' + QuotedStr(No)

        + ', name = ' + QuotedStr(Name)

        + ', name_eng = ' + QuotedStr(NameEng)

        + ', email = ' + QuotedStr(Email)

        + ', recv_cal_notify = ' + IntToStr(RecvCalNotify)

        + ', recv_cal_overdue = ' + IntToStr(RecvCalOverdue)

        + ', is_admin = ' + IntToStr(IsAdmin)

        + ' WHERE 1 = 1 '
        + '   AND no = ' + QuotedStr(No)
    ;

  ExecUpdate(stmt);
end;

procedure TUser.Delete;
var
  stmt: string;
begin
  stmt := 'DELETE FROM ' + TBL_User
      + ' WHERE 1 = 1 '
      + '   AND no = ' + QuotedStr(No)
    ;
  ExecUpdate(stmt);
end;

function TUser.ToString: string;
begin
  TToStringBuilder.toString(self);
end;

function TUser.GetNo: string;
begin
  result := FNo;
end;

procedure TUser.SetNo(value: string);
begin
  if FNo <> value then begin
    FNo := value;
  end;
end;

function TUser.GetName: string;
begin
  result := FName;
end;

procedure TUser.SetName(value: string);
begin
  if FName <> value then begin
    FName := value;
  end;
end;

function TUser.GetNameEng: string;
begin
  result := FNameEng;
end;

procedure TUser.SetNameEng(value: string);
begin
  if FNameEng <> value then begin
    FNameEng := value;
  end;
end;

function TUser.GetEmail: string;
begin
  result := FEmail;
end;

procedure TUser.SetEmail(value: string);
begin
  if FEmail <> value then begin
    FEmail := value;
  end;
end;

function TUser.GetRecvCalNotify: integer;
begin
  result := FRecvCalNotify;
end;

procedure TUser.SetRecvCalNotify(value: integer);
begin
  if FRecvCalNotify <> value then begin
    FRecvCalNotify := value;
  end;
end;

function TUser.GetRecvCalOverdue: integer;
begin
  result := FRecvCalOverdue;
end;

procedure TUser.SetRecvCalOverdue(value: integer);
begin
  if FRecvCalOverdue <> value then begin
    FRecvCalOverdue := value;
  end;
end;

function TUser.GetIsAdmin: integer;
begin
  result := FIsAdmin;
end;

procedure TUser.SetIsAdmin(value: integer);
begin
  if FIsAdmin <> value then begin
    FIsAdmin := value;
  end;
end;


initialization
  // Register this entity class for dynamic loading.
  RegisterClass(TUser);

finalization
  UnRegisterClass(TUser);


end.