Taigaです。今年は「良いコード悪いコードで学ぶ設計入門」の輪読会に参加したり、アプリケーションの保守性について勉強した一年だったと思います。
そのまとめとして、じゃんけんプログラムを丁寧にクラス化して保守性を向上した過程をお見せします!
今回は、コンソールに1~3の数字を入力すると結果を出力するような形式のじゃんけんプログラムを考えました。
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        Random random = new Random();
        
        System.out.println("出す手を決めて下さい");
        System.out.println("1: グー");
        System.out.println("2: チョキ");
        System.out.println("3: パー");
        
        System.out.print("あなたの手 > ");
        int player_hand = sc.nextInt();
        
        int opponent_hand = random.nextInt(3) + 1;
        System.out.println("相手の手 > " + opponent_hand);
        
        if(player_hand == 1){
            if(opponent_hand == 1) System.out.println("あいこ");
            if(opponent_hand == 2) System.out.println("勝ち!");
            if(opponent_hand == 3) System.out.println("負け...");
        }else if(player_hand == 2){
            if(opponent_hand == 1) System.out.println("負け...");
            if(opponent_hand == 2) System.out.println("あいこ");
            if(opponent_hand == 3) System.out.println("勝ち!");
        }else if(player_hand == 3){
            if(opponent_hand == 1) System.out.println("勝ち!");
            if(opponent_hand == 2) System.out.println("負け...");
            if(opponent_hand == 3) System.out.println("あいこ");
        }else{
            System.out.println("1, 2, 3のどれかを入力して下さい");
        }
    }
}
書いてみて気になるのは、勝敗を判定するロジックです。ではどんなところに問題があるでしょうか?
個人的にまず気になるのは、player_hand, opponent_handがint型になっている所です。
player_hand == 1 かつ opponent_hand == 2 の時が勝ちなのか負けなのか分かりにくいです。
また、ロジックがMainクラスにベタ書きになっている所も気になります。
mainクラスでは実行順だけわかるようになっているのが分かりやすいんじゃないかと思います。
じゃんけんの細かいルールについて、mainクラスが知っている必要はなさそうです。
Handクラスを作ります。
Handクラスはidを持っていて、fight_againstメソッドで他の手と戦った結果を出力します。
public interface Hand {
    public int id();
    public String name();
    public String fight_against(Hand opponent_hand);
}
public class Gu implements Hand {
    @Override
    public int id(){return 1;}
    @Override
    public String name(){return "グー";}
    @Override
    public String fight_against(Hand opponent_hand) {
        if(opponent_hand.id() == 1) return "あいこ";
        if(opponent_hand.id() == 2) return "勝ち!";
        if(opponent_hand.id() == 3) return "負け...";
        return "error: invalid hand";
    }
}
public class Main {
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);
        final Random random = new Random();
        
        final Map<Integer, Hand> handMap = Map.of(1, new Gu(), 2, new Choki(), 3, new Pa());
        
        final Hand player_hand = handMap.get(sc.nextInt());
        final Hand opponent_hand = handMap.get(random.nextInt(3) + 1);
        
        final String result = player_hand.fight_against(opponent_hand);
    }
}
Mapを使ってグー/チョキ/パーのインスタンスをそれぞれ1, 2, 3と紐付けることで、player_hand, opponent_handをHand型で表現することができました。
これで、mainクラスは細かいルールについて知る必要がなくなりました。
ただ、Handクラスの内部ではint型のidによる判断が残ってしまっています。
idで判断するのではなく、enumで判断するようにします。
public enum HandType {
    GU,
    CHOKI,
    PA,
}
public interface Hand {
    public HandType type();
    public String name();
    public String fight_against(Hand opponent_hand);
}
public class Gu implements Hand {
    @Override
    public HandType type(){return HandType.GU;}
    @Override
    public String name(){return "グー";}
    @Override
    public String fight_against(Hand opponent_hand) {
        if(opponent_hand.type() == HandType.GU) return "あいこ";
        if(opponent_hand.type() == HandType.CHOKI) return "勝ち!";
        if(opponent_hand.type() == HandType.Pa) return "負け...";
        return "error: invalid hand";
    }
}
ぱっと見でわかりやすい実装になってきました。
しかしまだ気になる点があります。Handクラスが勝敗を判断している所です。
Handクラスがじゃんけんゲームのルールまで担ってしまっているのは微妙な感じがします。
例えば3人以上でのじゃんけんに対応しよう!となった時、Handクラスが勝敗を判断していると修正が大変になります。
Handクラスは他の手との相性を返すようにして、帰ってきた相性をもとに勝ち負けを判断するJudgeクラスを作るのが良さそうです。
public enum Affinity {
    GOOD,
    EVEN,
    BAD,
}
public class Gu implements Hand {
    @Override
    public HandType type(){return HandType.GU;}
    @Override
    public String name(){return "グー";}
    @Override
    public Affinity fight_against(Hand opponent_hand) throws Exception {
        if(opponent_hand.type() == HandType.GU) return Affinity.EVEN;
        if(opponent_hand.type() == HandType.CHOKI) Affinity.GOOD;
        if(opponent_hand.type() == HandType.PA) return Affinity.BAD;
        throw new Exception("error: invalid hand");
    }
}
public class Judge {
    static String rule_2players(Hand player_hand, Hand opponent_hand){
        try{
            final Affinity affinity = player_hand.fight_against(opponent_hand);
            if (affinity == Affinity.GOOD)  return "勝ち!";
            if (affinity == Affinity.EVEN)  return "あいこ";
            if (affinity == Affinity.BAD)   return "負け...";
            return "error: invalid affinity";
        }catch (Exception e){
            return e.toString();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        final Scanner sc = new Scanner(System.in);
        final Random random = new Random();
        
        final Map<Integer, Hand> handMap = Map.of(1, new Gu(), 2, new Choki(), 3, new Pa());
        
        final Hand player_hand = handMap.get(sc.nextInt());
        final Hand opponent_hand = handMap.get(random.nextInt(3) + 1);
        
        final String result = Judge.rule_2players(player_hand, opponent_hand);
    }
}
Handクラスは相性を知っているだけで、勝ち負けについてはJudgeクラスが判断するようになりました。
もし3人以上のじゃんけんを実装しよう!となっても、Judgeクラスに新たな判断ロジックを書けば実装できそうです。
保守性の高いコードを書いていると、後で機能追加をするときに書き換える部分が少なくて済んだり、バグの発生を未然に防ぐ事ができたりするようです。
またこれは人によるかもしれませんが、きちんと整理されたコードの方が心理的安全性も高くなる気がします。
この記事が保守性について考えるきっかけになっていたら嬉しいです。最後まで読んで頂いてありがとうございました。
可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More