テーマ:
はじめに

みなさんはじめまして。
アメーバ事業本部ゲーム部門でJavaエンジニアをやってる朝倉です。

突然ですが、みなさんはオブジェクト指向できてますか?こんな風に考えていませんか?
・Javaでプログラミングしてるから
・UMLで設計しているから
・継承やインターフェースを使ってるから

残念ながら、これらはオブジェクト指向ではありません。

オブジェクト指向とは、
責務をもったオブジェクトがお互いに協調してシステムを構築する
ことです。

頭では理解できても、実際にコードに適応することはなかなか難しいですよね。
楽器やスポーツのように、オブジェクト指向も練習しないと身に着けることができません。

そこで今回は、オブジェクト指向エクササイズという、オブジェクト指向の練習方法を紹介します。


オブジェクト指向エクササイズって?

「オブジェクト指向エクササイズ」とは、
書籍「ThoughtWorksアンソロジー」の「第5章 オブジェクト指向エクササイズ」で紹介されている、
優れたオブジェクト指向設計の原理を自分のものにし、実際に使えるようになるためのエクササイズです。

ほぼ必然的にオブジェクト指向になるコードを書くように強制する9つのルールを、
1000行程度のプロジェクトに適用するオブジェクト思考の練習方法です。
9つのルールは、あくまで練習なので普遍的なルールではありません。

それでは、「9つのルール」を紹介していきます。

ルール1.1つのメソッドにつきインデントは1段落までにすること

みなさんは、どこから手をつけてよいかわからないほどの巨大なメソッドを見たことはありませんか?
巨大なメソッドは、複数の仕事をやっていることが多いです。そのため凝集度が低くなってしまいます。

各メソッドが厳密に1つの仕事を行うように、メソッドごとに制御構造またはコードブロックを1つだけにしましょう。
インデントがなくなるまで、リファクタリング:メソッドの抽出をするだけです。

次のコードにルール1を適応してみます。
    public void method1() {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                if (i == j) {
                    s.append("■");
                } else {
                    s.append("●");
                }
            }
            s.append("\r\n");
        }
        System.out.println(s.toString());
    }

2回メソッドの抽出をして完成です。
    public void method1() {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            appendLine(s, i);
            s.append("\r\n");
        }
        System.out.println(s.toString());
    }

    private void appendLine(StringBuilder s, int i) {
        for (int j = 0; j < 10; j++) {
            appendMark(s, i, j);
        }
    }

    private void appendMark(StringBuilder s, int i, int j) {
        if (i == j) {
            s.append("■");
        } else {
            s.append("●");
        }
    }

ルール2.
else句を使用しないこと

うんざりするほどネストされていたり、スクロールしなければ読めないほどの条件文を見たことありませんか?
さらに既存の条件文に単純に分岐を増やすほうが、適切な解決方法を考えるよりも楽なので修正のたびに条件文はどんどん複雑になっていきます。

簡単な条件文であれば、ガード節や早期リターンに置換えましょう。
また、複雑な条件文は、GoFデザインパターンのStrategyパターンを導入します。
nullチェックの条件文であれば、NullObjectパターンの導入を検討します。

次のコードに早期リターンを使ってみます。
    public String getGrade(int score) {
        if (score > 80) {
            return "A";
        } else {
            if (score > 60) {
                return "B";
            } else {
                return "C";
            }
        }
    }

早期リターンによってelse句がなくなります。
    public String getGrade(int score) {
        if (score > 80) {
            return "A";
        }
        if (score > 60) {
            return "B";
        }
        return "C";
    }

次は、Strategyパターンを使ってみます。
    public class Calculator {
        
        public static final int ADDITION = 1;
        public static final int SUBSTRACTION = 2;
        
        private int type;
        
        public Calculator(int type) {
            this.type = type;
        }
        
        public int calculate(int a, int b) {
            int result = 0;
            if (type == ADDITION) {
                result = a + b;
            } else if (type == SUBSTRACTION) {
                result = a - b;
            }
            return result;
        }
    }


Typeによってアルゴリズムが変わるので条件文がなくなります。
    public class Calculator {
        
        private Type type;
        
        public Calculator(Type type) {
            this.type = type;
        }
        
        public int calculate(int a, int b) {
            int result = type.execute(a, b);
            return result;
        }
    }
    
    public interface Type {
        int execute(int a, int b);
    }
    
    public class Addition implements Type {
        public int execute(int a, int b) {
            return a + b;
        }
    }

    public class Substraction implements Type {
        public int execute(int a, int b) {
            return a - b;
        }
    }

ルール3.すべてのプリミティブ型と文字列型をラップすること

int型などのプリミティブ型は、それだけではなんの意味ももたない単なるスカラ値です。
メソッドの引数がint型の場合、メソッド名や変数名で意図を表現するしかありません。
もしメソッドが「金額」オブジェクトを引数として受け取れば、意図がわかりやすくなりコンパイラのチェックも可能になります。

プリミティブ型はそのまま使用しないで、意味のあるファーストクラスオブジェクトとして扱いまます。
String型もプリミティブ型と同じ扱いをします。
ファーストクラスオブジェクトとすることで関連する操作も、メソッドとしてまとめるやすくなります。

String型のインスタンス変数をファーストクラスオブジェクトにします。
    public class Rule3 {
        private String name;
    }

String形をラップしたクラスを作ると関連する操作もまとめる事ができます。
    public class Rule3 {
        private Name name;
    }
    
    public class Name {
        
        private String value;
        
        // 操作をまとめる
    }

ルール4.1行につきドットは1つまでにすること

1行の中に複数のドットがある場合、その処理は間違った場所で実行されています。
複数のドットは、多くのオブジェクトを知りすぎていてカプセル化に違反している証拠です。

操作できるオブジェクトは、
・オブジェクト自身
・インスタンス変数のオブジェクト
・引数で渡されたオブジェクト
・メソッド内でインスタンス化したオブジェクト
です。
デメテルの法則(最小知識の原則)に従いましょう。

複数のドットがあるのでデメテルの法則に違反しています。
    public class Rule4 {
        
        private User user;
        
        public void method() {
            user.getAddress().method();
        }
    }
    
    public class User {
        
        private Address address;
        
        public Address getAddress() {
            return address;
        }
    }
    
    public class Address {
        
        public void method() {
            
        }
    }


デメテルの法則に従うように修正するとドットが1つになります。
    public class Rule4 {
        
        private User user;
        
        public void method() {
            user.method();
        }
    }
    
    public class User {
        
        private Address address;
        
        public void method() {
            address.method();
        }
    }
    
    public class Address {
        
        public void method() {
            
        }
    }

ルール5.名前を省略しないこと

同じ単語を何度も入力しているのなら、コードの重複が発生しています。
メソッド名が長くなっているのなら、複数の責務を持っています。

1つか2つの単語だけを扱うようにして省略しないようにしましょう。

名前が長くなっているメソッドは複数の責務を持っています。
    public void updateAndPlay() {
        
    }

メソッドを分割して責務をはっきりさせます。
    public void update() {
        
    }
    public void play() {
        
    }

ルール6.すべてのエンティティを小さくすること

50行を超えるクラスは、複数の仕事をしています。
10ファイルを超えるパッケージは、複数の目的があります。

50行を超えるクラス、10ファイルを超えるパッケージは作らないようにします。
クラスは単一責任の原則(SRP)、パッケージは閉鎖性共通の原則(CCP)に従いましょう。

ルール7.1つのクラスにつきインスタンス変数は2つまでにすること

オブジェクトのインスタンス変数が増えるほど、そのクラスの凝集度が低下していきます。
巨大なオブジェクトは、重複コードが発生し管理不可能なほど複雑になっていきます。

1つのインスタンス変数を管理するクラスと2つの独立した変数を調整するクラスの2種類のみ作成します。
インスタンス変数に関わる振る舞いは、自然とインスタンス変数と同じオブジェクトに作成されます。

インスタンス変数が3つあるので凝集度が低いです。
    public class User {
        private String name;
        private String phone;
        private String email;
    }

名前と連絡先のオブジェクトを抽出します。
    public class User {
        private Name name;
        private Contact contact;
    }
    
    public class Name {
        private String value;
    }
    
    public class Contact {
        private String phone;
        private String email;
    }

ルール8.ファーストクラスコレクションを使用すること

このルールは、ルール3と非常に似ています。
コレクションは非常に便利ですが、単なるプリミティブな型と同じで意図を表すことができません。

コレクションを独自のクラスでラップします。
コレクションをラップしたクラスには、多くのインスタンス変数を持たせないようにします。
フィルタなどのコレクションに関する振る舞いをまとめることができます。

コレクションをそのまま使っています。
    public class Rule8 {
        
        private List<User> users;
    }

コレクションをラップしたクラスを作って振る舞いもまとめます。
    public class Rule8 {
        
        private Users users;
    }
    
    public class Users {
        
        private List<User> users;
        
        // 操作をまとめる
    }

ルール9.Getter、Setter、プロパティを使用しないこと

インスタンス変数の値が他のオブジェクトから簡単に取得できるようになっていると、
振る舞いはそのインスタンス変数とは別の場所に実装されてしまいます。
その結果、コードの重複が発生してしまいます。

public変数やgetter,setterなどのアクセッサメソッドの使用は禁止です。
「求めるな、命じよ」を意識しましょう。

次の例は、amountに関する振る舞いがオブジェクトの外に出てしまっています。
    public class Rule9 {
        
        public void method(Item item) {
            int value = item.getAmount();
            item.setAmount(value + 1);
        }
    }
    
    public class Item {
        
        private int amount;
        
        public void setAmount(int amount) {
            this.amount = amount;
        }
        
        public int getAmount() {
            return amount;
        }
    }

アクセッサメソッドを取り除くことによって、amountに関する振る舞いがオブジェクトの中に納まります。
    public class Rule9 {
        
        public void method(Item item) {
            item.addAmount(1);
        }
    }
    
    public class Item {
        
        private int amount;
        
        public void addAmount(int value) {
            amount = amount + 1;
        }
    }

まとめ
オブジェクト指向エクササイズいかがでしたでしょうか?

9つのルールに完全に従うのはきついですよね?
特に、ルール4,7,9を完全に適応するのはかなり難しいと思います。

あきらめずにオブジェクト指向エクササイズに取り組んでいけば、
オブジェクト指向の理解が深まっていくのが実感できると思います。

オブジェクト指向エクササイズで、オブジェクト指向をマスタしましょう!!

最後まで読んでいただき、ありがとうございました。

いいね!した人  |  リブログ(0)

サイバーエージェント 公式エンジニアブログさんの読者になろう

ブログの更新情報が受け取れて、アクセスが簡単になります