業務ロジックから条件分岐をできるだけなくしてコードの見通しをよくする設計

# 目的
タイトルの通り業務ロジックから条件分岐を排除してコードの見通しを改善することで保守性を向上させたい
今回はSpringBoot・Mybatisの構成を想定したコードになっている

サンプルコード

https://github.com/aka-ao/FormSample

# 業務要件
- ユーザは2つのステータスがある
    - 一般ユーザ
    - プライムユーザ
- 一般ユーザは商品購入の際に送料が一律500円かかる
- プレミアユーザは商品購入の際の送料が無料となる
- テーブル定義は以下の通り

|  カラム名  |  型  | コメント | 備考 |
| ---- | ---- | ---- | ----|
|  userId   |  int | ユーザID | autoincrement |
|  userName  | varchar(64) | ユーザ名 | - |
|  status  | int | ステータス | 1: 一般ユーザ, 2: プライムユーザ |
---
# 改善前 条件分岐による実装
サービスクラスにステータスで条件分岐して一般会員の場合に送料をプラスするような処理を実装する

```java
public class User {
    private Integer userId;
    private String name;
    private String status;

    public int buyProduct(String name, Integer price) {
        int totalPrice = price;
        if(status.equals(1)) {
            totalPrice += 500;
        }

        return totalPrice;
    }
}
```

## 改善前 条件分岐による実装メリデメ
### メリット
コードの記述量が少なくて済む
複雑すぎない間は処理を追いやすい

### デメリット
もし会員のステータスがもっと増えてきたり表には出していない内部的なステータスが細分化された場合に条件分岐だらけになる  
例えば一般ユーザの中でも月1万円以上購入してくれているユーザは送料が半額になる
購入時に購入金額に応じてポイント付与を行う機能を追加する。プライムユーザは還元率１％だけど自社クレカ発行ユーザは２％還元する

すべてif文で表現すると業務ロジックのコードがif分だらけになって何の処理を行っている関数かわからなくなる

# 改善後 継承による実装
User抽象クラスを継承したPrimeUserクラスとNormalUserクラスを作成して商品購入メソッドを抽象化しておく
DBからユーザを取得したときにステータスに応じて生成するクラスのインスタンスを上記のどちらにするか決めておく
2つの実装クラスではそれぞれ商品購入メソッドの実装を行う

- 抽象クラス
```java
public abstract User {
    private Integer userId;
    private String name;
    private UserStatus status;

    public abstract Integer buyProduct(String name, Integer price);
}
```

- プライム会員
```java
public class PrimeUser extends User{

    private final Integer postage = 0;

    public PrimeUser(Integer userId, String userName, UserStatus status) {
        super(userId, userName, status);
    }

    @Override
    public Integer buyProduct(String name, Integer price) {
        Integer totalPrice = price + postage;
        return  totalPrice;
    }
}
```
- 一般会員
```java
public class NormalUser extends User {
    private final Integer postage = 500;

    public NormalUser(Integer userId, String userName, UserStatus status) {
        super(userId, userName, status);
    }

    @Override
    public Integer buyProduct(String name, Integer price) {
        Integer totalPrice = price + postage;
        return totalPrice;
    }
}
```

## 改善後 継承による実装 メリデメ

### メリット
条件分岐が複雑になってきてもその都度、実装クラスを増やして対応することでコードが複雑になりにくい

### デメリット
最初からコードの量が多い
開発メンバーで共通認識をしていないとコードが追いづらくなる
継承を多用するので気を付けないと実行時エラーになる

## 案2 継承による実装 ポイント
- ステータスはEnumで表現しクラスを生成するときはFactoryメソッドを使う。Userクラスをnewしたいときは直接実装クラスのインスタンスを生成するのではなくFactoryメソッドを通じてインスタンスを生成するルールを作る
  - Enumクラス
```java
public enum UserStatus {
    一般会員(1),
    プライム会員(2);

    private Integer statusCode;

    UserStatus(Integer statusCode) {
        this.statusCode = statusCode;
    }

    public static UserStatus valueOf(Integer statusCode) {
        UserStatus[] statuses = values();
        UserStatus status = null;
        for (UserStatus userStatus: statuses
             ) {
            if(userStatus.getStatusCode().equals(statusCode)) {
                status = userStatus;
                break;
            }
        }
        return status;
    }

    /**
     * UserStatusに定義されているステータスをMap形式で受け取る.
     * */
    public static Map<Integer, String> getAllStatus() {
        List<UserStatus> statuses = Arrays.asList(UserStatus.class.getEnumConstants());
        return statuses.stream().collect(
                Collectors.toMap(
                        status -> status.statusCode,
                        status -> status.name()
                )
        );
    }
}
```
  - Factoryクラス
```java
public class UserFactory {
    public static User exec(Integer userId, String userName, UserStatus status) {
        User user = null;
        switch (status) {
            case 一般会員:
                user = new NormalUser(userId, userName, status);
                break;
            case プライム会員:
                user = new PrimeUser(userId, userName, status);
                break;
        }

        return user;
    }
}
```

- DBから取得するときはDTOクラスで受け取り、リポジトリクラスでエンティティに変換してサービスクラスに返す
    - こうしないと抽象クラスのインスタンスを生成できないので実行時エラーになる
    - DBに渡すときも同様でサービスクラスから受け取ったエンティティクラスをDTOに変換してSQLを実行する
    
- Mapperクラス
```java
@Mapper
public interface UserMapper {
    @Delete("delete from user where user_id = #{userId}")
    void deleteById(Integer userId);

    @Delete({
            "<script>",
            "delete FROM user",
            "where user_id in",
            "<foreach item=\"item\" index=\"index\" collection=\"list\" open=\"(\" separator=\",\" close=\")\">",
            "#{item}",
            "</foreach>",
            "</script>"
    })
    void deleteAllById(List<Integer> deleteList);

    @Select(value = {
            "select user_id, name, status from `user`"
    })
    List<UserDto> findAll();

    @Select(value = {
            "select user_id, name, status from `user` where user_id = #{userId}"
    })
    UserDto findById(Integer userId);

    @Insert("insert into `user` (name, status) values (#{name}, #{status})")
    void save(UserDto user);
}

```

- Repositoryクラス
```java
@Repository
public class UserRepository  {

    @Autowired
    UserMapper userMapper;

    public void deleteById(Integer userId) {
        userMapper.deleteById(userId);
    }

    public void deleteAllById(List<Integer> deleteList) {
        userMapper.deleteAllById(deleteList);
    }

    public List<User> findAll() {
        List<User> userList = new ArrayList<>();
        userMapper.findAll().forEach(userDto -> {
            User user = UserFactory.exec(userDto.getUserId(), userDto.getName(), UserStatus.valueOf(userDto.getStatus()));
            userList.add(user);
        });
        return userList;
    }

    public User findById(Integer userId) {
        System.out.println(userId);
        UserDto userDto = userMapper.findById(userId);
        User user = UserFactory.exec(userDto.getUserId(), userDto.getName(), UserStatus.valueOf(userDto.getStatus()));
        return user;
    }

    public void save(User user) {
        userMapper.save(UserDto.transformUserDto(user));
    }
}
```