外部サービスにアクセスするコードのリファクタリング
外部サービスを扱うコードを書くとき、そのアクセスコードを別々のオブジェクトに分離することが有効だと感じています。ここでは、凝集したコードをこの分離の一般的なパターンにリファクタリングする方法を示します。
2015年2月17日
ソフトウェアシステムの特徴の1つは、単独では存在しないことです。何か有用なことを行うには、通常、異なる人々によって書かれた、私たちが知らない、そして私たちが書いているソフトウェアについても知らない、あるいは気にしない他のソフトウェア部分と通信する必要があります。
このような外部連携を行うソフトウェアを記述する際には、優れたモジュール性とカプセル化を適用することが特に有用だと思います。これを行う際に私が見てきて、価値があると確信している一般的なパターンがあります。
この記事では、簡単な例を取り上げ、私が求めているモジュール性を導入するリファクタリングの手順を説明します。
初期コード
この例コードの仕事は、JSONファイルから動画に関するデータを読み込み、YouTubeのデータでそれを強化し、いくつかの簡単な追加データ計算を行い、そのデータをJSONで返すことです。
初期コードを以下に示します。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')) ids = @video_list.map{|v| v['youtubeID']} client = GoogleAuthorizer.new( token_key: 'api-youtube', application_name: 'Gateway Youtube Example', application_version: '0.1' ).api_client youtube = client.discovered_api('youtube', 'v3') request = { api_method: youtube.videos.list, parameters: { id: ids.join(","), part: 'snippet, contentDetails, statistics', } } response = JSON.parse(client.execute!(request).body) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_record = response['items'].find{|v| id == v['id']} video['views'] = youtube_record['statistics']['viewCount'].to_i days_available = Date.today - Date.parse(youtube_record['snippet']['publishedAt']) video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list) end
この例で使用されている言語はRubyです。
ここで最初に言っておきたいのは、この例にはコードがあまりないということです。コードベース全体がこのスクリプトだけである場合、モジュール性についてそれほど心配する必要はありません。小さな例が必要ですが、現実のシステムを見ると、読者の目がすぐにガラス細工になるでしょう。そのため、このコードを数万行のシステム内の典型的なコードだと想像していただく必要があります。
YouTube APIへのアクセスは、GoogleAuthorizerオブジェクトを介して行われます。この記事の目的では、これを外部APIとして扱います。これは、Googleサービス(YouTubeなど)への接続の面倒な詳細を処理し、特に認証の問題を処理します。その仕組みを理解したい場合は、最近書いたGoogle APIへのアクセスに関する記事をご覧ください。
このコードはどうなっているのでしょうか?このコードが実行しているすべてを理解できなくても、異なる懸念事項が混在していることはわかるはずです。これは、下のコード例を色分けすることで示唆しています。変更を行うには、YouTubeのAPIへのアクセス方法、YouTubeのデータ構造、そしてドメインロジックの一部を理解する必要があります。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')) ids = @video_list.map{|v| v['youtubeID']} client = GoogleAuthorizer.new( token_key: 'api-youtube', application_name: 'Gateway Youtube Example', application_version: '0.1' ).api_client youtube = client.discovered_api('youtube', 'v3') request = { api_method: youtube.videos.list, parameters: { id: ids.join(","), part: 'snippet, contentDetails, statistics', } } response = JSON.parse(client.execute!(request).body) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_record = response['items'].find{|v| id == v['id']} video['views'] = youtube_record['statistics']['viewCount'].to_i days_available = Date.today - Date.parse(youtube_record['snippet']['publishedAt']) video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list) end
私のようなソフトウェアの達人は、「懸念事項の分離」についてよく話します。これは基本的に、異なるトピックは別々のモジュールにあるべきという意味です。私がこれを行う主な理由は理解です。適切にモジュール化されたプログラムでは、各モジュールは1つのトピックに関するものなので、理解する必要のないものについては無知を装うことができます。YouTubeのデータ形式が変更された場合、アクセスコードを再配置するためにアプリケーションのドメインロジックを理解する必要はありません。YouTubeから新しいデータを取得してドメインロジックで使用するという変更を行っている場合でも、そのタスクをそれらの部分に分割し、それぞれを個別に処理することで、頭の中で回転させる必要があるコードの行数を最小限に抑えることができます。
私のリファクタリングの使命は、これらの懸念事項を別々のモジュールに分割することです。完了したら、Video Service内のコードは、色付けされていないコード、つまりこれらの他の責任を調整するコードのみになります。
コードのテスト化
リファクタリングの最初のステップは常に同じです。うっかり何かを壊してしまうのではないかと確信する必要があります。リファクタリングは、すべて動作を維持する多数の小さなステップを繋げていくことです。ステップを小さくすることで、失敗する可能性が低くなります。しかし、私は自分のことをよく知っているので、最も単純な変更でも失敗することができるとわかっています。そのため、必要な自信を得るには、間違いをキャッチするためのテストが必要です。
しかし、このようなコードはテストするのが簡単ではありません。計算された毎月の視聴回数フィールドをアサートするテストを書ければ良いでしょう。結局のところ、他の何かが間違っていれば、これは間違った答えになります。しかし、問題は、ライブのYouTubeデータにアクセスしていて、人々は動画を見る習慣があることです。YouTubeの視聴回数フィールドは定期的に変化するため、テストが非決定的に赤くなります。
そのため、私の最初の仕事は、その不安定な部分を削除することです。そのためには、テストダブル、YouTubeのように見えるが決定的な方法で応答するオブジェクトを導入することができます。残念ながら、ここではレガシーコードのジレンマに遭遇します。
レガシーコードのジレンマ:コードを変更する際には、テストが整っている必要があります。テストを配置するには、多くの場合、コードを変更する必要があります。
-- マイケル・フェザーズ
テストなしに変更を行う必要があるため、YouTubeとのやり取りをテストダブルを導入できるシームの背後に配置するのに考えられる最小限かつ最も単純な変更を行う必要があります。そのため、私の最初のステップは、Extract Methodを使用して、YouTubeとのやり取りをルーチンの残りの部分から独自のメソッドに切り離すことです。
class VideoService…
def video_list
@video_list = JSON.parse(File.read('videos.json'))
ids = @video_list.map{|v| v['youtubeID']}
response = call_youtube ids
ids.each do |id|
video = @video_list.find{|v| id == v['youtubeID']}
youtube_record = response['items'].find{|v| id == v['id']}
video['views'] = youtube_record['statistics']['viewCount'].to_i
days_available = Date.today - Date.parse(youtube_record['snippet']['publishedAt'])
video['monthlyViews'] = video['views'] * 365.0 / days_available / 12
end
return JSON.dump(@video_list)
end
def call_youtube ids
client = GoogleAuthorizer.new(
token_key: 'api-youtube',
application_name: 'Gateway Youtube Example',
application_version: '0.1'
).api_client
youtube = client.discovered_api('youtube', 'v3')
request = {
api_method: youtube.videos.list,
parameters: {
id: ids.join(","),
part: 'snippet, contentDetails, statistics',
}
}
return JSON.parse(client.execute!(request).body)
end
これを行うことで、2つのことが達成されます。まず、Google APIの操作コードを独自のメソッドにきれいに抽出することで、他の種類のコードから(ほとんど)分離されます。これはそれ自体が価値のあることです。第二に、そしてより緊急なことに、テスト動作を代用するために使用できるシームが設定されます。Rubyのビルトインminitestライブラリを使用すると、オブジェクトの個々のメソッドを簡単にスタブできます。
class VideoServiceTester < Minitest::Test def setup vs = VideoService.new vs.stub(:call_youtube, stub_call_youtube) do @videos = JSON.parse(vs.video_list) @µS = @videos.detect{|v| 'wgdBVIX9ifA' == v['youtubeID']} @evo = @videos.detect{|v| 'ZIsgHs0w44Y' == v['youtubeID']} end end def stub_call_youtube JSON.parse(File.read('test/data/youtube-video-list.json')) end def test_microservices_monthly_json assert_in_delta 5880, @µS ['monthlyViews'], 1 assert_in_delta 20, @evo['monthlyViews'], 1 end # further tests as needed…
YouTubeの呼び出しを分離し、スタブすることで、このテストを決定的に動作させることができます。少なくとも今日はそうなるでしょう。明日動作させるには、`Date.today`の呼び出しについても同様の処理を行う必要があります。
class VideoServiceTester…
def setup
Date.stub(:today, Date.new(2015, 2, 2)) do
vs = VideoService.new
vs.stub(:call_youtube, stub_call_youtube) do
@videos = JSON.parse(vs.video_list)
@µS = @videos.detect{|v| 'wgdBVIX9ifA' == v['youtubeID']}
@evo = @videos.detect{|v| 'ZIsgHs0w44Y' == v['youtubeID']}
end
end
end
リモート呼び出しを接続オブジェクトに分離する
コードを異なる関数に入れることで懸念事項を分離することは、最初のレベルの分離です。しかし、懸念事項がドメインロジックと外部データプロバイダの処理のように異なる場合、異なるクラスへの分離レベルを高める方が好みです。

図1:最初は、videoserviceクラスには4つの責任があります。
そのため、私の最初のステップは、新しいクラスを作成し、Move Methodを使用することです。
class VideoService…
def call_youtube ids YoutubeConnection.new.list_videos ids end
class YoutubeConnection…
def list_videos ids client = GoogleAuthorizer.new( token_key: 'api-youtube', application_name: 'Gateway Youtube Example', application_version: '0.1' ).api_client youtube = client.discovered_api('youtube', 'v3') request = { api_method: youtube.videos.list, parameters: { id: ids.join(","), part: 'snippet, contentDetails, statistics', } } return JSON.parse(client.execute!(request).body) end
それによって、メソッドをスタブするのではなく、テストダブルを返すようにスタブを変更することもできます。
class VideoServiceTester…
def setup
Date.stub(:today, Date.new(2015, 2, 2)) do
YoutubeConnection.stub(:new, YoutubeConnectionStub.new) do
@videos = JSON.parse(VideoService.new.video_list)
@µS = @videos.detect{|v| 'wgdBVIX9ifA' == v['youtubeID']}
@evo = @videos.detect{|v| 'ZIsgHs0w44Y' == v['youtubeID']}
end
end
end
class YoutubeConnectionStub…
def list_videos ids JSON.parse(File.read('test/data/youtube-video-list.json')) end
このリファクタリングを行う際には、輝く新しいテストがスタブの背後で行う間違いをキャッチしないことを警戒する必要があります。そのため、本番コードがまだ機能することを手動で確認する必要があります。(そして、あなたが尋ねたので、はい、これを行う際に間違いを犯しました(list-videosへの引数を省略しました)。これほど多くテストする必要がある理由があります。)
個別のクラスによって得られる懸念事項のより大きな分離により、テストのためのより良いシームも得られます。スタブする必要があるすべてのものを単一のオブジェクト作成にラップできます。これは、テスト中に同じサービスオブジェクトへの複数の呼び出しを行う必要がある場合に特に便利です。
YouTubeへの呼び出しが接続オブジェクトに移動したので、Video Serviceのメソッドはもはや価値がないため、Inline Methodの対象となります。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')) ids = @video_list.map{|v| v['youtubeID']} response = YoutubeConnection.new.list_videos ids ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_record = response['items'].find{|v| id == v['id']} video['views'] = youtube_record['statistics']['viewCount'].to_i days_available = Date.today - Date.parse(youtube_record['snippet']['publishedAt']) video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list) end def call_youtube ids YoutubeConnection.new.list_videos ids end
スタブがJSON文字列を解析する必要があるのは好きではありません。全体として、私は接続オブジェクトをHumble Objectsとして維持することを好みます。なぜなら、それらが実行する動作はテストされないからです。そのため、解析を呼び出し元に引き出すことを好みます。
class VideoService…
def video_list
@video_list = JSON.parse(File.read('videos.json'))
ids = @video_list.map{|v| v['youtubeID']}
response = JSON.parse(YoutubeConnection.new.list_videos(ids))
ids.each do |id|
video = @video_list.find{|v| id == v['youtubeID']}
youtube_record = response['items'].find{|v| id == v['id']}
video['views'] = youtube_record['statistics']['viewCount'].to_i
days_available = Date.today - Date.parse(youtube_record['snippet']['publishedAt'])
video['monthlyViews'] = video['views'] * 365.0 / days_available / 12
end
return JSON.dump(@video_list)
end
class YoutubeConnection…
def list_videos ids
client = GoogleAuthorizer.new(
token_key: 'api-youtube',
application_name: 'Gateway Youtube Example',
application_version: '0.1'
).api_client
youtube = client.discovered_api('youtube', 'v3')
request = {
api_method: youtube.videos.list,
parameters: {
id: ids.join(","),
part: 'snippet, contentDetails, statistics',
}
}
return JSON.parse(client.execute!(request).body)
end
class YoutubeConnectionStub…
def list_videos ids
JSON.parse(File.read('test/data/youtube-video-list.json'))
end

図2:最初の主要なステップでは、YouTube接続コードを接続オブジェクトに分割します。
YouTubeデータ構造をゲートウェイに分離する
YouTubeへの基本的な接続を分離してスタブできるようになったので、YouTubeのデータ構造を掘り下げるコードに取り組むことができます。ここでの問題は、ビューカウントデータを取得するには結果の「statistics」部分を確認する必要があること、公開日を取得するには「snippet」セクションを確認する必要があることを、多くのコードが知っている必要があることです。このような掘り下げは、リモートソースからのデータでは一般的であり、それらにとって意味のある方法で整理されていますが、私にとってはそうではありません。これはまったく合理的な動作です。彼らは私のニーズを理解しておらず、私自身でそれを行うのに苦労しています。
これについて考える良い方法として、Eric EvansのBounded Contextの概念があります。YouTubeは自分のコンテキストに従ってデータを整理しますが、私は別のコンテキストに従って整理したいと考えています。2つのバウンデッドコンテキストを組み合わせたコードは、2つの異なる語彙を混在させているため、複雑になります。Ericが呼ぶ「アンチ腐敗層」でそれらを分離する必要があります。それらの間の明確な境界です。彼のアンチ腐敗層のイラストは万里の長城であり、このような壁と同様に、いくつかのものを通過させることができるゲートウェイが必要です。ソフトウェアの用語では、ゲートウェイを使用すると、壁を通してアクセスしてYouTubeのバウンデッドコンテキストから必要なデータを取得できます。しかし、ゲートウェイは、彼らのコンテキストではなく、私のコンテキストで意味をなす方法で表現する必要があります。

この簡単な例では、クライアントがYouTubeのデータ構造にどのように格納されているかを知らなくても、公開日とビューカウントを取得できるゲートウェイオブジェクトを意味します。ゲートウェイオブジェクトは、YouTubeのコンテキストから私のコンテキストに変換します。
まず、接続から得られたレスポンスで初期化するゲートウェイオブジェクトを作成します。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')) ids = @video_list.map{|v| v['youtubeID']} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_record = youtube.record(id) video['views'] = youtube_record['statistics']['viewCount'].to_i days_available = Date.today - Date.parse(youtube_record['snippet']['publishedAt']) video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list) end
class YoutubeGateway…
def initialize responseJson @data = JSON.parse(responseJson) end def record id @data['items'].find{|v| id == v['id']} end
現時点では、ゲートウェイのrecordメソッドは最終的には使用しない予定なので、可能な限り単純な動作を作成しました。実際、お茶休憩しなければ30分も持たないと思います。
次に、ビューに関する詳細なロジックをサービスからゲートウェイに移し、各ビデオレコードを表す個別のゲートウェイアイテムクラスを作成します。
class VideoService…
def video_list
@video_list = JSON.parse(File.read('videos.json'))
ids = @video_list.map{|v| v['youtubeID']}
youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids))
ids.each do |id|
video = @video_list.find{|v| id == v['youtubeID']}
youtube_record = youtube.record(id)
video['views'] = youtube.item(id)['views']
days_available = Date.today - Date.parse(youtube_record['snippet']['publishedAt'])
video['monthlyViews'] = video['views'] * 365.0 / days_available / 12
end
return JSON.dump(@video_list)
end
class YoutubeGateway…
def item id { 'views' => record(id)['statistics']['viewCount'].to_i } end
公開日についても同様です。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')) ids = @video_list.map{|v| v['youtubeID']} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_record = youtube.record(id) video['views'] = youtube.item(id)['views'] days_available = Date.today - youtube.item(id)['published'] video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list) end
class YoutubeGateway…
def item id
{
'views' => record(id)['statistics']['viewCount'].to_i,
'published' => Date.parse(record(id)['snippet']['publishedAt'])
}
end
キーで検索したゲートウェイ内のレコードを使用しているので、リストをハッシュに置き換えることで、内部データ構造でその使用方法をより適切に反映したいと思います。
class YoutubeGateway…
def initialize responseJson @data = JSON.parse(responseJson)['items'] .map{|i| [ i['id'], i ] } .to_h end def item id { 'views' => @data[id]['statistics']['viewCount'].to_i, 'published' => Date.parse(@data[id]['snippet']['publishedAt']) } end def record id @data['items'].find{|v| id == v['id']} end

図4:データ処理をゲートウェイオブジェクトに分離する
これで、目的のキー分離が完了しました。YouTube接続オブジェクトはYouTubeへの呼び出しを処理し、YouTubeゲートウェイオブジェクトに渡すデータ構造を返します。サービスコードは、異なるサービスでのデータの保存方法ではなく、データの表示方法に関するものになりました。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')) ids = @video_list.map{|v| v['youtubeID']} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} video['views'] = youtube.item(id)['views'] days_available = Date.today - youtube.item(id)['published'] video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list) end
ドメインロジックをドメインオブジェクトに分離する
YouTubeとのすべてのやり取りは別々のオブジェクトに分割されていますが、ビデオサービスはまだドメインロジック(月間ビューの計算方法)と、ローカルに保存されたデータとサービス内のデータ間の関係の調整を混在させています。ビデオのドメインオブジェクトを導入すれば、それを分離できます。
最初のステップは、ビデオデータのハッシュをオブジェクトで単純にラップすることです。
class Video…
def initialize aHash @data = aHash end def [] key @data[key] end def []= key, value @data[key] = value end def to_h @data end
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)} ids = @video_list.map{|v| v['youtubeID']} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} video['views'] = youtube.item(id)['views'] days_available = Date.today - youtube.item(id)['published'] video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list.map{|v| v.to_h}) end
計算ロジックを新しいビデオオブジェクトに移すには、まず移動のために適切な形にする必要があります。これは、ビデオドメインオブジェクトとYouTubeゲートウェイアイテムを引数とするビデオサービスの単一メソッドにすべてを分割することで行えます。その最初のステップは、ゲートウェイアイテムに対して
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)} ids = @video_list.map{|v| v['youtubeID']} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_item = youtube.item(id) video['views'] = youtube_item['views'] days_available = Date.today - youtube_item['published'] video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end return JSON.dump(@video_list.map{|v| v.to_h}) end
これで、計算ロジックを独自のメソッドに簡単に抽出できます。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)} ids = @video_list.map{|v| v['youtubeID']} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_item = youtube.item(id) enrich_video video, youtube_item end return JSON.dump(@video_list.map{|v| v.to_h}) end def enrich_video video, youtube_item video['views'] = youtube_item['views'] days_available = Date.today - youtube_item['published'] video['monthlyViews'] = video['views'] * 365.0 / days_available / 12 end
そして、メソッド移動を適用して、ビデオに移動するのは簡単です。
class VideoService…
def video_list
@video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)}
ids = @video_list.map{|v| v['youtubeID']}
youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids))
ids.each do |id|
video = @video_list.find{|v| id == v['youtubeID']}
youtube_item = youtube.item(id)
video.enrich_with_youtube youtube_item
end
return JSON.dump(@video_list.map{|v| v.to_h})
end
class Video…
def enrich_with_youtube youtube_item @data['views'] = youtube_item['views'] days_available = Date.today - youtube_item['published'] @data['monthlyViews'] = @data['views'] * 365.0 / days_available / 12 end
これで、ビデオのハッシュの更新を削除できます。
class Video…
def []= key, value @data[key] = value end
適切なオブジェクトができたので、サービスメソッド内のIDによる調整を簡素化できます。まず、youtube_item
に対して一時変数のインライン化を使用し、次に列挙インデックスへの参照をビデオオブジェクトのメソッド呼び出しに置き換えます。
class VideoService…
def video_list @video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)} ids = @video_list.map{|v| v['youtubeID']} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) ids.each do |id| video = @video_list.find{|v| id == v['youtubeID']} youtube_item = youtube.item(id) video.enrich_with_youtube(youtube.item(video.youtube_id)) end return JSON.dump(@video_list.map{|v| v.to_h}) end
class Video…
def youtube_id
@data['youtubeID']
end
これにより、列挙にオブジェクトを直接使用できるようになります。
class VideoService…
def video_list
@video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)}
ids = @video_list.map{|v| v['youtubeID']}
youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids))
@video_list.each {|v| v.enrich_with_youtube(youtube.item(v.youtube_id))}
return JSON.dump(@video_list.map{|v| v.to_h})
end
そして、ビデオ内のハッシュへのアクセサを削除します。
class Video…
def [] key @data[key] end
class VideoService…
def video_list
@video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)}
ids = @video_list.map{|v| v.youtube_id}
youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids))
@video_list.each {|v| v.enrich_with_youtube(youtube.item(v.youtube_id))}
return JSON.dump(@video_list.map{|v| v.to_h})
end
ビデオオブジェクトの内部ハッシュをフィールドに置き換えることもできますが、主にハッシュでロードされ、最終出力はJSON形式のハッシュであるため、それだけの価値はないと思います。埋め込みドキュメントは、完全に妥当なドメインオブジェクトの形式です。
最終的なオブジェクトに関する考察

図5:このリファクタリングによって作成されたオブジェクトとその依存関係
class VideoService...
def video_list @video_list = JSON.parse(File.read('videos.json')).map{|h| Video.new(h)} ids = @video_list.map{|v| v.youtube_id} youtube = YoutubeGateway.new(YoutubeConnection.new.list_videos(ids)) @video_list.each {|v| v.enrich_with_youtube(youtube.item(v.youtube_id))} return JSON.dump(@video_list.map{|v| v.to_h}) end
class YoutubeConnection
def list_videos ids
client = GoogleAuthorizer.new(
token_key: 'api-youtube',
application_name: 'Gateway Youtube Example',
application_version: '0.1'
).api_client
youtube = client.discovered_api('youtube', 'v3')
request = {
api_method: youtube.videos.list,
parameters: {
id: ids.join(","),
part: 'snippet, contentDetails, statistics',
}
}
return client.execute!(request).body
end
end
class YoutubeGateway
def initialize responseJson
@data = JSON.parse(responseJson)['items']
.map{|i| [ i['id'], i ] }
.to_h
end
def item id
{
'views' => @data[id]['statistics']['viewCount'].to_i,
'published' => Date.parse(@data[id]['snippet']['publishedAt'])
}
end
end
class Video
def initialize aHash
@data = aHash
end
def to_h
@data
end
def youtube_id
@data['youtubeID']
end
def enrich_with_youtube youtube_item
@data['views'] = youtube_item['views']
days_available = Date.today - youtube_item['published']
@data['monthlyViews'] = @data['views'] * 365.0 / days_available / 12
end
end
では、何が達成されたのでしょうか?リファクタリングは多くの場合、コードサイズを縮小しますが、この場合は26行から54行にほぼ倍増しています。他の条件が同じであれば、コードが少ない方が良いでしょう。しかしここでは、懸念事項を分離することで得られるモジュール性の向上の方が、サイズ増加の価値があると私は考えます。これも、教育的な(つまり、おもちゃの)例ではその点が不明瞭になる可能性があるところです。26行のコードは理解しやすいですが、このスタイルで2600行のコードについて話している場合、モジュール性の価値はコードサイズの増加をはるかに上回ります。そして、通常、より大きなコードベースでこのようなことを行うと、重複を排除することでコードサイズを削減する機会が増えるため、そのような増加ははるかに小さくなります。
ここでは、コーディネータ、ドメインオブジェクト、ゲートウェイ、接続の4種類のオブジェクトで終了していることに気づかれるでしょう。これは責任の一般的な配置ですが、依存関係のレイアウト方法には、ケースによって妥当なバリエーションがあります。責任と依存関係の最適な配置は、特定のニーズによって異なります。頻繁に変更する必要があるコードは、めったに変更しないコード、または単に異なる理由で変更するコードから分離する必要があります。広く再利用されるコードは、特定のケースでのみ使用されるコードに依存すべきではありません。これらの推進要因は状況によって異なり、依存関係のパターンを決定します。
一般的な変更の1つは、ドメインオブジェクトとゲートウェイ間の依存関係を逆にすることです。ゲートウェイをマッパーに変換します。これにより、ドメインオブジェクトはそれがどのように設定されるかとは無関係になります。ただし、マッパーはドメインオブジェクトについて知っており、その内部にアクセスできるようになるというコストがかかります。ドメインオブジェクトが多くのコンテキストで使用される場合は、これは貴重な配置になる可能性があります。
別の変更としては、接続の呼び出しコードをコーディネータからゲートウェイに移すことが考えられます。これにより、コーディネータは簡素化されますが、ゲートウェイはやや複雑になります。これが良いアイデアかどうかは、コーディネータが複雑になりすぎているか、多くのコーディネータが同じゲートウェイを使用し、接続の設定に重複したコードが発生しているかによって異なります。
また、特に呼び出し元がゲートウェイオブジェクトの場合、接続の動作の一部を呼び出し元に移動する可能性が高いと考えています。ゲートウェイは必要なデータを知っているので、呼び出しのパラメータにパーツのリストを供給する必要があります。しかし、これはlist_videos
を呼び出す他のクライアントがある場合にのみ問題となるため、その日まで待つ傾向があります。
ケースの詳細に関係なく重要なのは、関係するオブジェクトの役割に一貫した命名ポリシーを持つことです。パターン名をコードに含めるべきではないと言う人がいるのを耳にすることがありますが、私は同意しません。多くの場合、パターン名は異なる要素が果たす役割を伝えるのに役立つため、その機会を無駄にするのは愚かなことです。チーム内では、コードには共通のパターンが表示され、命名はそのことを反映する必要があります。「ゲートウェイ」は、P of EAAでゲートウェイパターンを考案したことに続いて使用しています。ここでは外部システムへの生のリンクを示すために「接続」を使用しており、今後の執筆でもその規則を使用する予定です。この命名規則は普遍的なものではなく、私の命名規則を使用することで私のプライドは心地よく膨らむでしょうが、重要なのはどの命名規則を使用するべきかではなく、何らかの規則を選択することです。
メソッドをこのようにオブジェクトのグループに分割すると、テストへの影響について自然な疑問が生じます。ビデオサービスの元のメソッドには単体テストがありましたが、3つの新しいクラスのテストも書くべきでしょうか?既存のテストが動作を十分にカバーしている場合は、すぐに追加する必要はないと思います。より多くの動作を追加するにつれて、より多くのテストを追加する必要があり、この動作が新しいオブジェクトに追加される場合は、新しいテストがそれらに焦点を当てます。時間が経つと、現在ビデオサービスを対象とするテストの一部は場違いに見えるようになり、移動する必要があるかもしれません。しかし、これらはすべて将来のことなので、将来対処すべきです。
テストで特に注意すべき点は、YouTube接続に追加したスタブの使用です。このようなスタブは簡単に手に負えなくなってしまうことが多く、単純な本番コードの変更が多くのテストの更新につながるため、変更を遅らせる可能性があります。ここでは、テストコードの重複に注意し、本番コードの重複と同じくらい真剣に取り組むことが重要です。
テストダブルの構成に関するこのような考え方は、サービスオブジェクトの組み立てというより広い問題につながります。単一のサービスオブジェクトから3つのサービスオブジェクトとドメインエンティティ(Evans分類を使用)に動作を分割したので、サービスオブジェクトをインスタンス化、構成、および組み立てする方法に関する自然な疑問が生じます。現在、ビデオサービスは依存関係に対して直接これを行っていますが、これは大規模なシステムでは簡単に手に負えなくなります。この複雑さを処理するために、サービスロケータと依存関係注入などのテクニックを使用するのが一般的です。今はそれについては話しませんが、続編の記事のトピックになるかもしれません。
この例では、オブジェクト指向スタイルの方が関数型スタイルよりもはるかに精通しているため、オブジェクトを多用しています。しかし、責任の基本的な分割は同じであると予想されますが、クラスとメソッドではなく、関数(または名前空間)によって境界が設定されます。その他の詳細が変更されます。ビデオオブジェクトはデータ構造になり、それを拡張すると、その場で変更するのではなく、新しいデータ構造が作成されます。関数型スタイルでこれを見ると、興味深い記事になるでしょう。
リファクタリングは、一連の小さな動作を維持する変換によってコードを変更する特定の方法です。単にコードを移動するだけではありません。
最後に、リファクタリングに関する重要な一般的な点を再強調したいと思います。リファクタリングは、コードベースの再構築に使用する用語ではありません。これは、一連の非常に小さな動作を維持する変更を適用するアプローチを具体的に意味します。ここでは、すぐに削除する予定のコードを意図的に導入したいくつかの例を示しました。これは、動作を維持する小さなステップを踏むためです。
ここでは、小さなステップを踏むことで、何も壊さずにデバッグを回避するため、より速く進むことができるという点を強調しています。ほとんどの人はこれを直感的に理解できないと思いますが、ケント・ベックが最初にリファクタリングの方法を示してくれたとき、私もそうでした。しかし、すぐにそれがいかに効果的であるかを発見しました。
重要な改訂
2015年2月17日:初版公開