並行変更

2014年5月13日

インターフェースへの変更がすべての利用者に影響を与える場合、2つの思考モードが必要です。変更の実装自体と、すべての利用箇所の更新です。特に、変更が複数のクライアントや外部クライアントを持つ公開されたインターフェースにある場合、両方を同時に行うのは難しい場合があります。

並行変更展開と縮小とも呼ばれる)は、インターフェースへの後方互換性のない変更を安全な方法で実装するためのパターンであり、変更を3つの異なるフェーズ(展開、移行、縮小)に分割します。

このパターンを理解するために、xyの整数座標のペアを使用してセルに関する情報を格納および提供する単純なGridクラスの例を使用しましょう。セルは内部的に2次元配列に格納され、クライアントはaddCell()fetchCell()isEmpty()メソッドを使用してグリッドと対話できます。

  class Grid {
    private Cell[][] cells;
    …

    public void addCell(int x, int y, Cell cell) {
      cells[x][y] = cell;
    }

    public Cell fetchCell(int x, int y) {
      return cells[x][y];
    }

    public boolean isEmpty(int x, int y) {
      return cells[x][y] == null;
    }
  }
  

リファクタリングの一部として、xyデータクランプであると検出し、新しいCoordinateクラスを導入することにしました。ただし、これはGridクラスのクライアントにとって後方互換性のない変更になります。すべてのメソッドと内部データ構造を一度に変更する代わりに、並行変更パターンを適用することにしました。

展開フェーズでは、古いバージョンと新しいバージョンの両方をサポートするようにインターフェースを拡張します。この例では、新しいMap<Coordinate, Cell>データ構造と、既存のコードを変更せずにCoordinateインスタンスを受け取ることができる新しいメソッドを導入します。

  class Grid {
    private Cell[][] cells;
    private Map<Coordinate, Cell> newCells;
    …

    public void addCell(int x, int y, Cell cell) {
      cells[x][y] = cell;
    }

    public void addCell(Coordinate coordinate, Cell cell) {
      newCells.put(coordinate, cell);
    }

    public Cell fetchCell(int x, int y) {
      return cells[x][y];
    }

    public Cell fetchCell(Coordinate coordinate) {
      return newCells.get(coordinate);
    }

    public boolean isEmpty(int x, int y) {
      return cells[x][y] == null;
    }

    public boolean isEmpty(Coordinate coordinate) {
      return !newCells.containsKey(coordinate);
    }
  }
  

既存のクライアントは引き続き古いバージョンを使用し、新しい変更は影響を与えることなく段階的に導入できます。

移行フェーズでは、古いバージョンを使用しているすべてのクライアントを新しいバージョンに更新します。これは段階的に行うことができ、外部クライアントの場合、これが最も長いフェーズになります。

すべての利用箇所が新しいバージョンに移行されたら、縮小フェーズを実行して古いバージョンを削除し、新しいバージョンのみをサポートするようにインターフェースを変更します。

この例では、古いメソッドが削除された後、内部の2次元配列はもう使用されないため、そのデータ構造を安全に削除し、newCellsの名前をcellsに戻すことができます。

  class Grid {
    private Map<Coordinate, Cell> cells;
    …

    public void addCell(Coordinate coordinate, Cell cell) {
      cells.put(coordinate, cell);
    }

    public Cell fetchCell(Coordinate coordinate) {
      return cells.get(coordinate);
    }

    public boolean isEmpty(Coordinate coordinate) {
      return !cells.containsKey(coordinate);
    }
  }
  

このパターンは、継続的デリバリーを実践する際に特に役立ちます。これは、コードをこれら3つのフェーズのいずれかでリリースできるためです。また、クライアントを移行し、新しいバージョンを段階的にテストできるため、変更のリスクを軽減します。

インターフェースのすべての利用箇所を制御できる場合でも、このパターンに従うことは、コードベース全体に一度に破損を広げるのを防ぐため、依然として役立ちます。移行フェーズは短くすることができますが、修正する必要のあるすべての利用箇所を見つけるためにコンパイラに頼る代わりになります。

このパターンのいくつかの適用例を以下に示します。

  • リファクタリング:メソッドまたは関数のシグネチャを変更する場合、特に長期的なリファクタリングを行う場合、または公開されたインターフェースを変更する場合。リファクタリング中にこのパターンのバリエーション実装として、新しいAPIに関して古いメソッドを実装し、インラインメソッドを使用してすべての利用箇所を一度に更新する方法があります。古いメソッドを新しいメソッドに委譲することも、移行フェーズをより小さく安全なステップに分割する方法であり、クライアントに公開されたAPIを変更する前に、最初に内部実装を変更できるようにします。これは、移行フェーズが長く、2つの別々の実装を維持する必要がない場合に役立ちます。
  • データベースリファクタリング:これは進化的データベース設計の重要な要素です。ほとんどのデータベースリファクタリングは並行変更パターンに従います。移行フェーズは、元のスキーマと新しいスキーマ間の移行期間であり、すべてのデータベースアクセスコードが新しいスキーマで動作するように更新されるまでです。
  • デプロイメント:カナリアリリースやブルーグリーンデプロイメントなどのデプロイメント手法は、古いバージョンと新しいバージョンのコードが並行してデプロイされており、ユーザーをあるバージョンから別のバージョンに段階的に移行するため、変更のリスクを軽減する並行変更パターンの適用例です。マイクロサービスアーキテクチャでは、サービス間のバージョン依存関係による複雑なデプロイメントオーケストレーションの必要性をなくすこともできます。
  • リモートAPIの進化:並行変更は、後方互換性のある方法で変更できない場合にリモートAPI(例:REST Webサービス)を進化させるために使用できます。これは、公開されたAPIで明示的なバージョンを使用する代わりに利用できます。APIによって受け入れられる、または返されるペイロードを特定のエンドポイントで変更する場合、または古いバージョンと新しいバージョンを区別するために新しいエンドポイントを導入する場合に、このパターンを適用できます。同じエンドポイントで並行変更を使用する場合、ポステルの法則に従うことは、ペイロードが拡張されたときに消費者が破損するのを防ぐための良い手法です。

移行フェーズでは、機能フラグを使用して、インターフェースのどのバージョンを使用するかを制御できます。クライアント側の機能トグルにより、サプライヤーの新しいバージョンと前方互換性を持たせることができ、サプライヤーのリリースをクライアントから分離できます。

BranchByAbstractionを実装する場合、並行変更はクライアントとサプライヤーの間に抽象化レイヤーを導入するのに適した方法です。また、サプライヤー側で置換用の継ぎ目として抽象化レイヤーを導入せずに大規模な変更を実行する別の方法でもあります。ただし、クライアントの数が多い場合は、抽象化による分岐を使用する方が、変更範囲を狭め、移行フェーズ中の混乱を減らすためのより良い戦略です。

並行変更を使用する欠点は、移行フェーズ中にサプライヤーが2つの異なるバージョンをサポートする必要があり、クライアントはどのバージョンが新しいバージョンで、どのバージョンが古いバージョンであるかについて混乱する可能性があることです。縮小フェーズが実行されないと、開始したときよりも悪い状態になる可能性があり、したがって、移行を正常に完了するための規律が必要です。非推奨の注釈、ドキュメント、またはTODO注釈を追加すると、クライアントや同じコードベースで作業している他の開発者に、どのバージョンが置き換えられているのかを知らせるのに役立つ場合があります。

さらに読む

Industrial Logicのリファクタリングアルバムでは、並行変更を実行する例を文書化して示しています。

謝辞

この手法は、2006年にJoshua Kerievskyによってリファクタリング戦略として最初に文書化され、2010年のLean Software and Systems Conferenceでの講演The Limited Red Societyで発表されました。

この投稿の最初のドラフトについてフィードバックを提供してくれたJoshua Kerievskyに感謝します。また、フィードバックを提供してくれた多くのThoughtworksの同僚にも感謝します:Greg Dutcher、Badrinath Janakiraman、Praful Todkar、Rick Carragher、Filipe Esperandio、Jason Yip、Tushar Madhukar、Pete Hodgson、およびKief Morris。