SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

Refactor Diary 1 (Java: APIレスポンス解析)

Refactor Diary 1 (Java: APIレスポンス解析)

例:ユーザロールチェック

ユーザ情報はAPIにより以下のようなものが取れるものとする

// API Response Data Model
public class User {
    private String userName;
    private List<RoleEnum> roles;
    // 他にも複数のフィールドがあるものとする
    // Getter,Setter等省略
}

リファクタ前

public class UserRoleValidator {

    private final Logger logger = LoggerFactory.getLogger(UserRoleValidator.class);
    private final ApiClient apiClient;  // OkHttpClient ラッパークラスとする
    private static final List<RoleEnum> REQUIRED_ANY_ROLES = Lists.of(RoleEnum.Admin, RoleEnum.Manage);

    UserRoleValidator(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    // APi実行~レスポンスボディ変換~ロールチェックまで全部入り
    public void execute() {
        OkHttp.Response response = apiClient.getUser(userName);
        if (!response.isSuccessful()) {
            LOGGER.error("Response Status Code: " + response.code());
            if (Objects.nonNull(response.body())) {
                LOGGER.error("Error Response Contents: " + response.body().string());
            }
            LOGGER.error("message");
            throw new IOException("message");
        }
        User user = new Gson().fromJson(response.body().string(), User.class)
        List<Role> roles = Objects.isNull(user.getRoles()) ? List.of() : user.getRoles();
        if (roles.isEmpty() || 
                REQUIRED_ANY_ROLES.stream().anyMatch(r -> roles.contains(r))) {
            LOGGER.error("message");
            throw new InsufficientRoleException("message");
        }
    }
}

何が問題か

  • (レスポンスに対して共通で行うような) 共通処理が入り込み、流用できない
  • ロールチェックのパターン網羅するテストを書くにあたり、全テストで apiClient をmockし、返すresponse body data を指定する必要がある等、テスト自体が見にくくなる

リファクタ後

public class UserRoleValidator {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserRoleValidator.class);
    private final ApiClient apiClient; 

    UserRoleValidator(ApiClient apiClient) {
        this.apiClient = apiClient;
    }

    public void execute(String userName) {
        var apiResponse = new DataStoreApiResponse(apiClient.getUser(userName));
        if (apiResponse.isSuccessful()) {
            LOGGER.error("message");
            throw new IOException("message");
        }
        validateUserRole(apiResponse.deserialize());
    }

    public void validateUserRole(User user) {
        if (RequiredRoles.isSatisfy(user.getRoles())) {
            LOGGER.error("message");
            throw new InsufficientRoleException("message");
        }
    }
}

// API Response Data Model
public class User {
    private String userName;
    private List<RoleEnum> roles;
    // Getter,Setter等省略
}

// レスポンスに対する共通処理の集約
class DataStoreApiResponse {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(DataStoreApiResponse.class);
    private final isSuccessful;
    
    DataStoreApiResponse(OkHttp.Response response) {
        if (!response.isSuccessful()) {
            LOGGER.error("Response Status Code: " + response.code());
            if (Objects.nonNull(response.body())) {
                LOGGER.error("Error Response Contents: " + response.body().string());
            }
            isSuccessful = false;
            return;
        }
        isSuccessful = true;
    }

    boolean isSuccessfull() {
        return isSuccessful;
    }

    int getStatusCode() {
        return response.code();
    }

    <T> T deserialize(Class<T> cls) {
        return new Gson().fromJson(response.body().string(), cls)
    }
}

// 知識の確立
public class RequiredRoles {

    private static final List<RoleEnum> REQUIRED_ANY_ROLES = Lists.of(RoleEnum.Admin, RoleEnum.Manage);

    boolean isSatisfy(User user) {
        List<Role> roles = Objects.isNull(user.getRoles()) ? List.of() : user.getRoles();
        if (roles.isEmpty()) {
            return false;
        }
        return REQUIRED_ANY_ROLES.stream().anyMatch(r -> roles.contains(r));
    }
}

先にどういうところでどういうテストが必要になるかを考えて、テストしやすい部品を作る

  • 不良を作りこみやすいような重要な箇所(上記例ならロールチェック処理)を見極め
  • そこに対して、容易にテストができるよう部品に分ける

それにより、複数の重要な知識が1か所に混在し、結果として見通しが悪くなるようなことが防げる。テストも見やすくなり、どういったケースがあり得るのか等テストから読み取りが容易になる