リファクタリング： Javaでブラックジャックを作る

追記:
@k73i55no5さんより、より良いリファクタリング案をコメントいただきました。ありがとうございます。詳しくはコメントをご覧下さい。
クラス・メンバ構成（構造）のリファクタリングだけしています。
処理コード（内装、実装）のリファクタリングには着手していません。
（ここまで追記）

@yuta-yoshinaga さんが投稿された「[Javaでブラックジャックを作る](https://qiita.com/yuta-yoshinaga/items/0a5976daa61fd69ec66d)」のソースコードを拝見しました。
セッター・ゲッターだらけでカプセル化が破壊されていて、仕事をしていないクラスもあり、ちょっと辛辣なコメントを書いてしまいました。
コメントだけでは何なので、私なりにリファクタリングしてみました。
まだ見直す個所はありますが、何か参考になることがあれば幸いです。

クラス図：

```plantuml
@startuml
skinparam nodesep 80
skinparam ranksep 80

+class BlackJack {
    -final Player player;
    -final Dealer dealer;

    +BlackJack(Player player, Dealer dealer)
    +void play()
    +void showJudge()
    +Player judgeWinner()
    +static void main(String[] args)
}

+abstract class Player {
    +final String name
    #final List<Card> cards
    #boolean stand = false;

    +Player()
    +Player(String name)
    +void reset()
    +void holdCard(Card card)
    ~abstract boolean play(CardDealer dealer)
    +int calcScore()
    +boolean isBlackJack()
    +boolean isBust()
    +void show()
    +void showCards()
}

+class CUIPlayer {
    - Scanner sc
    + boolean play(CardDealer dealer)
}

+class Dealer {
    +void dealCard(Player player)
    +boolean play(CardDealer dealer)
    +void show()
}

+interface CardDealer {
    +void dealCard(Player player)
}

+class Deck {
    -final ArrayList<Card> cards
    ~void reset()
    +void shuffle()
    +Card drowCard()
}

enum Suit {
    SPADE
    CLOBBER
    HEART
    DIAMOND
}

+class Card {
    +final Suit suit
    +final int rank
    -boolean visible

    +void faceUp()
    +void faceDown()
    +String toString()
}

BlackJack "1" --> "1" CUIPlayer
BlackJack "1" --> "1" Dealer
Dealer "1" -> "1" Deck
Dealer --|> Player
Dealer ..|> CardDealer
CUIPlayer --|> Player
Player "0..1" o-> "*" Card
CardDealer "1" <- "2" Player
Deck "0..1" o--> "*" Card
Card "*" o--> "1" Suit
@enduml
```

```java:BlackJack.java
public class BlackJack {
    private final Player player;
    private final Dealer dealer;

    public BlackJack() {
        this(new CUIPlayer(), new Dealer());
    }

    public BlackJack(Player player, Dealer dealer) {
        this.player = player;
        this.dealer = dealer;
    }

    public void play() {
        player.reset();
        dealer.reset();
        for (int i = 0; i < 2; i++) {
            dealer.dealCard(player);
            dealer.dealCard(dealer);
        }
        dealer.show();
        if (player.play(dealer)) {
            dealer.play(dealer);
            showJudge();
        }
    }

    public void showJudge() {
        dealer.show();
        player.show();
        Player winner = judgeWinner();
        System.out.println("----------");
        if (winner == player) {
            System.out.println("You are the winner.");
        } else if (winner == dealer) {
            System.out.println("It is your loss.");
        } else {
            System.out.println("It is a draw.");
        }
    }

    public Player judgeWinner() {
        if (player.isBust()) {
            return dealer;
        } else  if (dealer.isBust()) {
            return player;
        } else if (player.isBlackJack() && !dealer.isBlackJack()) {
            return player;
        } else if (dealer.isBlackJack() && !player.isBlackJack()) {
            return dealer;
        }
        int diff = player.calcScore() - dealer.calcScore();
        if (diff > 0) {
            return player;
        } else if (diff < 0) {
            return dealer;
        } else {
            return null;
        }
    }

    public static void main(String[] args) {
        BlackJack blackjack = new BlackJack();
        while (true) {
            blackjack.play();
            blackjack.player.play(null);
        }
    }
}
```

```java:Player.java
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;

abstract public class Player {
    public final String name;
    protected final List<Card> cards = new ArrayList<Card>();
    protected boolean stand = false;

    public Player() {
        this("Player");
    }

    public Player(String name) {
        this.name = name;
    }

    public void reset() {
        cards.clear();
        stand = false;
    }

    public void holdCard(Card card) {
        cards.add(card);
    }

    abstract boolean play(CardDealer dealer);

    public int calcScore() {
        int score = 0;
        boolean hasAce = false;
        for (Card card: cards) {
            score += card.rank < 10 ? card.rank : 10;
            if (card.rank == 1) {
                hasAce = true;
            }
        }
        if (score <= 11 && hasAce) {
            score += 10;
        }
        return score;
    }

    public boolean isBlackJack() {
        return cards.size() == 2 && calcScore() == 21;
    }

    public boolean isBust() {
        return calcScore() > 21;
    }

    public void show() {
        System.out.println("----------");
        showCards();
        System.out.println(name + "'s score: " + calcScore());
    }

    public void showCards() {
        System.out.println(name + "'s card: " + cards.stream().map(Object::toString).collect(Collectors.joining(", ")));
    }
}
```

```java:CUIPlayer.java
import java.util.Scanner;

public class CUIPlayer extends Player {
    private Scanner sc = new Scanner(System.in);

    public boolean play(CardDealer dealer) {
        while (!isBust() || dealer == null) {
            if (!stand) {
                show();
            }
            System.out.println("----------");
            System.out.println("Please enter a command.");
            System.out.println("  q: quit");
            System.out.println("  r: restart");
            if (!stand && dealer != null) {
                System.out.println("  h: hit");
                System.out.println("  s: stand");
            }
            System.out.print("? ");
            String inputStr = sc.nextLine();
            switch (inputStr) {
                case "q":
                case "quit":
                    System.out.println("bye.");
                    sc.close();
                    System.exit(0);
                    break;
                case "r":
                case "reset":
                    return false;
                case "h":
                case "hit":
                    if (!stand && dealer != null) {
                        dealer.dealCard(this);
                    }
                    break;
                case "s":
                case "stand":
                    stand = true;
                    return true;
                default:
                    System.out.println("Unsupported command.");
                    break;
            }
        }
        stand = true;
        return true;
    }
}
```

```java:Dealer.java
public class Dealer extends Player implements CardDealer {
    private final Deck deck;

    public Dealer() {
        this(new Deck());
    }

    public Dealer(Deck deck) {
        super("Dealer");
        this.deck = deck;
    }

    public void dealCard(Player player) {
        player.holdCard(deck.drowCard());
    }

    public boolean play(CardDealer dealer) {
        while (calcScore() < 17) {
            dealer.dealCard(this);
        }
        stand = true;
        return true;
    }

    @Override
    public void show() {
        if (stand || cards.size() != 2) {
            super.show();
        } else {
            System.out.println("----------");
            cards.get(1).faceDown();
            showCards();
            cards.get(1).faceUp();
        }
    }
}
```

```java:CardDealer.java
public interface CardDealer {
    public void dealCard(Player player);
}
```

```java:Deck.java
import java.util.ArrayList;
import java.util.Collections;

public class Deck {
    private final ArrayList<Card> cards = new ArrayList<Card>();

    public Deck() {
        reset();
    }

    void reset() {
        cards.clear();
        for (Suit suit: Suit.values()) {
            for (int rank = 1; rank <= 13; rank++) {
                cards.add(new Card(suit, rank));
            }
        }
        shuffle();
    }

    public void shuffle() {
        Collections.shuffle(cards);
    }

    public Card drowCard() {
        if (cards.size() == 0) {
            reset();
        }
        return cards.remove(0);
    }
}
```

```java:Card.java
enum Suit {
    SPADE, CLOBBER, HEART, DIAMOND;
}

public class Card {
    public static String[] RANK = {
        "", "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"
    };

    public final Suit suit;
    public final int rank;
    private boolean visible;

    public Card(Suit suit, int rank) {
        this.suit = suit;
        this.rank = rank;
        faceUp();
    }

    public void faceUp() {
        this.visible = true;
    }

    public void faceDown() {
        this.visible = false;
    }

    @Override
    public String toString() {
        if (visible) {
            return suit.name() + ' ' + RANK[rank];
        } else {
            return "???";
        }
    }
}
```