♻️

読みやすく保守しやすい Request Handler の構造

に公開

はじめに

ソースコードを書くうえで品質を意識するのは大切です。

多くの技術書や記事が品質が高いソースコードの知見を紹介していますが、コードレビューの指摘やソフトウェアエンジニア内での雑談として共有されるだけで体系的に残っていない知見も少なくありません。

この記事では、私が読みやすく保守しやすいと考えている Request Handler の構造を紹介します。あわせて、その Request Handler の構造が、なぜ読みやすく保守しやすいのかについても説明します。

読みやすく保守しやすい Request Handler の構造

まずはじめに、読みやすく保守しやすい Request Handler の構造を紹介します。

読みやすく保守しやすい Request Handler の構造、以下のシンプルな処理の流れになっていると考えます。

  1. 入力値の検証や変換
  2. ビジネスロジックの呼び出し
  3. 戻り値の作成

読みやすく保守しやすいと考える理由

提案した構造で書かれた Request Handler は、1. 入力値の検証や変換3. 戻り値の作成でビジネスロジックの呼び出し処理が含まれていたらコードレビューで発見され指摘されやすく複雑化しにくいと考えます。

1. 入力値の検証や変換3. 戻り値の作成の処理は様々な Request Handler で利用できるように、共通の関数として実装することが多いです。

共通の関数として定義された1. 入力値の検証や変換3. 戻り値の作成の処理にビジネスロジックの呼び出し処理が含まれている場合、読みづらく保守がやりづらいです。

共通の関数として定義された1. 入力値の検証や変換3. 戻り値の作成の処理にビジネスロジックの呼び出し処理が含まれることで読みづらく保守がやりづらい原因としては、分岐の追加による処理の複雑化が考えられます。1. 入力値の検証や変換3. 戻り値の作成の処理にビジネスロジックの呼び出し処理が含まれている場合、ある Request Handler では必要だが別の Request Handler では必要ないという問題が発生するのは想像に難くないです。その対応として1. 入力値の検証や変換3. 戻り値の作成の処理に分岐をいれると思います。様々な Request Handler に対応するために分岐を入れていき、やがて1. 入力値の検証や変換3. 戻り値の作成の処理が複雑になっていきます。

提案した構造では、1. 入力値の検証や変換3. 戻り値の作成でビジネスロジックの呼び出し処理が含まれていたら違和感を覚えると思います。なぜならビジネスロジックの呼び出し処理は、2. ビジネスロジックの呼び出しで行うからです。

読みやすく保守しやすい Request Handler の例

提案した構造が具体的にどのようなソースコードになるのかを、ユーザーの一覧取得の処理を用いて紹介します。具体的なソースコードの例では、Golang[1] と Protocol Buffers[2] を利用します。

ユーザーの一覧取得の Request Handler を ListUsers として、ユーザーの一覧取得の仕様は以下のとおりとします。

  • user id は必ず指定しなければならない。
  • user id で絞り込みをすることができる
  • user の一覧を返す

提案した構造を用いた ListUsers の実装は以下のとおりです。

func (s *UserService) ListUsers(
	ctx context.Context, req *userpb.ListUsersRequest,
) (*userpb.ListUsersResponse, error) {
	// 1. 入力値の検証や変換

	if req == nil {
		return &userpb.ListUsersResponse{}, nil
	}

	userIDs := make(uuid.UUIDs, 0, len(req.UserIds))
	for _, v := range req.UserIds {
		userID, err := uuid.Parse(v)
		if err != nil {
			return nil, err
		}
		userIDs = append(userIDs, userID)
	}

	// 2. ビジネスロジックの呼び出し

	// ユーザーを取得する
	users, err := s.userService.ListUsers(ctx, userIDs)
	if err != nil {
		return nil, err
	}

	// ユーザーが所属しているグループを取得する
	groups, err := s.groupService.ListGroupByUserIDs(userIDs)
	if err != nil {
		return nil, err
	}

	groupMap := make(map[uuid.UUID][]*Group)
	for _, group := range groups {
		for _, userID := range group.UserIDs {
			groupMap[userID] = append(groupMap[userID], group)
		}
	}

	// 3. 戻り値の作成

	pbGroupsMap := make(map[string][]*userpb.Group)
	for userID, gs := range groupMap {
		pbGroups := make([]*userpb.Group, 0, len(gs))
		for _, g := range gs {
			pbGroups = append(pbGroups, &userpb.Group{
				Id:   g.ID.String(),
				Name: g.Name,
			})
		}
		pbGroupsMap[userID.String()] = pbGroups
	}

	pbUsers := make([]*userpb.User, 0, len(users))
	for _, v := range users {
		pbUsers = append(pbUsers, &userpb.User{
			Id:     v.ID.String(),
			Name:   v.Name,
			Groups: pbGroupsMap[v.ID.String()],
		})
	}

	return &userpb.ListUsersResponse{Users: pbUsers}, nil
}

まとめ

私が読みやすく保守しやすい Request Handler の構造を1つ紹介しました。私はこの Request Handler の構造を意識して書くことにより、意識していなかったときと比べてきれいな Request Handler を書けるようになりました。みなさんも Request Handler を書く際に、この Request Handler の構造を意識してみてください。

KNOWLEDGE WORK Blog Sprint、明日9/20の執筆者はフロントエンドエンジニアの deepblue_will さんです。 お楽しみに!

Appendix

「読みやすく保守しやすい Request Handler の例」で利用する、ユーザーの一覧取得の ProtoBuf 定義は以下のようになります。
syntax = "proto3";

package user.v1;

option go_package = "example.com/user/gen/user/v1;userpb";

message User {
  string id = 1;
  string name = 2;
  repeated Group groups = 3;
}

message Group {
    string id = 1;
    string name = 2;
}

message ListUsersRequest {
  repeated string user_ids = 1;
}

message ListUsersResponse {
  repeated User users = 1;
}

service UserService {
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}
脚注
  1. https://go.dev/ ↩︎

  2. https://protobuf.dev/ ↩︎

株式会社ナレッジワーク

Discussion