🙆‍♀️

Go言語~リフレクション活用事例~

2022/09/28に公開

こんちには!
LIFULLエンジニアの吉永です。

本日は私の普段の業務でメイン言語として利用しているGo言語のリフレクションを活用した実装事例を紹介したいと思います。
※本記事はQiitaにて一度公開済みなのですが、ZennとQiitaで記事の読まれ方の違いを把握したいので、Zennにも投稿させていただきます。いずれどちらかに集約したいと思っています。

リフレクションは結構取り扱いが難しく、自分もあまり積極的に過去の実装で採用したことはなかったので、どんな場面で使う?のイメージが曖昧でした。

この記事では私と同じように、リフレクションの存在は知ってるけど、具体的にどんな場面で使うの?っていう一つの事例として参考になれば幸いです。

リフレクションとは?

リフレクション自体はGo言語特有の機能ではなく、現在一般的に利用されているプログラミング言語ではほぼ対応している機能となります。

https://ja.wikipedia.org/wiki/リフレクション_(情報工学)

上記で提示したWikipediaにも記載がありますが、プログラム実行中にプログラム自身で構造を読み取って、読み取った結果から何かしらの出力を行ったり、構造を書き換えたりする技術の事です。

Go言語だと、リフレクションの解説だけで1冊の本が出版されてたりもしてます。

https://www.amazon.co.jp/Go言語reflectハンドブック-技術の泉シリーズ(NextPublishing)-千葉-大二郎/dp/4844379321

一般的にはコードの可読性が悪くなる為、積極的に実装する場面は業務アプリケーション系の開発ではあまりないかと思いますが、抽象度高い状態で実装したいバリデーションチェック処理やフレームワークを開発する際には活用した方が良い場合もあります。

リフレクション活用事例

LIFULL HOME'Sでは物件を検索する際の条件にこだわり条件というものを設定することが可能です。
image.png

上記のこだわり条件に合致した物件情報をエンドユーザーへお知らせメール配信やLINEでPUSH配信する際に、こだわり条件に応じて、カテゴライズしたラベルを表示したいという要件が出たことがあり、ひとまずバックエンドを大きく変えることなく試してみたかったことから、BFF層にあたるアプリケーションでバックエンドからのレスポンスにカテゴライズしたラベルを表示するようにしました。

カテゴライズしたラベルとは、例えばこだわり条件で「2階以上」、「南向き」のいずれかが選択されていて、検索結果の物件がそのこだわり条件に合致している場合は「日当たりを良くしたい」というラベルを表示することです。

これらを実現する為にリフレクションを一部で活用しました。

物件検索APIのレスポンス

実際のレスポンスとは違いますが、イメージが湧きやすいように似た構造のレスポンス値を掲載しておきます。

{
  "Results": [
    {
      "name": "物件種別テスト3 3",
      "id": "1202810028307",
      "rent": "9.0万円",
      "management_fee": "1,000円",
      "address": "東京都千代田区麹町1-4-4",
      "traffic": "地下鉄半蔵門線 半蔵門駅 3b出口より徒歩2分",
      "details_url": "https://www.homes.co.jp/chintai/b-xxx/",
      "building_structure_type": "鉄筋系",
      "building_type": "マンション",
      "discerning_condition": [
        "バス・トイレ別",
        "オートロック",
        "トランクルーム"
      ]
    }
  ]
}

物件検索APIの戻り値を格納する構造体

上記のJsonをシリアライズして格納する構造体です。
下記の例ではCategoryNamesという拡張用のメンバーが追加されています。
これ以降で、APIレスポンス値からCategoryNamesへの値を設定していきます。

type RealestateArticle struct {
	Name                  string   `json:"name"`
	Id                    string   `json:"id"`
	Rent                  string   `json:"rent"`
	ManagementFee         string   `json:"management_fee"`
	Address               string   `json:"address"`
	Traffic               string   `json:"traffic"`
	DetailsURL            string   `json:"details_url"`
	BuildingStructureType string   `json:"building_structure_type"`
	BuildingType          string   `json:"building_type"`
	DiscerningCondition   []string `json:"discerning_condition"`
	CategoryNames         []string
}

カテゴリー名とこだわり条件との対応表のYAML

リフレクションと組み合わせて、カテゴリー名とAPIレスポンス値との対応表の定義をしたYAMLになります。

category_names:
  - &category_name1 "日当たりを良くしたい"
  - &category_name2 "騒音を気にせず暮らしたい"
  - &category_name3 "セキュリティを高めたい"
  - &category_name4 "快適に料理をしたい"
  - &category_name5 "水回りを便利にしたい"
  - &category_name6 "楽に買い物したい"
result_keys:
  building_structure_type:
    "鉄筋系":
      categorys:
        - *category_name2
  building_type:
    "マンション":
      categorys:
        - *category_name2
  discerning_condition:
    "バス・トイレ別":
      categorys:
        - *category_name5
    "洗面所独立": 
      categorys:
        - *category_name5
    "コンロ二口以上":
      categorys:
        - *category_name4
    "システムキッチン":
      categorys:
        - *category_name4
    "カウンターキッチン":
      categorys:
        - *category_name4
    "室内洗濯機置場":
      categorys:
        - *category_name5
    "オートロック":
      categorys:
        - *category_name3
    "TVモニタ付インターホン":
      categorys:
        - *category_name3
    "2階以上":
      categorys:
        - *category_name1
        - *category_name3
    "最上階":
      categorys:
        - *category_name2
    "南向き":
      categorys:
        - *category_name1
    "スーパー 800m以内":
      categorys:
        - *category_name4
        - *category_name6
    "コンビニ 800m以内":
      categorys:
        - *category_name6

リフレクションを活用した実装

// GetCategoryNamesForList 物件情報からカテゴリー名のリストを取得する
func GetCategoryNamesForList(data RealestateArticle) (categoryNames []string, err error) {
	// こだわり条件マスタ情報を取得
	var yaml map[string]interface{}
	err = ReadYaml("category_name_for_result_keys.yaml", &yaml)
	if err != nil {
		return
	}
	// マスタ情報からこだわり条件部分のみを抽出
	resultKeys := yaml["result_keys"].(map[string]interface{})
	// RealestateArticleの構造体情報からタグ一覧とフィールドを取得する
	sType := reflect.TypeOf(data)
	sValue := reflect.ValueOf(data)
	// 構造体のメンバーを順に走査しながら該当するカテゴリー名を取得していく
	for i := 0; i < sType.NumField(); i++ {
		field := sType.Field(i)
		tagName := field.Tag.Get("json")
		if value, ok := resultKeys[tagName]; ok {
			dataValue := sValue.FieldByName(field.Name)
			switch dataValue.Kind() {
			case reflect.String:
				categoryNames = append(categoryNames, getCategoryForMap(value, dataValue.Interface().(string))...)
			case reflect.Slice:
				for _, index := range dataValue.Interface().([]string) {
					categoryNames = append(categoryNames, getCategoryForMap(value, index)...)
				}
			}
		}
	}

	return
}

// YAMLファイル読み込み
func ReadYaml(path string, out interface{}) (err error) {
	var yamlByte []byte
	// ファイル読み込み
	yamlByte, err = ioutil.ReadFile(path)
	if err == nil {
		// データ変換
		if err = yaml.Unmarshal(yamlByte, out); err == nil {
			return
		}
	}
	return
}

// getCategoryForMap mapから該当するカテゴリー名を取得する
func getCategoryForMap(value interface{}, index string) (categoryNames []string) {
	if val, ok := value.(map[interface{}]interface{})[index]; ok {
		for _, value := range val.(map[string]interface{})["categorys"].([]interface{}) {
			categoryNames = append(categoryNames, value.(string))
		}
	}
	return
}

上記のGetCategoryNamesForList内でRealestateArticle構造体からリフレクションを用いて、jsonタグ名を取得、フィールドの型情報からフィールド内に格納されている値をインプットとして、YAML上から対応する部分を取得して、カテゴリ名を取得することができています。
※実際には、カテゴリ名が重複して出力配列に入ってしまうので、重複を除去したりする処理も本実装上は存在しますが、ここでは触れたい部分の本質ではないので割愛しています。

この実装の魅力的な部分は、上記のビジネスロジックを変えることなく、YAML定義を変更するだけで今後のカテゴリ名のメンテナンスができるところだと思います。

※フレームワークを用いるとよくある、バリデーションチェックのルールをYAMLで管理している実装は上記のようなリフレクションを活用して実装していることも多いと思います

ちなみに、もっと汎用的にする場合は、現状構造体のメンバ変数の型がstringかstringの配列かの2パターンしかリフレクションの下記switch文で網羅していないので、intやfloatなどにも対応する場合caseを増やす必要があります。

			dataValue := sValue.FieldByName(field.Name)
			switch dataValue.Kind() {
			case reflect.String:
				categoryNames = append(categoryNames, getCategoryForMap(value, dataValue.Interface().(string))...)
			case reflect.Slice:
				for _, index := range dataValue.Interface().([]string) {
					categoryNames = append(categoryNames, getCategoryForMap(value, index)...)
				}
			}

上記処理を経て最終的に出力されるjson

{
  "Results": [
    {
      "name": "物件種別テスト3 3",
      "id": "1202810028307",
      "rent": "9.0万円",
      "management_fee": "1,000円",
      "address": "東京都千代田区麹町1-4-4",
      "traffic": "地下鉄半蔵門線 半蔵門駅 3b出口より徒歩2分",
      "details_url": "https://www.homes.co.jp/chintai/b-xxx/",
      "building_structure_type": "鉄筋系",
      "building_type": "マンション",
      "discerning_condition": [
        "バス・トイレ別",
        "オートロック",
        "トランクルーム"
      ],
      "CategoryNames": [
        "騒音を気にせず暮らしたい",
        "水回りを便利にしたい",
        "セキュリティを高めたい"
      ],
    }
  ]
}

GetCategoryNamesForListメソッドで取得したCategoryNamesにcategory_name_for_result_keys.yamlで定義された、こだわり条件と一致したカテゴリ名が最終的なBFF層から返却するjson内に入ります。

まとめ

リフレクションを使うとビジネスロジックを抽象度高く実装することができ、その恩恵として設定ファイルの変更のみでよくなり、今後の運用やメンテが楽になるケースもあり得るというイメージが湧いたでしょうか?

正直、フレームワークを自作するような機会でもない限り、あまりリフレクションを活用する場面はないかなーと個人的には思っていましたが、検討してみると、業務アプリケーションでも採用する場面は多くはなくても、一部ユースケースでは有効であろうということが分かりました!

最後までご覧いただき、ありがとうございました。
それではまた次の記事でお会いしましょう。

Discussion