ポリモーフィズムとReflectionでランタイム環境からクラスにアクセスしてみた

# はじめに

こんにちは。

この記事では初めての現場に配属されてから、任された初開発業務に対して、
どのような悩みがあって、それをどのように解決したのかについて記録を兼ねて書いていこうと思います。

### 開発環境
- 言語：**JAVA 1.5**
- フレームワーク：seaser2
- DB：PostgreSQL

### この記事で使う用語

- **セクション**：診療情報の中で、**情報の枠**を意味します

例）患者情報、処方情報など（Javaではclassを意味）
- **項目**：実際の**診療情報のデータ**を意味します

例）患者情報の患者名・性別、処方情報の症状・服薬指導など（Javaではfieldを意味）

# プロジェクトの概要

当時、自分は病院向けの電子カルテパッケージを開発するチームに参加していました。

HL7 FHIRという国際規格に合わせて、オンライン紹介状の機能を改修するプロジェクトを行うことになりました。

HL7 FHIRは患者の診療情報を複数のセクションに分けている構造になっていたため、
紹介状に記載されている内容の中で、**印刷したいセクションを選択して、必要なデータだけ印刷する機能を実装**するのが、自分の担当でした。

印刷機能には次のような要求事項がありました。

### 要求事項

- 医師が**印刷したい診療情報のセクションと項目の選択ができる**こと
  - 診療情報を印刷するとき、すべての情報を出す必要はないため
- **他の機能に影響がないように**印刷機能を**実装**すること
  - 紹介状印刷のため、紹介状のレイアウトや保存などのロジックの修正は禁止

上記の要求事項に対する自分の初設計・開発業務が始まりました。

# 設計・製造

業務は基本設計を行ってから、その設計をもとに実装をしました。

## 最初は基本設計から

実装をする前に、基本設計をしなければならなかったため、
どのように設計をするか自分なりに案を出して、チームメンバーにレビューしてもらいました。

### 案１（誤った設計）

最初に出した案は最もシンプルな設計でした。
- **紹介状にボタンなどを追加**し、医師が**紹介状を作成するとき、印刷したい診療情報を選択**する

このように設計を行い、レビューを受けてましたが、たくさんの指摘をもらい、
その中でも、重要なポイントは次のようなところでした。

- 医師が**紹介状を印刷するたびに、出したいセクションを選択するのはUXが悪い**
- 案１は**紹介状のレイアウトと保存ロジックに影響**がある

自分は、
> **ボタンを追加するぐらいは問題なさそう。。**

と思ったところが、それは大きな勘違いでした。
実際には**紹介状のロジック全般的に影響を与える誤った設計**になってしまいました。

このように、設計書についていろいろ指摘を受けてから、
改めて、紹介状周りの既存設計とコードを見直しました。

### 案２（採用）

既存のロジックに影響がないようにするため、
**印刷したい診療情報のセクションや項目を外部で設けて、印刷時にその状態を読み込んで、印刷する**ように設計をした方がいいと思いました。

そのため、Utilテーブルに印刷に関する情報を保存する方向で設計を進めました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/5f2eae5e-172a-843e-7db2-d1feb9075a02.png)

その後、案２の設計が採用され、それをもとに開発を進めました。

## 開発スタート

実際に製造を進めながらどのような問題があって、それをどのように解決したのかを共有したいと思います。

### 困難にぶつかる - ランタイム環境でのDTOのフィールドアクセス

製造をする前から、困難にぶつかってしまいました。

それは、ランタイム環境でどうすれば、印刷したい項目をDTOから取り出して印刷をするか？でした。

最初に考えた方法は各DTOごとに、メソッドを定義し、そこで、**分岐で印刷したい項目と一致するフィールドにアクセス**する方法でした。

```java
public String print(content) {
    // contentは印刷する項目（データ）
    if (フィールド１.equals(content)) {
      return getフィールド１();
    } else if (フィールド２.equals(content)) {
      return getフィールド２();
    }
    // ... その他、フィールドに関する else if文
}
```

しかし、この方法はすべてのDTOにメソッドを追加しないといけないし、**フィールド分の分岐処理が必要になる問題**がありました。
また、項目やセクションが追加されるたびに、ソースコードの追加が必要になるため、**メンテナンスも困難**である問題があります。

そのため、if文を使わずに、印刷したい項目のデータだけを取り出す方法があるか、
ネットで検索したり、チームメンバに質問をするなどいろいろ調べてみました。

幸いに長い経歴を持っているチームメンバーの方から**Reflection**を使えば、分岐なしでフィールドのデータの取得ができると教えていただけいました。

その情報をもとにReflectionについて調査を行いました。

### Reflectionでフィールドにアクセスができた！！

**Reflectionの特徴**はいろいろありましたが、
その中で、自分が注目したのは次のような特徴でした。.

> **特定のクラスのメタデータをランタイム環境で動的にアクセスができる**

ここで、メタデータは`Class` `Field` `Constructor` `Method`などを意味します。

この特徴を活用して、フィールドから値を取るロジックを実装しました。

```java
public class FhirDto {

  // ... field

  public String print(List<String> contents) throws NoSuchFieldException, IllegalAccessException {

        // クラスのメタデータにアクセスできる Class オブジェクトを取得
        Class clazz = this.getClass();

        StringBuilder sb = new StringBuilder();
        // contentsはユーザーが印刷したいフィールドリスト
        for (String content : contents) {
            // contentと一致するクラスのフィールドを取得
            Field field = clazz.getDeclaredField(content);

            // 該当フィールドにアクセスできるように設定（privateの場合必要） 
            field.setAccessible(true);

            // フィールドから値を取得し、StringBuilderに追加
            sb.append(field.get(this));
        }
        return sb.toString();
    }

    // ... その他、ロジック
}
```

上記のコードは自分の`Class`情報を**clazz**という変数に保存し、パラメータで渡された、印刷項目を書く変数名と比較を行ってます。
その後、contentと一致するフィールドの値を`StringBuilder`に格納して返却をします。

これで、分岐を使って変数名と印刷する項目を比較する手間はなくなり、**動的でフィールドにアクセスできる**ようになりました。

### ポリモーフィズムで印刷処理を抽象化

ここで、また別の問題が発生しました。

それは、**印刷の処理の重複**が生じることです。

HL7 FHIR規格には複数のセクションが存在します。
これは、紹介状を作成・印刷するときも、複数のセクションが存在することになり、
そのため、**各セクションに印刷処理が重複**することを意味します。

このような、重複を解決するためには処理を`抽象化`する必要がありました。

幸いに各セクションは診療情報の一つという共通点を持っていたため、この**診療情報という概念で抽象化**を進めました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/7180fc2e-8dcf-b81f-e910-a8da6b11673c.png)

各セクション（DTO）に対する**親クラスを定義し、そのクラスに印刷処理を定義**することで、
重複を解決することができました。

```java
public class FhirPrintBase extends {既存の親クラス} {

    protected FhirPrintBase() {
        // インスタンス化を防止
        // 印刷処理は継承したDTOのみ、使える
    }

    public String print(List<String> contents) throws NoSuchFieldException, IllegalAccessException {

        Class<?> clazz = this.getClass();

        StringBuilder sb = new StringBuilder();
        for (String content : contents) {
            Field field = clazz.getDeclaredField(content);
            field.setAccessible(true);
            sb.append(field.get(this));
        }
        return sb.toString();
    }
}

public class FhirDto extends FhirPrintBase {
  // 印刷処理は削除
}
```
診療情報の印刷処理を`FhirPrintBase`という**クラスに定義**し、コンストラクタを`protected`で定義することで、
`FhirPrintBase`クラスの**インスタンス化を防止**し、FhirPrintBaseクラスを**継承したDTOのみ、印刷処理を使える**ようにしました。

継承構成は次のように変更されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/f5e200c4-d71d-c875-e082-dfa398ff8181.png)

このように設計することで、既存ロジックに影響を与えずに、重複をなくすことができました。

### もし、JAVA 8を使っていたら？？？

このプロジェクトはJAVAのバージョンが 1.5バージョンで、かなり古いプロジェクトでした。
重複を解決するために、継承を使いましたが、今見るとこの構成はほかの問題がありそうです。

- **多重継承の限界**
  - 継承は多重継承ができないため、上記のように継承が深くなりました（継承のdepthが増えた）。こうなると、コードの可読性が低下すると思います

もし、JAVA 8バージョンを使っていたら、おそらく次のように**インタフェースで重複を解決**することができたはずです。

```java
public interface FhirPrintBase {

    default String print(List<String> contents) throws NoSuchFieldException, IllegalAccessException {

        Class<?> clazz = this.getClass();

        StringBuilder sb = new StringBuilder();
        for (String content : contents) {
            Field field = clazz.getDeclaredField(content);
            
            field.setAccessible(true);
            
            sb.append(field.get(this));
        }
        return sb.toString();
    }
}

public class FhirDto implements FhirPrintBase {}
```

JAVA８で追加されたインタフェースの`default method`で**印刷処理を抽象化**しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/af5e02bc-df04-46ee-4506-3f6cc6712970.png)


クラスをインタフェースに変えることで、印刷機能だけを分離することができました。

また、今後機能が追加されてもインタフェースで設計すれば機能ごとに一つのオブジェクトに明確に分離することができるので、コードの可読性が向上されると思います。

### クライアントから各セクションのデータを取り出す

では、最後にクライアントから印刷のリクエストが来たら診療情報で印刷するセクションを探し、その項目を印刷する処理を次のように作成しました。

```java
public class Client {

    // 印刷するセクションをループさせ、
    // sectionPrintのパラメータでsectionデータとそのセクションで印刷したい項目が渡される
    public static String sectionPrint(List<? extends FhirPrintBase> sections, List<String> contents) throws Exception {

        StringBuilder sb = new StringBuilder();
        for (FhirPrintBase section : sections) {
            sb.append(section.print(contents));
        }
        return sb.toString();
    }
}
```

パラメータの**sectionsのデータタイプ**を`List<? extends FhirPrintBase>`に絞って、`FhirPrintBase`**を継承したクラスのみ、渡されるように**しました。

また、FhirPrintBaseを継承したDTOは**親クラスのprintメソッド**を使って自分が持っているデータの中、印刷対象のデータを抽出し、変換するようになりました。

# おわりに

最後までご覧いただき、ありがとうございました。

この記事で書いたプロジェクトは自分が配属してから、初めて任された開発でした。
（この業務前には、主に総合テストをやってました。）

確かに、自分が勉強した自由な環境とな異なり、色んな制限事項や要求事項があることが分かりました。

このようないろいろ制限された環境での開発は初めてでしたが、
現場ではこのような場面とよく出会う気がしました。

また、限られたリソースと環境で期間に合わせて最もいい結果を出すことが大事だということを感じたプロジェクトでした。

これからも、知識や経験を伸ばして、どんな状況でもいいものが作れるように頑張っていきます。