DelphiでWeb API呼び出しを抽象化しテスト可能なアーキテクチャにしてみる

# Delphiでもテストコード書きたい
API呼び出しのニーズは増える中、DelphiをApiClientとし、テスト可能なアーキテクチャにしたいニーズはあるはず。世の中的にDDDやClean Architectureにしたいという機運がある中、Delphiを使ってのサンプルコードは少なく、なんか取り残されている。そこで、ちょっとでもノウハウを共有できたらなと。
なお、次のエントリでは「DUnitX」と「Delphi Mocks」を使いテストコードの実装方法も紹介できたらと考えてます。

# 前提
テスト可能なアーキテクチャの書き方を何となく理解してるのが前提です。

# 書くコードとアーキテクチャ論
今回はWebAPIのデータ取得まわりを抽象化しテスト可能としていきます。コードをがっつり書くとキリないんで、掻い摘んで説明できたらなと。WebAPIを使って実装したApplication Service（UseCase）のテストコードを書いてく感じです。

WEBアプリケーションとクライアントアプリケーションを連携する場合、WEB APIを経由 or WebViewで画面を表示する方法が考えられます。WEB APIを呼び出すとき薄いラッパー用のDLLを作成することでモジュール化でき、複数サービス間で共有できます。

関数の呼び出し規約は「stdcall」とし開発言語特有の型は使用しないことで、開発言語やプラットフォームに依らない呼び出しが可能になります。昔のC#(.NET Framework)でDLL作ると、他の言語で作ったアプリから呼び出せません。たとえば、Delphiで作ったアプリ(EXE)はC#のDLLを呼べません(´;ω;｀)

DLLを作るにあたり、Delphiって結構使える言語と思ったり思わなかったり(ただし、.NETもAOT対応したことで今後はそちらも選択肢に入ってくる？)。

## 全体設計
- RESTな操作を抽象化するクラスを実装
- ↑を使って各APIに特化したApiClientを実装
- ↑で作ったApiClientを使ってApplication Service（UseCase）を実装

アクセストークンを取得後、そのトークンで設定情報を取得するアプリケーションを作成します。

## REST操作の実装と抽象化

### REST操作の実行結果を定義
HTTPステータスコードやJSONなどの結果をまとめたクラスを定義する。

```pascal:RestResponseU.pas
unit RestResponseU;

interface

type
  TRestResponse = class
  private
    FStatusCode: Integer;
    FResultStr: string;
    function GetIsSucceeded: Boolean;
  public
    constructor Create(AStatusCode: Integer; AResultStr: string);
    property IsSucceeded: Boolean read GetIsSucceeded;
    property StatusCode: Integer read FStatusCode;
    property ResultStr: string read FResultStr;
  end;

implementation

constructor TRestResponse.Create(AStatusCode: Integer; AResultStr: string);
begin
  inherited Create;
  FStatusCode := AStatusCode;
  FResultStr := AResultStr;
end;

function TRestResponse.GetIsSucceeded: Boolean;
begin
  Result := FStatusCode = 200;
end;

end.
```
オブジェクトはイミュータブルにします。

### REST操作のインターフェイス
GETやPOSTといったHTTPメソッドを抽象化したインターフェイスを定義します。
テスタビリティを担保します。

```pascal:IRestClientU.pas
unit IRestClientU;

interface

uses
  System.Classes, RestResponseU;

type
  {$M+}
  IRestClient = interface
  ['{B8062E7E-E6C5-4EFB-9ED6-94947B5B87D7}']
    function Get(AUrl: string; AnAccept: string): TRestResponse; overload;
    function Get(AUrl: string; AnAccept: string; AnAccessToken: string): TRestResponse; overload;
    function Post(AUrl: string; AnAccessToken: string; AnAccept: string;
      AContentType: string): TRestResponse; overload;
    function Post(AUrl: string; AnAccessToken: string; AnAccept: string;
      AContentType: string; APostStream: TStringStream): TRestResponse; overload;
  end;
  {$M-}

implementation

end.
```
ここで、「{$M+}」と」「{$M-}」で括るのはDelphi Mocksを使えるようにするためです。

### REST操作の実装
定義したインターフェイスを実装します。詳細な実装は省いていて、イメージは以下。

```pascal:RestClientU.pas
unit RestClientU;

interface

uses
  System.Classes,
  System.Net.URLClient,
  IRestClientU,
  RestResponseU;

type
  /// <summary>
  /// HTTPリクエストの種類
  /// </summary>
  THttpMethod = (Get, Post);

  TRestClient = class(TInterfacedObject, IRestClient)
  private
    function SendData(AHttpMethod: THttpMethod; AUrl, ARequestStr,
      AnAccessToken, AContentType, AnAccept: string): TRestResponse; overload;
    function SendData(AHttpMethod: THttpMethod; AUrl, ARequestStr,
      AnAccessToken, AContentType, AnAccept: string;
      APostStream: TStringStream): TRestResponse; overload;
  public
    function Get(AUrl: string; AnAccept: string): TRestResponse; overload;
    function Get(AUrl: string; AnAccept: string; AnAccessToken: string): TRestResponse; overload;
    function Post(AUrl: string; AnAccessToken: string; AnAccept: string;
      AContentType: string): TRestResponse; overload;
    function Post(AUrl: string; AnAccessToken: string; AnAccept: string;
      AContentType: string; APostStream: TStringStream): TRestResponse; overload;
  end;

implementation

{ TRestClient }

uses
  System.Net.HttpClient,
  System.JSON,
  System.SysUtils,
  System.IOUtils,
  System.Generics.Collections,
  System.NetConsts,
  IdURI,
  IPPeerAPI,
  IdStack,
  IdMultipartFormData;

function TRestClient.Get(AUrl, AnAccept: string): TRestResponse;
begin
  Result := SendData(THttpMethod.Get, AUrl, string.Empty, string.Empty,
    string.Empty, string.Empty);
end;

function TRestClient.Get(AUrl, AnAccept, AnAccessToken: string): TRestResponse;
begin
  Result := SendData(THttpMethod.Get, AUrl, string.Empty, AnAccessToken,
    string.Empty, AnAccept);
end;

function TRestClient.Post(AUrl, AnAccessToken, AnAccept,
  AContentType: string): TRestResponse;
begin
  Result := SendData(THttpMethod.Post, AUrl, string.Empty, AnAccessToken,
    AContentType, AnAccept);
end;

function TRestClient.Post(AUrl, AnAccessToken, AnAccept,
  AContentType: string; APostStream: TStringStream): TRestResponse;
begin
  Result := SendData(THttpMethod.Post, AUrl, string.Empty, AnAccessToken,
    AContentType, AnAccept, APostStream);
end;

function TRestClient.SendData(AHttpMethod: THttpMethod; AUrl, ARequestStr,
  AnAccessToken, AContentType, AnAccept: string): TRestResponse;
begin
  Result := SendData(AHttpMethod, AUrl, ARequestStr, AnAccessToken,
    AContentType, AnAccept, nil);
end;

function TRestClient.SendData(AHttpMethod: THttpMethod; AUrl, ARequestStr,
  AnAccessToken, AContentType, AnAccept: string;
  APostStream: TStringStream): TRestResponse;
begin
  // THttpClientでGETやPOSTする。実装は省略。
end;

end.
end.
```

### REST操作の失敗時の例外
REST操作の失敗時は例外を出力します。例外を実装することで「何に失敗したか」の表現力を向上します。
```pascal:ApiExceptionU.pas
unit ApiExceptionU;

interface

uses
  System.SysUtils;

type
  EApiException = class(Exception)
  private
    FErrorCode: Integer;
  public
    /// <summary>
    /// コンストラクタ
    /// </summary>
    constructor Create(ErrorCode: Integer); overload;

    /// <summary>
    /// エラーコード
    /// </summary>
    property ErrorCode: Integer read FErrorCode;

    property Message;
  end;

implementation

{ EApiException }

/// <summary>
/// コンストラクタ
/// </summary>
constructor EApiException.Create(ErrorCode: Integer);
begin
  inherited Create(string.Empty);
  Self.FErrorCode := ErrorCode;
end;

end.
```

## ApiClientの実装と抽象化

### APIの戻り値のオブジェクト化
APIの結果の表現力を高めるため、APIの戻りをオブジェクトで表現します。

```pascal:ApiResultU.pas
unit ApiResultU;

interface

type
  TApiResult = class
  private
    FIsSucceeded: Boolean;
    FApiResult: string;
    FHttpStatusCode: Integer;
    FErrorCode: Integer;
    FErrorMessage: string;
  public
    constructor Create(AIsSucceeded: Boolean; AnApiResult, AnErrorMessage: string;
      AHttpStatusCode, AnErrorCode: Integer);
    property IsSucceeded: Boolean read FIsSucceeded;
    property ApiResult: string read FApiResult;
    property HttpStatusCode: Integer read FHttpStatusCode;
    property ErrorCode: Integer read FErrorCode;
    property ErrorMessage: string read FErrorMessage;
  end;

implementation

{ TApiResult }

constructor TApiResult.Create(AIsSucceeded: Boolean; AnApiResult,
  AnErrorMessage: string; AHttpStatusCode, AnErrorCode: Integer);
begin
  FIsSucceeded := AIsSucceeded;
  FApiResult := AnApiResult;
  FErrorMessage := AnErrorMessage;
  FHttpStatusCode := AHttpStatusCode;
  FErrorCode := AnErrorCode;
end;

end.
```

もちろん、**イミュータブル**。

### ApiClientのロギングについて
調査用に(エラー or 情報)ログの出力ロジックはほしいところ。「なににロギングするか？」を抽象化した方がベターで、これによりテスト可能とする。実装の例示は割愛します。

```pascal:ILogger.pas
unit ILoggerU;

interface

uses
  System.SysUtils;

type
  {$M+}
  ILogger = interface
    ['{C76E7EDA-92A4-4F9C-BECE-C100F9FAEAEE}']
    procedure WriteInfo(AMessage: string);
    procedure WriteError(AExeption: Exception); overload;
    procedure WriteError(AMessage: string); overload;
  end;
  {$M-}

implementation

end.
```

### ApiClientのインターフェイス
RestClientを使って各APIのApiClientを実装します。
こちらもテスタビリティを担保するため、インターフェイスを定義します。

```pascal:IApiClientU.pas
unit IApiClientU;

interface

uses
  ApiResultU;

type
  {$M+}
  IApiClient = interface
    ['{00428085-040A-47DA-BEE2-1065321F9B0E}']
      /// <summary>
      /// ユーザー名・パスワードから認証情報を取得します。
      /// </summary>
      function GetToken(AUrl, AUserId, APassword: string): TApiResult;
      /// <summary>
      /// 設定情報を取得します。
      /// </summary>
      function GetSettings(AUrl, AnAccessToken: string): TApiResult;
  end;
  {$M-}

implementation

end.
```

### ApiClientの実装
IApiClientUを継承して実装します。
```pascal:ApiClientU.pas
unit ApiClientU;

interface

uses
  IRestClientU,
  ILoggerU,
  IApiClientU,
  ApiResultU;

type
  TApiClient = class(TInterfacedObject, IApiClient)
  private
    FRestClient: IRestClient;
    FLogger: ILogger;
  public
    constructor Create(
      ARestClient: IRestClient;
      ALogger: ILogger);
    /// <summary>
    /// ユーザー名・パスワードから認証情報を取得します。
    /// </summary>
    function GetToken(AUrl, AUserId, APassword: string): TApiResult;
    /// <summary>
    /// 設定情報を取得します。
    /// </summary>
    function GetSettings(AUrl, AnAccessToken: string): TApiResult;
  end;

implementation

uses
  System.SysUtils,
  System.Classes,
  System.JSON,
  System.NetEncoding,
  Web.HTTPApp,
  RestResponseU,
  ApiExceptionU;

{ TApiClient }

constructor TApiClient.Create(ARestClient: IRestClient;
  ALogger: ILogger);
begin
  FRestClient := ARestClient;
  FLogger := ALogger;
end;

function TApiClient.GetToken(AUrl, AUserId, APassword: string): TApiResult;
begin
  // リクエストのBodyを構築する。
  var LPostStream: TStringStream := TStringStream.Create(
    'granttype=password' +
    '&userName=' + TNetEncoding.URL.Encode(UTF8Encode(AUserID)) +
    '&Password=' + TNetEncoding.URL.Encode(UTF8Encode(APassword)) +
    '&clientid=' + 'CLIENT_ID' +
    '&clientsecret=' + 'CLIENT_SECRET' +
    '&scope=' + 'SCOPE');

  var LResponse: TRestResponse := nil;
  var LErrorJson: TJSONObject := nil;
  try
    var LRequestUrl: string := AUrl;

    try
      LResponse := FRestClient.Post(LRequestUrl, string.Empty, 'application/json',
        'application/x-www-form-urlencoded', LPostStream);
    except on E: EAPiException do
      begin
        Result := TApiResult.Create(False, string.Empty, E.Message, 0, E.ErrorCode);
        FLogger.WriteError('予期せぬネットワークエラーが発生しました。');
        Exit;
      end;
    end;
    if LResponse.IsSucceeded then
    begin
      Result := TApiResult.Create(True, LResponse.ResultStr, string.Empty,
        LResponse.StatusCode, 0);
    end else
    begin
      // 以下はエラーハンドリング(詳細は割愛)
      Result := TApiResult.Create(False, string.Empty, 'error',
        LResponse.StatusCode, 200);
      FLogger.WriteError('APIの実行に失敗しました。'
        + Format('HttpStatus:%d', [LResponse.StatusCode]));
    end;
  finally
    if Assigned(LPostStream) then
    begin
      FreeAndNil(LPostStream);
    end;
    if Assigned(LResponse) then
    begin
      FreeAndNil(LResponse);
    end;
    if Assigned(LErrorJson) then
    begin
      FreeAndNil(LErrorJson);
    end;
  end;
end;

function TApiClient.GetSettings(AUrl, AnAccessToken: string): TApiResult;
begin
  var LResponse: TRestResponse := nil;
  var LErrorJson: TJSONObject := nil;
  try
    try
      // APIを投げる。
      LResponse := FRestClient.Get(AUrl, 'application/json', AnAccessToken);
    except on E: EAPiException do
      Result := TApiResult.Create(False, string.Empty, E.Message,
        LResponse.StatusCode, E.ErrorCode);
    end;

    if LResponse.IsSucceeded then
    begin
      Result := TApiResult.Create(True, LResponse.ResultStr, string.Empty,
        LResponse.StatusCode, 0);
    end else
    begin
      // 以下はエラーハンドリング(詳細は割愛)
      var LErrorMessage: string := '情報の取得に失敗しました。';;
      Result := TApiResult.Create(False, string.Empty, LErrorMessage,
        LResponse.StatusCode, 2010);
      FLogger.WriteError('APIの実行に失敗しました。'
        + Format('HttpStatus:%d', [LResponse.StatusCode]));
    end;
  finally
    if Assigned(LResponse) then
    begin
      FreeAndNil(LResponse);
    end;
    if Assigned(LErrorJson) then
    begin
      FreeAndNil(LErrorJson);
    end;
  end;
end;

end.
```

HTTPリクエストで予期しないエラーが起こった場合、RestClient側でEApiExceptionを投げ、ApiClient側でcatchすることでコードの表現力を高めた。

## Application Service(UseCase)の実装
ApiClientで実装したロジックをもとに、トークンを取得し、そのトークンで設定値を取得します。
```pascal:GetSettingsUseCaseU.pas
unit GetSettingsUseCaseU;

interface

uses
  IApiClientU,
  ILoggerU;

type
  TGetSettingsUseCase = class
  private
    FApiClient: IApiClient;
    FLogger: ILogger;
  public
    constructor Create(AnApiClient: IApiClient; ALogger: ILogger);

    function Execute(AUserId, APassword: string;
      out ASettingsResult: string): Integer;
  end;

implementation

uses
  System.SysUtils,
  System.JSON,
  ApiResultU;

{ TGetSettingsUseCase }

constructor TGetSettingsUseCase.Create(AnApiClient: IApiClient; ALogger: ILogger);
begin
  FApiClient := AnApiClient;
  FLogger := ALogger;
end;

function TGetSettingsUseCase.Execute(AUserId, APassword: string;
  out ASettingsResult: string): Integer;
begin
  ASettingsResult := string.Empty;

  var LGetTokenApiResult: TApiResult := nil;
  var LApiResult: TApiResult := nil;
  try
    var LAccessToken: string := string.Empty;
    LGetTokenApiResult := FApiClient.GetToken('url', AUserId, APassword);
    if LGetTokenApiResult.IsSucceeded then
    begin
      LAccessToken := LGetTokenApiResult.ApiResult;
    end else
    begin
      Result := 1;
      FLogger.WriteError('トークン取得に失敗');
      Exit;
    end;

    LApiResult := FApiClient.GetSettings('url', LAccessToken);
    if LApiResult.IsSucceeded then
    begin
      ASettingsResult := LApiResult.ApiResult;
      Result := 0;
    end else
    begin
      Result := 1;
      FLogger.WriteError('設定取得に失敗');
    end;
  finally
    if Assigned(LGetTokenApiResult) then
    begin
      FreeAndNil(LGetTokenApiResult);
    end;
    if Assigned(LApiResult) then
    begin
      FreeAndNil(LApiResult);
    end;
  end;
end;

end.
```

言うまでもないが、コンストラクタでApiClientとロギング処理の依存性を注入することでテスト可能としている。


## そこに、大義はあるか。
はてさて、こんな感じでオブジェクト指向を頑張ってみたわけだが。。。実装してて辛過ぎた。

私はC#erでVisual Studioでの開発に慣れてると、RAD Studioでの開発にストレスがたまり、もはや開発生産性が低下してるのでは？とすら感じました。とくに、インターフェイスを使うと実装先に飛ぶため、頑張って探さなきゃならない。VSならすぐに飛べるのに...
本当にDDDやClean Architectureライクな開発手法がDelphiでの開発で効果的なのか？そんな議論がQiitaで巻き起こるくらいにコミュニティが活性化してくれたらなぁー(;'∀')