RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

22歳になる長寿サービスのUI刷新 ~密結合システムからViewを分離した苦労話~

こんにちは、メールディーラー開発課のUKoniです。
2023年9月のことですが、弊社で開催した【ラクスMeetUp】持続的改善の実践/UI刷新・SQL改善・EOL対応で登壇させていただきました。
そこで話した、長寿サービスの密結合システムからViewを分離した話をご紹介します。

発表資料

speakerdeck.com

概要

メールディーラーでは、最近のメールプロダクトのトレンドやお客様からのご要望などを検討した結果、UIを刷新することになりました。
詳細な背景は下記記事を、刷新内容はスライドをご参照ください。
市場トレンドと顧客ニーズを密に連携~老舗メールプロダクトのUI刷新プロセス~ | エンジニアストーリー技術・デザイン情報 | 株式会社ラクス キャリア採用

しかし、以下の理由から現状では無理な状態でした。

  • 多機能かつ複雑なロジックのため、手動テストで品質が担保できない
  • 自動テストがない
    • 作成するにしてもビューロジックとビジネスロジックが密結合の状態のため、細かい仕様の確認ができない

自動テストの作成が難しいのはビューロジックとビジネスロジックが混在していることが原因です。
ならば、ビューロジックとビジネスロジックを分離してしよう、ということになりました。

作業内容

こちらの説明の際に、以下のサンプルコードを元に説明します。

<?php

// HTMLのコード start
$isEdit = $_REQUEST['mode'] === 'edit';
if ($isEdit) {
  if ($_REQUEST['input'] === 'radio') {
    print "<input type='radio' id='male' name='gender' value='0'>男";
    print "<input type='radio' id='female' name='gender' value='1'>女";
  } else {
    print "<input type='text' id='input' name='text' value=''>";
  }
}
// HTMLのコード end

// JavaScriptのコード start
if ($isEdit) {
  $js = "alert('edit');";
} else {
  $js = "alert('view');";
}
print "<script>{$js}</script>";
// JavaScriptのコード end

// PHPのコード start
if ($isEdit) {
  $sum = 0;
  foreach ($_REQUEST['count'] as $count) {
    $sum += $count;
  }
}
// PHPのコード end

// HTMLのコード start
print "<p>{$count}</p>";
// HTMLのコード end

1. 旧画面のコードから機能一覧を作成する

機能落ちや想定外の不具合を防ぐために、どういう機能があるかを一覧にしました。
サンプルコードの機能を一覧にすると、以下のようになります。

2. IDEの機能を使用して、共通利用するロジックをメソッドに切り出す

表題の通りです。
新UIでも使用するロジックを共通化します。

3. 切り出したメソッドのユニットテストを作成する

こちらも表題の通りです。
ここでようやく、旧画面のロジックを「テスト可能な状態」にすることができました。

4. ビューロジックとビジネスロジックを分割する

Laravel/Vue.jsを導入して、テンプレートエンジンに渡すようにします。

手順

  1. 旧画面のコードをそのままLaravelへ移植
  2. HTML作成のコードをコメントアウト
  3. 機能一覧に従って、新UIを実装
  4. ③で作成した機能のActionクラスの自動テストを作成・実施
  5. 想定通りレスポンスパラメータが送信されているかを確認
  6. 不要なコードを削除(コメントアウトしたコードなど)
  7. 機能一覧を使って、受入テストを実施(最終確認)

詳しい内容は、同じ課のメンバーが発表していますので、こちらをご参照ください。

fortee.jp

上記の作業をした結果、コードは以下のようになりました。

ビューロジック

JavaScriptコード
const vm = {
 mounted() {
   if (params.isEdit) {
     alert('edit')
   } else {
     alert('view')
   }
 }
}
export default vm;
HTMLコード(bladeファイル)
<?php

@if($isEdit === 'edit')
   @if($input === 'radio')
       <input type='radio' id='male' name='gender' value='0'><input type='radio' id='female' name='gender' value='1'>女
   @else
       <input type='text' id='input' name='text' value=''>
   @endif
@endif
<script src="/js/page_a.js"></script>
<p>{{$count}}</p>

ビジネスロジック

Actionクラス
<?php

class PageAAction {
 protected $Domain;
 protected $Responder;

 public function __construct(Domain $Domain, Responder $Responder) {
   $this->Domain     = $Domain;
   $this->Responder  = $Responder;
 }

 public function __invoke(Request $request): Response {
   return $this->Responder->response($request, $this->Domain->get($request));
 }
}
Responderクラス
<?php

class PageAResponder {
 protected $response;
 protected $view;
 public function __construct(Response $response, ViewFactory $view) {
   $this->response = $response;
   $this->view = $view;
 }
 public function response(Request $request, $data): Response {
   $isEdit = $request->get('mode') === 'edit';
   if ($isEdit) {
    $sum = 0;
    foreach ($request->get('count') as $count) {
      $sum += $count;
    }
   }
   $this->response->setContent(
     $this->view->make('page_a', [
       'isEdit' => ($isEdit) ? 'edit' : 'view',
       'input' => $request->get('input'),
       'count' => $sum,
     ])
   );
   return $this->response;
 }
}

その結果・・・

UIを新しくすることができました。

Before

After

今回の結果

  • 自動テストを作成したので、今後の追加実装や改修がしやすくなった
  • クリティカルな不具合が少なかった
  • 新UIへの移行の手法が確立できたので、他の機能でも活用できる

苦労した話

  • 3000行超えの複雑なコードから機能一覧を作るのがしんどかった
  • 旧画面のロジックを移植したことで変数のスコープが変化し、それが原因でバグが発生した

今後の展望

  • 今回の刷新で対象外だった画面を新UIに改修
  • 今回改修したロジックのリファクタリング
  • ADR実装に沿った実装にする
    • デグレ防止のために旧画面のロジックをそのまま移植しているため、適切な箇所に実装する

まとめ

長寿サービスの密結合システムからViewを分離した話をご紹介しました。
同じような悩みを持っている方のお役に立てたら幸いです。

Copyright © RAKUS Co., Ltd. All rights reserved.