Главная » Delphi » Язык определения  интерфейсов (IDL)

0

Язык IDL — это очень  большая  тема. На компакт диске Delphi 6 Enterprise в каталоге Delphi6\Doc\CORBA есть документ в формате PDF, в котором описывается преобразо вание  исходного кода на языке  Object Pascal. В этом  документе подробно рассматрива ются все типы  данных, модули, вопросы наследования и пользовательские типы.  В на стоящем разделе приведены лишь  некоторые наиболее важные аспекты IDL,  а более подробную информацию по этой теме можно найти в упомянутом документе.

Файлы  IDL должны  соответствовать нескольким требованиям. Прежде всего,  они должны  иметь  расширение .idl (регистр  символов при  этом  значения не  имеет). Другие расширения файлов не допускаются.

Содержимое файла  IDL должно  соответствовать определенной структуре.  Описа ние интерфейсов чувствительно к регистру символов. Так, в языках  C++ и Java, имена Foo и foo не являются эквивалентными, но в Delphi эти имена  будут рассматриваться как одно, что неизбежно приведет к конфликту интерфейса.

Для комментариев в файлах IDL используется синтаксис языка C или C++:

// Это комментарий для одной строки.

/* Это пример блочного комментария,

который может занимать

несколько строк. */

Все ключевые слова  языка  IDL пишутся  в нижнем регистре, иначе  они  не  будут восприняты компилятором IDL2Pas. По возможности следует избегать использования ключевых слов Delphi, так как, согласно спецификации преобразования, всем ключе вым словам  Delphi  должен  предшествовать символ  подчеркивания (_). Другими  сло вами, зарезервированные слова Delphi  лучше не использовать.

С помощью директивы #include в файлы IDL можно  подключать другие  файлы

IDL. Это позволяет объединять большие файлы IDL в небольших группах.

Основные типы данных

В языке  IDL для описания интерфейсов используется несколько основных типов данных.   Они  перечислены в  табл. 19.1  с  указанием   соответствующих  типов   языка Object  Pascal.Таблица 19.1. Основные типы данных языка IDL

Тип языка IDL                                                                Тип языка Pascal

boolean                          Boolean Char   Char wchar  Wchar octet Byte

string                           AnsiString wstring   WideString short  SmallInt unsigned short                            Word

long                             Integer unsigned long Cardinal long long      Int64 unsigned long long               Int64 float    Single double     Double long double                                 Extended

fixed                            соответствующего типа не существует

В языке  IDL нет типа int. Вместо него используются целочисленные типы  short, long, unsigned short и  unsigned long. Символьные типы  соответствуют типу ISO Latin-1, эквивалентному таблице ASCII. Единственным исключением является символ  NUL (#0). Программисты C и C++ просили группу OMG не использовать этот символ,  так как он в языке С указывает на конец  строки.

Реализация типа  Boolean зависит от разработчиков. Тип  Boolean IDL в Delphi соответствует типу с тем же названием, а тип Any — типу Variant.

Пользовательские типы данных

В IDL можно  определять собственные типы.  Синтаксис подобен определению структур в языке  С. К общим пользовательским типам  данных  относятся псевдонимы, перечисления, структуры, массивы и последовательности.

Псевдонимы

Псевдонимы используются  для  присвоения типам  данных  более  удобочитаемых имен. Например, тип года может быть описан  следующим образом:

typedef short YearType;

Перечисления

Перечислениям  языка  IDL  соответствует  перечислимый  тип  Delphi.  Перечисле

ние нескольких цветов может выглядеть следующим образом:

enum Color(red, white, blue, green, black);

Структуры

Структуры   IDL  аналогичны записям в  языке   Pascal.  Ниже   представлен пример структуры, предназначенной для хранения значения времени:

struct TimeOfDay { short hour; short minute; short seconds;

};

Массивы

Массивы  бывают  одномерными и многомерными. Они  определяются при помощи ключевого слова typedef. Вот несколько примеров:

typedef Color ColorArray[4];    // одномерный массив типа Color typedef string StringArray[10][20]; // 10 строк по 20 символов

Последовательности

Последовательности в языке  IDL используются очень  часто.  В Delphi им соответ ствуют  массивы  переменной длины.  Последовательности бывают  ограниченными и неограниченными.

typedef sequence<Color> Colors;

typedef sequence<long, 1000> NumSeq;

Первый параметр в объявлении последовательности обозначает базовый тип мас сива переменной длины.  Второй параметр является не обязательным и задает  длину для ограниченной последовательности.

Чаще всего в программировании CORBA последовательности используются для передачи записей баз данных  между серверами и клиентами. После  получения после довательности клиентским приложением из нее  в цикле  извлекаются значения всех полей  записи. Затем  полученная информация используется  элементами управления пользовательского интерфейса. При  этом  средства CORBA могут взаимодействовать со средствами MIDAS.

Параметры методов

Все аргументы методов должны  объявляться с одним  из трех  атрибутов: in, out

или inout.

Значение параметра, объявленного как in, устанавливается клиентом. В Delphi та

кому параметру отвечает параметр типа const.

Значение параметра, объявленного как out, устанавливается сервером. В Delphi

такому параметру соответствует параметр типа var.

Начальное значение параметра, объявленного как inout, устанавливается клиен 

том. Получив данные, сервер изменяет их значение и возвращает клиенту.  Параметру

типа inout в Delphi  соответствует параметр типа var.

Модули

Ключевое слово  module используется для группирования интерфейсов и типов. Имя  модуля  используется компилятором IDL2Pas  для  присвоения имени соответст вующему модулю Delphi. Интерфейсы и типы, определенные внутри  модуля, являют ся для него  локальными. Для обращения к интерфейсу Bar, определенному в модуле по имени  Foo, используется следующий синтаксис: Foo::Bar.

В IDL не поддерживаются закрытые (private) или защищенные (protected) типы  и

методы.  Все интерфейсы и методы  считаются открытыми (public). Это  имеет  значе ние в том случае, когда в файле IDL описаны интерфейсы, предоставляемые сервером внешнему  миру. В этом  случае некоторые методы  или  типы  желательно скрыть или защитить.

Для того  чтобы  понять подходы  программирования на IDL, лучше всего  изучить примеры, написанные другими.  В каталоге VisiBroker  (по  умолчанию  c:\Inprise) есть каталог  IDL, содержащий файлы IDL с различными интерфейсами CORBA, типа ORB и различных служб. Эти файлы являются хорошей отправной точкой для изуче ния, так как в них есть множество примеров объявления и определения интерфейсов, вложенных модулей и ссылок на внешние типы данных.

Наряду  с основными понятиями IDL будут рассмотрены несколько примеров раз работки приложений CORBA, демонстрирующих возможности распределенного объ ектного программирования. Оставшаяся часть главы посвящена вопросам разработки серверов и клиентов CORBA.

Модуль Bank

В  CORBA  есть  традиционный  пример  модуля,  аналогичного  программе  вывода строки “Hello, world” в языке  C. Он называется Bank и содержит простой вызов  ме тода, возвращающего банковский баланс. В нем используются методы  deposit (внести) и withdraw (снять), соответствующие переводу  денег на счет и снятию их со счета.  Не обходимо также реализовать запрет на снятие со счета сумм, превышающих количество наличных денег. Модуль IDL для этого примера представлен в листинге 19.1.

Листинг 19.1. Bank.idl

module Bank {

exception WithdrawError {

float current_balance;

};

interface Account {

void deposit(in float amount);

void withdraw(in float amount) raises (WithdrawError);

float balance();

};

};Исключение объявлено с одним элементом данных.  Если клиент попытается снять со счета  больше  денег,  чем это возможно, возникнет исключение, в элементе данных которого будет указан баланс.  Клиентское приложение может  перехватить такое  ис ключение и отобразить соответствующее сообщение. В данном  случае сообщение бу дет содержать текущий баланс.

Методы deposit и withdraw эквивалентны процедурам, поэтому они возвращают значение типа  void. Каждый  из этих  методов принимает один  параметр: количество денег,  которые клиент вносит на счет или снимает со счета.  Остаток представляет со бой число  с плавающей запятой, которому в Delphi  соответствует тип  single. Обра тите  внимание: параметры в методах  deposit и withdraw объявлены как  in. Это связано тем,  что  методы  передают значения таких  параметров от  клиента серверу. Метод  balance —  это  функция, которая возвращает число  с плавающей запятой, со держащее текущий баланс средств  на счету.

В состав интегрированной среды разработки Delphi 6 входит множество мастеров, значительно облегчающих разработку клиентской и серверной частей приложений CORBA. Итак, начнем с разработки серверной части.  Для вызова  мастера выберите в меню File пункты New и Other, а в появившемся диалоговом окне  перейдите на вклад ку CORBA и дважды  щелкните на пиктограмме CORBA server. В результате появится главное  окно мастера (рис. 19.2).

Рис. 19.2. Мастер CORBA в Delphi 6

Это окно содержит список  всех файлов IDL, которые можно  применить для созда ния  исходного кода приложения. Вначале  данный список  будет пуст. Для того  чтобы добавить имена  файлов, необходимо щелкнуть  на  кнопке Add (Добавить) и  в стан дартном диалоговом окне  Open найти каталог  с файлом Bank.idl, отметить этот файл  и щелкнуть на кнопке OK. В результате файл  Bank.idl будет добавлен в список файлов, обрабатываемых компилятором IDL2Pas. Для данного приложения это един ственный файл  IDL, поэтому  щелкните на кнопке Generate (Создать). В результате бу дет создано  новое  приложение сервера.

Компилятор IDL2Pas обработает указанный файл IDL, и мастер создаст соответст

вующее приложение сервера, состоящее из четырех файлов:

•  Bank_I.pas — этот файл содержит определения всех интерфейсов и типов.•  Bank_C.pas — данный файл содержит все пользовательские типы, исключения и клиентские классы  заглушек.  Кроме  того,  все  эти  пользовательские типы  и классы заглушек обладают вспомогательными классами, предназначенными для организации обмена  данными с буферами CORBA.

•    Bank_S.pas — этот файл содержит определение класса каркаса на стороне сер

вера.

•  Bank_Impl.pas — данный файл  содержит определение общего  класса для реа лизации на  стороне сервера. В методы  данного класса  можно  вносить собст венный программный код. Такой  файл пока что использоваться не будет.

Из этого  списка  файлов очевидно, что  клиентская заглушка,  представленная в ар хитектуре CORBA, находится в файле Bank_C.pas, а серверный каркас — в файле Bank_S.pas. Реализация серверной части находится в файле Bank_Impl.pas.

Определение интерфейса этого приложения представлено в листинге 19.2. В дан

ном случае используется только  один интерфейс по имени Account. Он содержит три метода,  объявленные в файле IDL.

Листинг 19.2. Bank_I.pas

unit Bank_i;

interface uses

CORBA;

type

Account = interface;

Account = interface

[‘{99FCA96D-77B2-4A99-7677-E1E0C32F8C67}’]

procedure deposit (const amount : Single);

procedure withdraw (const amount : Single);

function         balance : Single;

end;

implementation initialization end.

В листинге 19.3 представлено содержимое файла Bank_C.pas. В нем объявлено ис

ключение Overdrawn. Здесь же определен базовый класс исключений UserException.Листинг 19.3. Файл Bank_C.pasunit Bank_c;

interface uses

CORBA, Bank_i;

type

EWithdrawError = class;

TAccountHelper = class;

TAccountStub = class;

EWithdrawError = class(UserException)

private

Fcurrent_balance: Single;

protected

function         _get_current_balance: Single; virtual;

public

property         current_balance: Single read _get_current_balance;

constructor Create; overload;

constructor Create(const current_balance: Single); overload;

procedure Copy(const _Input: InputStream); override;

procedure WriteExceptionInfo(var _Output: OutputStream);

override;

end;

TAccountHelper = class

class procedure Insert (var _A: CORBA.Any; const _Value:

Bank_i.Account);

class function         Extract(var _A: CORBA.Any): Bank_i.Account;

class function         TypeCode: CORBA.TypeCode;

class function         RepositoryId: string;

class function         Read (const _Input: CORBA.InputStream):

Bank_i.Account;

class procedure Write(const _Output: CORBA.OutputStream;

const _Value: Bank_i.Account);

class function         Narrow(const _Obj: CORBA.CORBAObject;

_IsA: Boolean = False): Bank_i.Account;

class function         Bind(const _InstanceName: string = ”;

_HostName: string = ”): Bank_i.Account;

overload;

class function         Bind(_Options: BindOptions;

const _InstanceName: string = ”;

_HostName: string = ”): Bank_i.Account;

overload;

end;

TAccountStub = class(CORBA.TCORBAObject, Bank_i.Account)

public

procedure deposit         ( const amount : Single); virtual;

procedure withdraw ( const amount : Single); virtual;

function         balance: Single; virtual;

end;

implementation var

WithdrawErrorDesc: PExceptionDescription;

function EWithdrawError._get_current_balance: Single;

begin

Result := Fcurrent_balance;

end;

constructor EWithdrawError.Create;

begin

inherited Create;

end;

constructor EWithdrawError.Create(const current_balance: Single);

begin

inherited Create;

Fcurrent_balance := current_balance;

end;

procedure EWithdrawError.Copy(const _Input: InputStream);

begin

_Input.ReadFloat(Fcurrent_balance);

end;

procedure EWithdrawError.WriteExceptionInfo(var _Output: OutputStream);

begin

_Output.WriteString(‘IDL:Bank/WithdrawError:1.0′);

_Output.WriteFloat(Fcurrent_balance);

end;

function     WithdrawError_Factory: PExceptionProxy; cdecl;

begin

with Bank_c.EWithdrawError.Create() do Result := Proxy;

end;

class procedure TAccountHelper.Insert(var _A: CORBA.Any;

const _Value: Bank_i.Account);

begin

_A := Orb.MakeObjectRef( TAccountHelper.TypeCode,

_Value as CORBA.CORBAObject);

end;

class function TAccountHelper.Extract(var _A:

CORBA.Any): Bank_i.Account;

var

_obj: Corba.CorbaObject;

begin

_obj := Orb.GetObjectRef(_A);Result := TAccountHelper.Narrow(_obj, True);

end;

class function TAccountHelper.TypeCode: CORBA.TypeCode;

begin

Result := ORB.CreateInterfaceTC(RepositoryId, ‘Account’);

end;

class function TAccountHelper.RepositoryId: string;

begin

Result := ‘IDL:Bank/Account:1.0′;

end;

class function TAccountHelper.Read(const _Input: CORBA.InputStream): Bank_i.Account;

var

_Obj: CORBA.CORBAObject;

begin

_Input.ReadObject(_Obj);

Result := Narrow(_Obj, True)

end;

class procedure TAccountHelper.Write(const _Output: CORBA.OutputStream; const _Value: Bank_i.Account);

begin

_Output.WriteObject(_Value as CORBA.CORBAObject);

end;

class function TAccountHelper.Narrow(const _Obj: CORBA.CORBAObject; _IsA: Boolean): Bank_i.Account;

begin

Result := nil;

if (_Obj = nil) or

(_Obj.QueryInterface(Bank_i.Account, Result) = 0) then exit;

if _IsA and _Obj._IsA(RepositoryId) then

Result := TAccountStub.Create(_Obj);

end;

class function TAccountHelper.Bind(const _InstanceName:

string = ”; _HostName: string = ”): Bank_i.Account;

begin

Result := Narrow(ORB.bind(RepositoryId, _InstanceName,

_HostName), True);

end;

class function TAccountHelper.Bind(_Options: BindOptions; const _InstanceName: string = ”; HostName: string = ”): Bank_i.Account;

begin

Result := Narrow(ORB.bind(RepositoryId, _Options, _InstanceName,

_HostName), True);

end;procedure TAccountStub.deposit ( const amount: Single);

var

_Output: CORBA.OutputStream;

_Input:       CORBA.InputStream;

begin

inherited _CreateRequest(‘deposit’, True, _Output);

_Output.WriteFloat(amount);

inherited _Invoke(_Output, _Input);

end;

procedure TAccountStub.withdraw ( const amount : Single);

var

_Output: CORBA.OutputStream;

_Input:       CORBA.InputStream;

begin

inherited _CreateRequest(‘withdraw’, True, _Output);

_Output.WriteFloat(amount);

inherited _Invoke(_Output, _Input);

end;

function     TAccountStub.balance: Single;

var

_Output: CORBA.OutputStream;

_Input:       CORBA.InputStream;

begin

inherited _CreateRequest(‘balance’, True, _Output);

inherited _Invoke(_Output, _Input);

_Input.ReadFloat(Result);

end;

initialization

Bank_c.WithdrawErrorDesc := RegisterUserException(‘WithdrawError’,

‘IDL:Bank/WithdrawError:1.0′,

@Bank_c.WithdrawError_Factory);

finalization

UnRegisterUserException(Bank_c.WithdrawErrorDesc);

end.В листинге 19.4  представлено определение класса  реализации Account. Данный класс привязан к CORBA, поэтому  может  быть  использован и в других приложениях или интерфейсах. Его методы  объявлены в файле Bank.idl. Для реализации полно функционального сервера в методы  класса  TAccount был  внесен дополнительный программный код.

Листинг 19.4. Класс реализации для сервера Bank

unit Bank_impl;interface

uses

SysUtils, CORBA, Bank_i, Bank_c;

type

TAccount = class;

TAccount = class(TInterfacedObject, Bank_i.Account)

protected

_balance: Single;

public

constructor Create;

procedure deposit         (const amount: Single);

procedure withdraw (const amount: Single);

function         balance: Single;

end;

implementation

constructor TAccount.Create;

begin

inherited;

_balance := random(10000);

end;

procedure TAccount.deposit(const amount: Single);

begin

if amount > 0 then

_balance := _balance + amount;

end;

procedure TAccount.withdraw(const amount: Single);

begin

if amount < _balance then

_balance := _balance – amount

else

raise EWithdrawError.Create(_balance);

end;

function     TAccount.balance: Single;

begin

result := _balance;

end;

initialization randomize;

end.Класс TAccount является производным от класса  TInterfacedObject, а значит, подсчет ссылок  он  выполняет автоматически. Реализация интерфейса  Account со держится в файле Bank_I.pas. В методе  deposit выполняется простая проверка, исключающая передачу отрицательного числа. В методе withdraw выполняется про верка  суммы денег,  снимаемой клиентом со счета.  Если эта сумма превышает размер остатка на счету,  то будет передано исключение с указанием  текущего  остатка. Кли ентская часть приложения может  обработать это исключение и отобразить пользова телю соответствующую информацию. Метод balance возвращает текущий баланс.

В листинге 19.5  представлен класс  заглушки,  который используется  клиентским

приложением  в  качестве  посредника.  Подобно  каркасу  сервера,  класс  заглушки  со

держит три метода,  определенные для интерфейса Account в файле IDL.

Листинг 19.5. Класс заглушки на стороне клиента

TAccountStub = class(CORBA.TCORBAObject, Bank_i.Account)

public

procedure deposit         ( const amount: Single); virtual;

procedure withdraw ( const amount: Single); virtual;

function         balance: Single; virtual;

end;В листинге 19.6  метод  deposit() представлен полностью. Два  буфера  потоков CORBA объявлены как локальные переменные. Метод CreateRequest() обращается к брокеру  ORB,  который определяет корректный выходной буфер  и заносит в него информацию. Заглушка  передает имя  метода,  который будет вызываться на стороне сервера, а также  определяет, требуется ли ожидание завершения выполнения задачи на сервере. В зависимости от этого вызов будет односторонним или двусторонним.

Листинг 19.6. Метод deposit класса заглушки

procedure TAccountStub.deposit(const amount: Single);

var

_Output: CORBA.OutputStream;

_Input:       CORBA.InputStream;

begin

inherited _CreateRequest(‘deposit’, True, Output);

_Output.WriteFloat(amount);

inherited _Invoke(Output, Input);

end;Теперь данные, предназначенные для передачи на сервер, необходимо занести в выходной буфер. В данном случае в буфер заносится сумма, вносимая на счет. В конце вызывается метод Invoke. Это — еще одно обращение к брокеру  ORB, после которого запрос и содержимое выходного буфера  передается на сервер. Выполнение приложе ния  на стороне клиента продолжается только  после  того,  как сервер завершит обра ботку  запроса. При  этом  в случае  вызова  метода,  представляющего собой  функцию (например метод balance), возвращаемый результат будет занесен во входной буфер. Программный код для считывания значений из входного буфера  создается компиля тором IDL2Pas. В данном  примере вызываемый метод представляет собой  процедуру, поэтому никакого результата не возвращается.Весь  код заглушки  создается компилятором IDL2Pas  автоматически, и вносить в него  какие либо  изменения самостоятельно не  требуется.  Однако важно  понимать, что же происходит в этом программном коде.

Завершающая часть  кода  приложения имеет  отношение к графическому интер фейсу клиентской части.  Интерфейс клиента состоит из трех  кнопок, двух полей  вво да текста  и одной  метки  (рис. 19.3).  Все переменные интерфейса CORBA объявлены как  типы  интерфейса. В данном  случае  интерфейс Account объявлен как  тип  Ac- count. При  таком  подходе  переменные инициализируются на основании определен ного  в файле Bank_i.pas типа,  который содержит три  метода  (определены в файле Bank.idl). Еще одно преимущество использования переменных типа  интерфейса за ключается в автоматическом подсчете ссылок, который должен  выполняться для всех объектов CORBA. Программный код для этого  автоматически создается компилято ром IDL2Pas.

   

Рис. 19.3. Клиентское приложение CORBA

Наиболее интересной частью  приложения является обработчик события btn- Withdraw.OnClick. Исходный код  клиентской части  представлен в листинге 19.7. В методе  Withdraw() сопоставляется сумма, снимаемая клиентом, и остаток на счету. Если снимаемая сумма превышает остаток, то возникает исключение. Обратите вни мание:  передача исключений в CORBA идентична передаче исключений в Delphi. Ис ключение Delphi  преобразуется в исключение CORBA автоматически.

Листинг 19.7. Исходный код клиентской части приложения

unit ClientMain;

interface uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,

Dialogs, Corba, Bank_c, Bank_i, StdCtrls;

type

TForm1 = class(TForm)

btnDeposit: TButton;

btnWithdraw: TButton;

btnBalance: TButton;

Edit1: TEdit;

Edit2: TEdit;

Label1: TLabel;

procedure btnDepositClick(Sender: TObject);

procedure btnWithdrawClick(Sender: TObject);

procedure btnBalanceClick(Sender: TObject);

procedure FormCreate(Sender: TObject);private

{ Закрытые объявления }

protected

Acct: Account;

procedure InitCorba;

{ Защищенные объявления }

public

{ Открытые объявления }

end;

var

Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.InitCorba;

begin

CorbaInitialize;

// Привязка к серверу Corba

Acct := TAccountHelper.bind;

end;

procedure TForm1.btnDepositClick(Sender: TObject);

begin

Acct.deposit(StrToFloat(Edit1.text));

end;

procedure TForm1.btnWithdrawClick(Sender: TObject);

begin

try

Acct.withdraw(StrToFloat(Edit2.Text));

except

on e: EWithdrawError do

ShowMessage(‘Withdraw Error. The balance = ‘ +

FormatFloat(‘$##,##0.00′, E.current_balance));

end;

end;

procedure TForm1.btnBalanceClick(Sender: TObject);

begin

label1.caption := FormatFloat(‘Balance = $##,##0.00′,

acct.balance);

end;

procedure TForm1.FormCreate(Sender: TObject);

begin

InitCorba;

end;

end.

После  компиляции  клиентской  и  серверной  частей  приложения  необходимо  за

пустить программу OSAgent.  В системах Windows NT, OSAgent VisiBroker  может  бытьустановлен как служба. В других операционных системах он запускается вручную. Для ручного  запуска OSAgent  на любой  платформе MS Windows  выберите в меню кнопки Start пункт  Run и наберите команду  OSAgent –C, которая запустит  OSAgent  в кон сольном режиме. Кроме того, пиктограмма данного агента  появится на панели  задач.

Затем   запускается серверное  приложение,  и  только   после  этого —   клиентское. Графический интерфейс  клиентского приложения представлен на  рис. 19.3.  Он  со стоит  из трех кнопок, двух полей  ввода текста  и метки, отображающей баланс. Чтобы получить с сервера исходное значение баланса, щелкните на кнопке Balance. Затем внесите на счет  определенную сумму и щелкните на кнопке Balance еще  раз,  чтобы обновить значение на стороне клиента. При  вызове  методов deposit и withdraw значение баланса  также  автоматически обновится на стороне клиента. Теперь попы тайтесь снять со счета сумму, превышающую остаток. В результате на экране появится сообщение об ошибке.

Источник: Тейксейра, Стив, Пачеко, Ксавье.   Borland Delphi 6. Руководство разработчика. : Пер.  с англ. — М. : Издательский дом “Вильямс”, 2002. —  1120 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии