はじめに
こんにちは。
今回は実務でJava8
の機能であるStreamAPIとラムダ式を活用し、既存のコードをリファクタリングしてみたので、その時の経験を皆さんに共有したいと思います。
最近、Javaの勉強中にStreamAPIとラムダ式で、古いコードをリファクタリングするあるブログの記事を見かけました。
そのブログの事例を今回の自分の課題に積極的に活用してみました。
こういう使い方もあるんだな。。ぐらいで読んでいただければ幸いです。
この記事で紹介される事例は各セクションを説明をするために作成されたものです。
事例1 - メモを職種別にグルーピングする
プロジェクトを進めてみると、多様な情報が混在しているデータの中で特定のキーワードがある情報だけを抽出する場合は頻繁にあると思います。
例えば、ある病院で作成された掲示板のメモの中で、看護師が作成したメモを見たいまたは医師が作成したメモを見たいなど、職種別で作成されたメモを確認したい場面があると想定します。
医師が作成したメモを取得するには、
// すべてのメモが保存されているリスト
List<Memo> memos = XXX.getMemos();
// 医師が作成したメモを保存するリスト
List<Memo> doctorMemos = new ArrayList<>();
// 看護師が作成したメモを保存するリスト
List<Memo> nurseMemos = new ArrayList<>();
for (Memo memo : memos) {
if ("doctor".equals(memo.getJobName())) {
// 作成者が医師の場合、リストに保存
doctorMemos.add(memo);
} else if ("nurse".equals(memo.getJobName())) {
// 作成者が看護師の場合、リストに保存
nurseMemos.add(memo);
}
// 他職種の分岐
// ...
}
上記のように全メモをループさせて、メモの作成者がdoctorであればdoctorMemosに保存することが一番簡単だと思います。
しかし、上記のコードは次のような問題があります。
- 可読性が低下する
- 他の職種のメモを抽出したい場合、その職種分、if文が増える
- 再利用ができない
- 医師が作成したメモを抽出するたびに、同じコードを書かないといけない
これ以外も色んな問題があると思います。
このコードを可読性を高め、再利用できるようにリファクタリングしてみたいと思います。
リファクタリング1 - ラムダ式とStreamAPIを活用
上記の問題をラムダ式を活用し、メソッドのパラメータに渡して解決していきます。
@Test
void logic() {
// すべてのメモが保存されているリスト
List<Memo> memos = XXX.getMemos();
List<Memo> doctorMemos = getDoctorMemos(memos);
List<Memo> nurseMemos = getNurseMemos(memos);
// ...
}
// 医師が作成したメモを取得するメソッド
private List<Memo> getDoctorMemos(List<Memo> memos) {
// 医師が作成したメモを抽出
return getMemos(memos, memo -> "doctor".equals(memo.getJobName()));
}
// 看護師が作成したメモを取得するメソッド
private List<Memo> getNurseMemos(List<Memo> memos) {
// 看護師が作成したメモを抽出
return getMemos(memos, memo -> "nurse".equals(memo.getJobName()));
}
private List<Memo> getMemos(List<Memo> memos, Predicate<Memo> predicate) {
// 各呼び出しもとから渡されたメソッドで作成者をフィルターする
return memos.stream()
.filter(memo -> predicate.test(memo))
.collect(Collectors.toList());
}
まず、職種ごとの分岐はifではなくメソッドで作成します。
ここで、メソッド名をget(職種名)Memosで設定することで、各メソッドの役割が分かるようになります。
こうすれば、ただifで分岐していた最初のコードより可読性が上がると思います。
その後、各メソッドでは役割に合わせて抽出したい職種を返すメソッドをパラメータに渡します。
getMemosメソッドでは受け取ったパラメータでメモをフィルターし、返却してくれます。
しかし、このコードにはまだ、問題点が存在します。
- 同じ機能のメソッドを重複で作成される可能性があります
- プロジェクトに慣れていない人が製造に入る場合、上記のメソッドの存在を分からずに、同じ機能のメソッドを作成する恐れがあります
- もし、ロジックの修正が必要な場合、すべてのメソッドを探し、修正しないといけません
リファクタリング2- ファーストクラスコレクションを活用
リファクタリング1のコードで解決できなかったメソッド重複生成の問題はファーストクラスコレクションで解決できます。
まず、ファーストクラスコレクション(First Class Collection)を簡単に説明すると、
- コレクションをラッピングしているクラス
- ラッピングしたコレクション以外の変数は存在しない
- コレクションとそのコレクションを処理するロジックが同じクラスにある
- 名前のあるコレクション
などがあります。
まず、List<Memo> memos
と、職種ごとメモを取得する処理
を一つのクラスで作成します。
class Memos {
List<Memo> memos;
public Memos(List<Memo> memos) {
this.memos = memos;
}
// 医師が作成したメモを取得するメソッド
public List<Memo> getDoctorMemos() {
return getMemos(memo -> "doctor".equals(memo.getJobName()));
}
// 看護師が作成したメモを取得するメソッド
public List<Memo> getNurseMemos() {
return getMemos(memo -> "nurse".equals(memo.getJobName()));
}
private List<Memo> getMemos(Predicate<Memo> predicate) {
// 各呼び出しもとから渡されたメソッドで作成者をフィルターする
return memos.stream()
.filter(memo -> predicate.test(memo))
.collect(Collectors.toList());
}
}
上記のようにすると、コレクションとロジックが同じクラスで管理されます。
なので、メソッドの重複生成を防止し、メンテナンスもしやすくなります。
@Test
void logic() {
Memos memos = XXX.getMemos();
List<Memo> doctorMemos = memos.getDoctorMemos();
List<Memo> nurseMemos = memos.getNurseMemos();
}
もちろん、doctorMemosとnurseMemosもラッピングし、名前のあるコレクションにすることも可能です。
事例2 - CSVデータをDTOに変換するロジックを抽象化
業務でWEBからアップロードされたcsvファイルのデータをDTOに変換するロジックを作成すると、想定します。
この記事では、Springで使われているアップロードファイルを操作用インタフェースであるMultipartFileで説明します。
CSVファイルをDTOに変換する流れは次のようになります。
- BufferedReaderでCSVファイルを読み込む
- データをループさせる
- 文字列を","で分割し、配列にする
- 配列のデータをDTOに変換する
@Test
void logic(MultipartFile file) throws Exception {
// BufferedReaderでCSVファイルを読み込む
BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream()));
List<CsvDTO> collect =
// データをループさせる
br.lines()
// 文字列を","で分割し、配列にする
.map(str -> str.split(","))
// 配列のデータをDTOに変換する
.map(data -> new CsvDTO(data))
// DTOのリストを返す
.collect(Collectors.toList());
}
class CsvDTO {
private String data1;
public CsvDTO(String[] args) {
this.data1 = args[0];
}
// ...
}
最初は上記のように作成しましたが、いくつかの問題があることに気づきました。
- 同じ機能のメソッドを重複で作成される可能性があります
- ほかの機能でもCSVファイルをDTOに変換する場合同じロジックが重複で作成される
-
変換ロジックの再利用ができない
- 現在、CsvDTOしか対応していないですが、今後、フォーマットの異なるCSVファイルが増えれば各CSVファイルに対応するロジックを新たに作る必要がある
リファクタリング - ジェネリックとラムダ式を活用
まず、共通で使えるConvertクラスを作成し、変換ロジックを作成します。
class CsvToDtoConverter {
static public List<CsvDTO> convert(MultipartFile file) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream()));
return br.lines()
.map(str -> str.split(","))
.map(data -> new CsvDTO(data))
.collect(Collectors.toList());
}
}
こうすることで、CSVをDTOに変換するロジックを抽象化しました。
次は、変換ロジックで実際のデータをDTOに変換するところをパラメータで渡されたラムダ式に修正します。
Functionインタフェースを使って、このconvertメソッドを呼び出す側からDTOへの変換処理を直接設定できるようになります。
class CsvToDtoConverter {
// 返却をList<CsvDTO>からList<T>に修正
static public <T> List<T> convert(MultipartFile file, Function<String[], T> function) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream()));
return br.lines()
.map(str -> str.split(","))
// Function<T, R>を活用し、呼び出す側から変換するDTOを設定できるようにする
.map(data -> function.apply(data))
.collect(Collectors.toList());
}
}
このように変換するDTOを呼び出す側からラムダ式で設定し、そのメソッドをパラメータで渡すと、次のようにCSVフォーマットが異なる場合も対応できるようになります。
また、DTOに変換する前、各CSVのデータ検証などの処理もラムダ式に追加することができます。
@Test
void logic(MultipartFile file) throws Exception {
// CsvDTO_1に変換
List<CsvDTO_1> csvDTO_1 = CsvToDtoConverter.convert(file, args -> new CsvDTO_1(args));
// CsvDTO_2に変換
List<CsvDTO_2> csvDTO_2 = CsvToDtoConverter.convert(file, args -> new CsvDTO_2(args));
}
class CsvDTO_1 {
private String data1;
public CsvDTO_1(String[] args) {
this.data1 = args[0];
}
}
class CsvDTO_2 {
private String data1;
private String data2;
public CsvDTO_2(String[] args) {
this.data1 = args[0];
this.data2 = args[1];
}
}
この記事では、JavaのクラスでConverterを作成しましたが、Springで提供しているConverterを使うこともできます。
まとめ
今回は自分が勉強したJavaのラムダ式を実際の業務で活用した経験を共有しました。
まだ、完璧な書き方ではないですが、このように自分が勉強したことを業務に活かしていくと、もっと成長できるかと思います。
最後までご覧くださりありがとうございました。