1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-08-31

はじめに

こんにちは。

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

開発環境

  • 言語:JAVA 1.5
  • フレームワーク:seaser2
  • DB:PostgreSQL

この記事で使う用語

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

例)患者情報、処方情報など(Javaではclassを意味)

  • 項目:実際の診療情報のデータを意味します

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

プロジェクトの概要

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

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

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

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

要求事項

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

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

設計・製造

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

最初は基本設計から

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

案1(誤った設計)

最初に出した案は最もシンプルな設計でした。

  • 紹介状にボタンなどを追加し、医師が紹介状を作成するとき、印刷したい診療情報を選択する

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

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

自分は、

ボタンを追加するぐらいは問題なさそう。。

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

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

案2(採用)

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

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

image.png

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

開発スタート

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 {}

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

image.png

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

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

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

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

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メソッドを使って自分が持っているデータの中、印刷対象のデータを抽出し、変換するようになりました。

おわりに

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

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

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

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

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?