Title: SHAMPOO - the better SOAP (Made in Delphi)
Question: The Delphi approach to distributed computing.
Answer:
SHAMPOO - the better SOAP (Made in Delphi)
Recently a new buzz word came around - SOAP - which stands for Simple Object Access Protocol. While the idea of SOAP is nice - making a platform-independend RPC mechanism over XML - I think it is more like "teaching a new dog old tricks". The problem with SOAP is that it is based on an old paradigm - calling functions in an interface implemented by a distributed component. XML is good for representing data structures in a uniform way, but SOAP tries to make XML encode interfaces while it is far better to just use it for encoding data.
SHAMPOO stands for "Simple Hyper Access Meta Protocol - Object Oriented". And OO is what makes the difference. Instead of trying to mimic function calls like SOAP, SHAMPOO's aim is to transfer a component from one place to another - either for in-process or out-of-process communication. Instead of calling functions in a remote object you simply transfer a component to the remote location for processing. The type of processing of course depends of the type of component being sent. As a result of the processing a component is produced which is send back and processed by the calling party. So the only thing both parties need to know about the communication between them is the type of components being exchanged. The actual transfer of the components is acomplished thru component streaming - saving the properties of the component to a stream (either binary or text) and then reading the component back from the stream. Delphi uses this mechanism since version 1 for its form files (.dfm) and in fact everything in Delphi is based on persistent objects and RTTI (run-time type information). With the standarization of XML as a platform-independent text format for data exchange it is very easy to extend the streaming of Delphi components to XML. Now that Borland released Kylix (Delphi for Linux) making programs which work seamlessly on Windows and/or Linux is a trivial task. (Personaly, I think the Delphi text format for representing components is far better than XML - smaller, cleaner and easy to read).
So how about a little example in SHAMPOO. Here is the "Hello, World" program ;).
unit ShampooDemoMainFormUnit;
interface
uses SysUtils, Classes, Controls, Forms, StdCtrls;
type TShampooDemoMainForm = class (TForm)
PersonNameEdit: TEdit;
SayHelloButton: TButton;
RequestMemo: TMemo;
ResponseMemo: TMemo;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
procedure SayHelloButtonClick(Sender: TObject);
end;
TPerson = class (TPersistent)
protected
FPersonName : String;
public
procedure Assign (aPerson : TPersistent); override;
published
property PersonName : String read FPersonName write FPersonName;
end;
TBaseSHAMPOOComponent = class (TComponent);
THelloRequest = class (TBaseSHAMPOOComponent)
protected
FPerson : TPerson;
procedure SetPerson (const aPerson : TPerson);
public
constructor Create (aOwner : TComponent); override;
destructor Destroy; override;
published
property Person : TPerson read FPerson write SetPerson;
end;
TGreetingResponse = class (TBaseSHAMPOOComponent)
protected
FGreetingText : String;
published
property GreetingText : String read FGreetingText write FGreetingText;
end;
EBaseSHAMPOOException = class (TBaseSHAMPOOComponent);
EGreetingError = class (EBaseSHAMPOOException)
protected
FText : String;
public
constructor Create (const aText : String); reintroduce;
published
property Text : String read FText write FText;
end;
var ShampooDemoMainForm : TShampooDemoMainForm;
implementation
{$R *.DFM}
uses Dialogs;
function ComponentToString (const aComponent : TComponent) : String;
var aBinStream : TMemoryStream;
aStrStream : TStringStream;
begin
aBinStream := TMemoryStream.Create;
try
aStrStream := TStringStream.Create ('');
try
aBinStream.WriteComponent (aComponent);
aBinStream.Position := 0;
ObjectBinaryToText (aBinStream, aStrStream);
aStrStream.Position := 0;
Result:= aStrStream.DataString;
finally
aStrStream.Free;
end;
finally
aBinStream.Free
end;
end;
function StringToComponent (const aString : String) : TComponent;
var aStrStream : TStringStream;
aBinStream : TMemoryStream;
begin
aStrStream := TStringStream.Create (aString);
try
aBinStream := TMemoryStream.Create;
try
ObjectTextToBinary (aStrStream, aBinStream);
aBinStream.Position := 0;
Result := aBinStream.ReadComponent (Nil);
finally
aBinStream.Free;
end;
finally
aStrStream.Free;
end;
end;
function Process (const aComponent : TComponent) : TComponent;
begin
Result := Nil;
try
if aComponent is THelloRequest then
begin
with aComponent as THelloRequest do
begin
if Person.PersonName = ''
then raise EGreetingError.Create ('I do not talk to strangers');
Result := TGreetingResponse.Create (Nil);
with Result as TGreetingResponse do
begin
GreetingText := 'Hello, ' + Person.PersonName + ' !';
end;
end;
end;
except
if Assigned (Result)
then FreeAndNil (Result);
raise;
end;
end;
function RemoteServerProcess (const aString : String) : String;
var aRequestComponent : TComponent;
aResponseComponent : TComponent;
begin
aRequestComponent := StringToComponent (aString);
try
aResponseComponent := Process (aRequestComponent);
try
Result := ComponentToString (aResponseComponent);
finally
aResponseComponent.Free;
end;
finally
aRequestComponent.Free;
end;
end;
function CallRemoteProcess (const aComponent : TComponent) : TComponent;
var aRequestString : String;
aResponseString : String;
begin
aRequestString := ComponentToString (aComponent);
ShampooDemoMainForm.RequestMemo.Lines.Text := aRequestString; // for the example only
aResponseString := RemoteServerProcess (aRequestString);
ShampooDemoMainForm.ResponseMemo.Lines.Text := aResponseString; // for the example only
Result := StringToComponent (aResponseString);
end;
constructor EGreetingError.Create (const aText : String);
begin
inherited Create (Nil);
FText := aText;
end;
procedure TPerson.Assign (aPerson : TPersistent);
begin
if aPerson is TPerson then
begin
with aPerson as TPerson do
begin
Self.PersonName := PersonName;
end;
end
else inherited;
end;
constructor THelloRequest.Create (aOwner : TComponent);
begin
inherited;
FPerson := TPerson.Create;
end;
destructor THelloRequest.Destroy;
begin
FPerson.Free;
inherited;
end;
procedure THelloRequest.SetPerson (const aPerson : TPerson);
begin
FPerson.Assign (aPerson);
end;
procedure TShampooDemoMainForm.SayHelloButtonClick(Sender: TObject);
var aHelloRequest : THelloRequest;
begin
aHelloRequest := THelloRequest.Create (Nil);
try
aHelloRequest.Person.PersonName := PersonNameEdit.Text;
with CallRemoteProcess (aHelloRequest) as TGreetingResponse do
try
ShowMessage (GreetingText);
finally
Free;
end;
finally
aHelloRequest.Free;
end;
end;
initialization
RegisterClasses ([THelloRequest, TGreetingResponse, EGreetingError]);
end.
What I haven't showed is how to stream the exceptions but this is a trivial task.
So, in order to make SHAMPOO cross-platfrom and language independent you can easily substitute the "ComponentToString" and "StringToComponent" functions (which I ripped off from the Delphi Help) with "ComponentToXML" and "XMLToComponent" which you can find on the Borland CodeCentral site (http://community.borland.com) or (www.rapware.com). You can use any transport protocol to transffer the text - HTTP, CORBA, Sockets, SMTP.
Any opinions and suggestions are highly appreciated. (rossen_assenov@yahoo.com)