2005年08月13日

自己記述型データを名前でアクセスする方法

あなたがクライアント/サーバー型の
アプリケーションをつくる場合、
アプリケーション間で送受信するメッセージの
データ構造はどのように設計し実装していますか?
そんな迷えるあなたに、
自己記述型のデータ構造(Self Describe Data)!
ちょいとかじってみますか。

ひと昔前のプログラムが扱うメッセージのデータ構造は、
データだけを順に連ねたとてもシンプルな構造でした。
このメッセージを送受信するプログラムは、
データ構造を解釈するために、
データ構造をあらわす構造体をデータに被せ、
各項目にアクセスしていました。

このデータ構造はデータ自身から、
その構造を求めることが出来ず、
必ずデータ構造をあらわす入れ物が必要になると同時に、
各項目の並びや長さが厳密に規定されているため、
データ構造の変更に柔軟に対応できませんでした。

最近では、SOAPのXMLやSEMIのSECSといった
データ構造に代表される通り、
データがデータの中身である値と、その構造をあらわす
自己記述型のデータ構造が主流となっています。
今回はこの自己記述型のデータ構造を
実現するサンプルプログラムをDelphiでご紹介します。

サンプルプログラムの作成に当り、
2つのキーとなるクラスを作成しています。
「TDatumNode」はデータ構造内の
1項目(要素)をあらわすオブジェクト、
「TContainerNode」は
「TDatumNode」のオブジェクトや、
「TDatumNode」の派生である
「TContainerNode」のオブジェクトを
複数格納するコンテナオブジェクトとして機能します。
丁度、OSファイルシステムの
「ファイル」と「ディレクトリィ」の概念に相当します。
「ファイル」が「TDatumNode」に、
「ファイル」や「ディレクトリィ」を格納する
「ディレクトリィ」が「TContainerNode」に、
それぞれ対応するイメージだと思ってください。

それでは、お疲れ様です。
じっくりソースを解析し、色々と試して下さい。

全ソースは【続きを読む】をクリック!

尚、サンプルプログラムは
「TContainerNode」オブジェクトのデータを
ストリームに保存したり、読込んだりする
メソッドは実装しておりません。
データ構造内の1項目(要素)をあらわす
「TDatumNode」オブジェクトに、
格納できるデータは便宜上文字型のみとしています。
必要あらば、あなた自身で追加してください。(笑い)

【クラスダイアグラム】

DatumClass.gif

【画面イメージ】

DatumClassTester.gif

関連書籍


Copyright guy@かしらもんじ でぇ〜

注意:
 下記ソースファイルは、本ページの管理者である「guy」が個人的に作成しました。
 このソースは作者に断り無く、個人がコピー、改造することは許可しますが、
 いかなる場合であっても、商用目的に使用することを固く禁じます。
 あと、ホームページでの公開の都合上、各ソースの先頭に全角スペースが入ってます。
 あなたがDelphiでコンパイルする前に、この全角スペースを半角スペースに変換してくださいね。

Project File: DatumClassTester.dpr
program DatumClassTester;

uses
 Forms,
 DatumClassTesterUnit in 'DatumClassTesterUnit.pas' {Form1},
 DatumClass in 'DatumClass.pas';

{$R *.res}

begin
 Application.Initialize;
 Application.CreateForm(TForm1, Form1);
 Application.Run;
end.


Form File: DatumClassTesterUnit.dfm
object Form1: TForm1
 Left = 192
 Top = 107
 Width = 352
 Height = 194
 Caption = 'DatumClass Tester'
 Color = clBtnFace
 Font.Charset = SHIFTJIS_CHARSET
 Font.Color = clWindowText
 Font.Height = -12
 Font.Name = #65325#65331' '#65328#12468#12471#12483#12463
 Font.Style = []
 OldCreateOrder = False
 OnCreate = FormCreate
 OnDestroy = FormDestroy
 PixelsPerInch = 96
 TextHeight = 12
 object lblName: TLabel
  Left = 4
  Top = 126
  Width = 36
  Height = 12
  Alignment = taRightJustify
  AutoSize = False
  Caption = 'Name:'
 end
 object lblValue: TLabel
  Left = 4
  Top = 148
  Width = 36
  Height = 12
  Alignment = taRightJustify
  AutoSize = False
  Caption = 'Value:'
 end
 object edtName: TEdit
  Left = 42
  Top = 122
  Width = 140
  Height = 20
  TabOrder = 1
 end
 object edtValue: TEdit
  Left = 42
  Top = 144
  Width = 140
  Height = 20
  TabOrder = 2
 end
 object btnSetContainer: TButton
  Left = 264
  Top = 122
  Width = 75
  Height = 20
  Caption = 'SetContainer'
  TabOrder = 5
  OnClick = btnSetContainerClick
 end
 object btnSetString: TButton
  Left = 186
  Top = 122
  Width = 75
  Height = 20
  Caption = 'SetString'
  TabOrder = 3
  OnClick = btnSetStringClick
 end
 object btnGetString: TButton
  Left = 186
  Top = 144
  Width = 75
  Height = 20
  Caption = 'GetString'
  TabOrder = 4
  OnClick = btnGetStringClick
 end
 object tvContainer: TTreeView
  Left = 2
  Top = 2
  Width = 337
  Height = 116
  Indent = 19
  TabOrder = 0
 end
 object btnRemove: TButton
  Left = 264
  Top = 144
  Width = 75
  Height = 20
  Caption = 'Remove'
  TabOrder = 6
  OnClick = btnRemoveClick
 end
end


Source File: DatumClassTesterUnit.pas
unit DatumClassTesterUnit;

interface

uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, StdCtrls, ComCtrls, DatumClass;

type
 TForm1 = class(TForm)
  lblName: TLabel;
  lblValue: TLabel;
  tvContainer: TTreeView;
  edtName: TEdit;
  edtValue: TEdit;
  btnSetString: TButton;
  btnGetString: TButton;
  btnSetContainer: TButton;
  btnRemove: TButton;
  procedure FormCreate(Sender: TObject);
  procedure FormDestroy(Sender: TObject);
  procedure btnSetContainerClick(Sender: TObject);
  procedure btnSetStringClick(Sender: TObject);
  procedure btnGetStringClick(Sender: TObject);
  procedure btnRemoveClick(Sender: TObject);
 private
  mNode: TContainerNode;
  procedure DrawTreeView;
 public
 end;

var
 Form1: TForm1;

implementation

{$R *.dfm}

//------------------------------------------------------------------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
 mNode := TContainerNode.Create('\');
 DrawTreeView;
end;

//------------------------------------------------------------------------------
procedure TForm1.FormDestroy(Sender: TObject);
begin
 if (mNode <> Nil) then
 begin
  mNode.Free;
  mNode := Nil;
 end;
end;

//------------------------------------------------------------------------------
procedure TForm1.btnSetStringClick(Sender: TObject);
begin
 mNode.SetString(edtName.Text, edtValue.Text);
 DrawTreeView;
end;

//------------------------------------------------------------------------------
procedure TForm1.btnGetStringClick(Sender: TObject);
begin
 edtValue.Text := mNode.GetString(edtName.Text);
end;

//------------------------------------------------------------------------------
procedure TForm1.btnSetContainerClick(Sender: TObject);
var
 lNode: TContainerNode;
begin
 lNode := TContainerNode.Create(edtName.Text);
 mNode.SetContainer(edtName.Text, lNode);
 DrawTreeView;
end;

//------------------------------------------------------------------------------
procedure TForm1.btnRemoveClick(Sender: TObject);
begin
 mNode.Remove(edtName.Text);
 DrawTreeView;
end;

//------------------------------------------------------------------------------
procedure TForm1.DrawTreeView;
 procedure _Doc2View(pNode1: TDatumNode; pNode2: TTreeNode);
 var
  lCnt: Integer;
  lTxt: String;
  lTree: TTreeNode;
 begin
  lTxt := pNode1.Name;
  if (pNode1.Value <> '') then
   lTxt := lTxt + Format('(%s)', [pNode1.Value]);
  lTree := tvContainer.Items.AddChild(pNode2, lTxt);
  if (pNode1 is TContainerNode) then
  begin
   for lCnt := 0 to TContainerNode(pNode1).Contents.Count-1 do
    _Doc2View(
     TDatumNode(TContainerNode(pNode1).Contents.Objects[lCnt]), lTree);
  end;
 end;
begin
 tvContainer.Items.Clear;
 _Doc2View(mNode, Nil);
 tvContainer.FullExpand;
end;

end.


Source File: DatumClass.pas
unit DatumClass;

interface

uses
 Classes;

type
 //----------------------------------------------------------------------------
 TDatumNode = class
 protected
  mName,
  mValue:  String;
  mPrevNode: TDatumNode;
 public
  constructor Create; overload; virtual;
  constructor Create(pName, pValue: String); overload; virtual;
  property Name: String
   read mName write mName;
  property Value: String
   read mValue write mValue;
 end;
 //----------------------------------------------------------------------------
 TContainerNode = class(TDatumNode)
 protected
  mContents: TStringList;
  function GetDatumNode(pNode: TContainerNode; pName: String): TDatumNode;
  function GetDatumNodeByName(pName: String): TDatumNode;
  procedure SetDatumNodeByName(
   pMode: Integer; pName: String; pValue: TDatumNode);
 public
  constructor Create; overload; override;
  constructor Create(pName: String); overload; virtual;
  destructor Destroy; override;
  function GetContainer(pName: String): TContainerNode;
  function GetString(pName: String): String;
  procedure SetContainer(pName: String; pValue: TContainerNode);
  procedure SetString(pName, pValue: String);
  procedure Remove(pName: String);
  property Contents: TStringList
   read mContents;
 end;

implementation

//------------------------------------------------------------------------------
function _GetToken(var pSource: String; pDelimiter: String): String;
 //----------------------------------------------------------------------------
 function IsDelimiter(pSource, pDelimiter: String): Boolean;
 var
  lCnt: Integer;
 begin
  Result := False;
  for lCnt := 0 to Length(pDelimiter) do
  begin
   if (pSource = Copy(pDelimiter, lCnt, 1)) then
   begin
    Result := True;
    Break;
   end;
  end;
 end;
var
 lCnt,
 lPos: Integer;
 lStr,
 lChr: String;
 lFlg: Boolean;
begin
 Result := '';
 lStr := pSource + Copy(pDelimiter, 1, 1);
 lFlg := False;
 for lCnt := 1 to Length(lStr) do
 begin
  lChr := Copy(lStr, lCnt, 1);
  // 区切り文字を検索し文字開始位置を記憶している場合、
  // 区切り文字までの文字列トークンを関数の戻り値に設定し、
  // 入力文字列を検索した区切り文字移行の文字列に調整する
  if (IsDelimiter(lChr, pDelimiter)) then
  begin
   if (lFlg) then
   begin
    Result := Copy(lStr, lPos, lCnt-lPos);
    pSource := Copy(pSource, lCnt+1, Length(pSource));
    Exit;
   end;
  end
  else
  // 区切り文字でない場合、文字開始位置を記憶する
  begin
   if (not lFlg) then
   begin
    lPos := lCnt;
    lFlg := True;
   end;
  end;
 end;
 pSource := Copy(pSource, lCnt+1, Length(pSource));
end;

//------------------------------------------------------------------------------
procedure _ExtractName(pName: String; var pPrefix, pSuffix: String);
var
 lFlg: Boolean;
 lCnt: Integer;
begin
 // pName文字列の末尾から"."を検索し、
 // "."の前半をpPrefixへ、後半をpSuffixへ格納する
 lFlg := False;
 for lCnt := Length(pName) downto 1 do
 begin
  if (Copy(pName, lCnt, 1) = '.') then
  begin
   lFlg := True;
   pPrefix := Copy(pName, 1, lCnt-1);
   pSuffix := Copy(pName, lCnt+1, Length(pName));
   Break;
  end;
 end;
 // "."が見つからない場合、
 // pPrefixにNullを、pSuffixにpNameを格納する
 if (not lFlg) then
 begin
  pPrefix := '';
  pSuffix := pName;
 end;
end;

//------------------------------------------------------------------------------
constructor TDatumNode.Create;
begin
 mName := '';
 mValue := '';
 mPrevNode := Nil;
end;

//------------------------------------------------------------------------------
constructor TDatumNode.Create(pName, pValue: String);
begin
 mName := pName;
 mValue := pValue;
 mPrevNode := Nil;
end;

//----------------------------------------------------------------------------
function TContainerNode.GetDatumNode(
 pNode: TContainerNode; pName: String): TDatumNode;
var
 lIdx: Integer;
 lName: String;
begin
 // コンテナノードpNodeからpNameで指定された名前のデータムノードを検索する
 Result := Nil;
 lName := _GetToken(pName, '.');
 if (lName <> '') then
 begin
  lIdx := pNode.mContents.IndexOf(lName);
  if (lIdx >= 0) then
  begin
   if (pName = '') then
    Result := TDatumNode(pNode.mContents.Objects[lIdx])
   else
   if (pNode.mContents.Objects[lIdx] is TContainerNode) then
    Result := GetDatumNode(
     TContainerNode(pNode.mContents.Objects[lIdx]), pName);
  end;
 end;
end;

//------------------------------------------------------------------------------
function TContainerNode.GetDatumNodeByName(pName: String): TDatumNode;
begin
 Result := GetDatumNode(Self, pName);
end;

//------------------------------------------------------------------------------
procedure TContainerNode.SetDatumNodeByName(
 pMode: Integer; pName: String; pValue: TDatumNode);
var
 lIdx:  Integer;
 lPrefix,
 lSuffix: String;
 lNode:  TDatumNode;
begin
 // pNameで指定された名前のデータムノードを追加/更新/削除する
 _ExtractName(pName, lPrefix, lSuffix);
 if (lPrefix = '') then
  lNode := Self
 else
  lNode := GetDatumNode(Self, lPrefix);
 if (lNode <> Nil) and (lNode is TContainerNode) then
 begin
  lIdx := TContainerNode(lNode).mContents.IndexOf(lSuffix);
  case pMode of
  0: // 削除モード
   begin
    if (lIdx >= 0) then
    begin
     TDatumNode(TContainerNode(lNode).mContents.Objects[lIdx]).Free;
     TContainerNode(lNode).mContents.Delete(lIdx);
    end;
   end;
  1: // 更新/追加モード
   begin
    if (lIdx >= 0) then
    begin
     TDatumNode(TContainerNode(lNode).mContents.Objects[lIdx]).Free;
     TContainerNode(lNode).mContents.Objects[lIdx] := pValue;
    end
    else
     TContainerNode(lNode).mContents.AddObject(lSuffix, pValue);
    pValue.mName := lSuffix;
    pValue.mPrevNode := lNode;
   end;
  end;
 end;
end;

//------------------------------------------------------------------------------
constructor TContainerNode.Create;
begin
 inherited;
 mContents := TStringList.Create;
end;

//------------------------------------------------------------------------------
constructor TContainerNode.Create(pName: String);
begin
 inherited;
 mName := pName;
 mContents := TStringList.Create;
end;

//------------------------------------------------------------------------------
destructor TContainerNode.Destroy;
begin
 // コンテナノード/データムノードを廃棄する
 if (mContents <> Nil) then
 begin
  while (mContents.Count > 0) do
  begin
   if (mContents.Objects[0] <> Nil) then
    TDatumNode(mContents.Objects[0]).Free;
   mContents.Delete(0);
  end;
  mContents.Free;
  mContents := Nil;
 end;
 inherited;
end;

//------------------------------------------------------------------------------
function TContainerNode.GetContainer(pName: String): TContainerNode;
var
 lNode: TDatumNode;
begin
 // pNameで指定された名前のコンテナノードを取得する
 Result := Nil;
 lNode := GetDatumNodeByName(pName);
 if (lNode <> Nil) and (lNode is TContainerNode) then
  Result := TContainerNode(lNode);
end;

//------------------------------------------------------------------------------
function TContainerNode.GetString(pName: String): String;
var
 lNode: TDatumNode;
begin
 // pNameで指定された名前のデータムノードを検索し、文字型データを戻す
 Result := '';
 lNode := GetDatumNodeByName(pName);
 if (lNode <> Nil) then
  Result := lNode.mValue;
end;

//------------------------------------------------------------------------------
procedure TContainerNode.SetContainer(pName: String; pValue: TContainerNode);
begin
 // pNameで指定された名前のコンテナノードpValueを追加/更新する
 SetDatumNodeByName(1, pName, pValue);
end;

//------------------------------------------------------------------------------
procedure TContainerNode.SetString(pName, pValue: String);
var
 lNode: TDatumNode;
begin
 // pNameで指定された名前と文字データpValueより、
 // データムノードを作成し、コンテナノードへ追加/更新する
 lNode := TDatumNode.Create('', pValue);
 SetDatumNodeByName(1, pName, lNode);
end;

//------------------------------------------------------------------------------
procedure TContainerNode.Remove(pName: String);
begin
 // pNameで指定された名前のデータムノードを削除する
 SetDatumNodeByName(0, pName, Nil);
end;

end.

posted by guy at 07:22 | 通信編
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。