みなさん、こんにちは。 この間、久々にJavaを触ってSystem.debugと書いてコンパイルエラーになった戸塚です。 新しいネタを探したのですがなかなか見つからなかったので、今回は前回の記事の続きにしようと思います。
前回の記事で、テストクラスでテストデータの作成をサポートするクラスを紹介しましたが、時間の関係上1クラスにごちゃっと実装していたので、今回リファクタリングしたものを紹介致します。
前回はTestDataLoaderというクラスに全ての処理を実装していましたが、今回以下のようなクラス構成に変更しました ① TestDataLoader.cls
前回作成したクラスを変更したもので、静的リソースからCSVデータを取得してテストデータを作成する責務を持つクラスです。 前回のクラスではパースする処理を隠蔽していましたが、今回のクラスではパーサーを指定可能にしました。
② CSVReader.cls
受け取ったCSVデータの文字列を元に、指定されたパーサーを使用してレコードのリストを作成する責務を持つクラスです。
③ CSVParser.cls
CSVデータをパースするクラスの抽象クラスです。 下記の④のSimpleCSVParserと⑤のStrictCSVParserクラスが当該クラスを継承してます。 そして②のCSVReaderでは、下記の④や⑤の派生先のクラスに直接依存した実装とするのではなく、汎用的にするため当該クラスを使用する実装しています。
④ SimpleCSVParser.cls
項目値に行や項目の区切り文字と括り文字が含まれていることは考慮せず、CSVデータをシンプルにパースする責務を持つクラスです。
⑤ StrictCSVParser.cls
項目値に行や項目の区切り文字と括り文字が含まれていることを考慮し、CSVデータを厳密にパースする責務を持つクラスです。
⑥ CSVConverter.cls
②のCSVReaderで項目ベースに分割された文字列から、SObject型のリストに変換する責務を持つクラスです。
使用方法
CSVデータを厳密にパースする場合、下記のように実行します。 前回との違いは、パースの内容を呼出側で指定できるよう、loadメソッドの引数にCSVReaderのインスタンスを受け取るようにしたことです(ここ違和感ある・・・) ※事前に静的リソースにCSVファイルを登録している前提です。
ただ単純にカンマ区切りでデータ登録する場合は下記のように、SimpleCSVParserのインスタンスをCSVReaderのコンストラクタに渡してください。 ※もしくは、loadメソッドをオーバーロードしているので、引数に静的リソース名のみを渡してください。
また、もしCSVデータを独自の方式でパースしたいのであれば、CSVParserを継承したクラスを作成し、上記のようにCSVReaderのコンストラクタに渡せば実現は可能かと思います。
まとめ
前回1クラスで実装していたものを、責務毎に別クラスに切り出しました。 その結果、処理自体の見通しが良くなったのに加えて、隠蔽していた処理が外に出たことにより、呼出し側でCSVデータをどうパースするか指定可能になるという自由が生まれました(大袈裟?) ※どうして最初からクラスを分割していなかったの?というご指摘はなしの方向で・・・
それと同時に、今回作成したCSVReader等はユニットテストがしやすいクラスになります。 例えば、CSVReaderのテストコードを書こうとした際に、関連しているCSVParser(実際にはCSVParserの派生クラス)がどう実装されているのかなんて意識したくありません。 CSVParserのinとoutさえ把握しておればよく、CSVReaderのテストに注力したいです。
この場合、CSVParserを継承したMockクラスを用意して、CSVReaderのコンストラクタにMockクラスのインスタンスを渡すことで、CSVParserのoutをいかようにも制御できるのでテストがしやすくなると思います(バグ潰しへ注力可能 + CSVParserの実装完了を待たずしてテスト可能 + 上手くいくと工数削減)
話がそれますが、CSVReaderはコンストラクタでCSVParserと関連付けしていますが、例えばTestDataLoaderクラスがCSVConverterクラスに依存しているような場合はどうすればよいのでしょうか?FactoryMethodパターンを適用?なんか違うな・・・。CSVConverterもloadメソッドの引数でもらう?う~ん・・・。実はApexの裏にJavaEEとか隠れてて、いずれはApexでDI(CDI)ができるようになったりして・・・
そして、前回も今回もテストデータを作成する用途で使用するクラスとして説明してきましたが、今回クラスを分割したことにより、②~⑥のクラスは別の用途でも使用できるようになります。 例えば、あるオブジェクトの添付ファイルにCSVファイルが登録されていて、そのデータを元に別オブジェクトのレコードを作成する等、色々と再利用できる場面があると思います。
また責務毎にクラスを切り分ける事により、何か仕様変更が発生した場合の影響範囲の限定化等につながる場合もありますし、更にエラーハンドリングやログ出力を適切に実装すると問題発生時の原因特定の容易さにもつながります。
最後に
私が書いてきた記事は全てプログラムに関連した記事ですが、Salesforceに限って言えば極力開発すべきではないと思っています。 それはよく言われるように、開発することにより発生する下記リスクをお客様に受け入れて頂く必要があるからです。
- Salesforceの機能追加や機能拡張に伴うバージョンアップにより、お客様が有効に活用する機会を閉ざしてしまうリスク
- Salesforceのセキュリティ向上に伴うバージョンアップにより、以前は動作していたが動作しなくなる場合があり、お客様に改修コストの負担が発生するリスク
上記を考慮しても補って有り余るメリットがあったり、リリース後も上記リスクを受け入れられる場合は開発してもよいと思います。
ただその場合、バグを出さない(ように努める)、非効率な実装は避ける、ガバナ制限を意識する等はもちろんのこと、スケジュールが厳しくなるとおざなりになりがちな保守性や拡張性等を可能な限り高めたものをお客様に提供できるようにしたいですね。 でないと、リリース後に仕様変更が発生して改修する際に、影響範囲やテスト工数が増大するリスクとなり、お客様が負担するコストの増大や費用対効果が下がるという可能性が高まり、下手すると最終的に自社に対する顧客満足度を下げるという要因の一つになり得ると思います。
今回作成したプログラム
時間の関係上、テストも手抜きですし、依然としてリファクタリングの余地はまだまだある状態であることをご了承頂ければ、下記プログラムを自由に使用して頂いて構いません。 但し、下記プログラムに関して発生する一切の責任は負いかねますのであしからず。
※最初はSalesforceのパッケージにしていたのですが、インストール先の組織で動作検証していたところSObjectのリストをinsertするところで権限に関するエラーになってしまい、どうにも対処できなかったためこちらに直接UPします。あのエラーってどう対処すればよいのだろう・・・
① TestDataLoader.cls(新)
前回作成したクラスを変更したもので、静的リソースからCSVデータを取得してテストデータを作成する責務を持つクラスです。 前回のクラスではパースする処理を隠蔽していましたが、今回のクラスではパーサーを指定可能にしました。
② CSVReader.cls
受け取ったCSVデータの文字列を元に、指定されたパーサーを使用してレコードのリストを作成する責務を持つクラスです。
上記クラスのテストクラスです。
③ CSVParser.cls
CSVデータをパースするクラスの抽象クラスです。 下記の④のSimpleCSVParserと⑤のStrictCSVParserクラスが当該クラスを継承してます。 そして②のCSVReaderでは、下記の④や⑤の派生先のクラスに直接依存した実装とするのではなく、汎用的にするため当該クラスを使用する実装しています。
④ SimpleCSVParser.cls
項目値に行や項目の区切り文字が含まれていることは考慮せず、CSVデータをシンプルにパースする責務を持つクラスです。
⑤ StrictCSVParser.cls
項目値に行や項目の区切り文字が含まれていることを考慮し、CSVデータを厳密にパースする責務を持つクラスです。
⑥ CSVConverter.cls
②のCSVReaderで項目ベースに分割された文字列から、SObject型のリストに変換する責務を持つクラスです。
上記クラスのテストクラスです
⑦ TestDataLoader.cls(旧)