狠狠撸

狠狠撸Share a Scribd company logo
オブジェクト指向できていますか?
 真のオブジェクト指向が身に付くコーディング規約




    日本工学院八王子専門学校
         大圖 衛玄
CEDEC 2012講演時のスライドです。
http://cedec.cesa.or.jp/2012/program/PG/C12_P0084.html
自己绍介
                         1992年~1997年
                           某ゲーム会社
                           プログラマ
                           SFC,GB,PS1,N64のゲーム開発経験

                         1998年~現在
                           日本工学院八王子専門学校
    @mozmoz1972            専任講師
                           プログラミング教育を中心に担当




twitterもfacebookも実名です。よかったらフォローしてください。
オブジェクト指向できていますか?
オブジェクト指向
できていますか?
なぜ
オブジェクト指向
できないのか?
手続き型の言语から学习した人がOOPするには大きな発想の転换が必要です。
そのクラスは巨大な1枚岩のコードで作成され???(次に続く)
全ての机能が実装されていた???(次に続く)
人々はそのクラスを「神クラス」と呼んだ。非OOPなコードの特徴です。
Stage1   Stage2   Stage3   Stage4    Stage5




実際にあった伝説のコード。シンプルな2Dゲームなのに1万行ありました。
調べてみると、ステージごとに、まるごとコピペで作成した神クラスが5つ。
1ゼウス2千行、5ゼウスで、合計1万行となっていました。
理想的な
オブジェクト指向の
  世界とは?
小さな大量のオブジェクトが???(次に続く)
お互いメッセージを送りながら协调し复雑なシステムを构筑する。(次に続く)
各クラスは、1つの机能に集中し、最小限のインターフェースで构成されています。
コーディング规约で
        理想的な
      オブジェクト指向の
       世界を目指す

本セッションのゴールです。
命名规则やスペースの数、{}位置などのコーディング规约のお话ではありません。
オブジェクト指向できていますか?
オブジェクト指向
  エクササイズ
        Jeff Bay


ソフトウェア設計を改善する
      9つのステップ
第5章に書かれているJeff Bay氏によるエッセイになります。
読后の衝撃は忘れられません。理想的なオブジェクト指向の姿が理解できました。
Jeff Bayが冒頭で紹介している、7つのコード品質特性です。
Extreme
必然的にオブジェクト指向になってしまう、Extremeなコーディング规约です!
オブジェクト指向できていますか?
メソッドにつきインデントは1段階

 ?1つのメソッドごとに制御文を1つに制限
 ?if、for、whileごとにメソッド化する
 ?制御文の内側をメソッド化する
 ?早期リターンを活用しインデントを浅くする



制御文のネストをなくすルールです。小さく、単純なメソッドを作成するのが目的です。
void Class::method() {       void Class::method() {
   for (外側ループ) {                for (外側ループ) {
     for (内側ループ) {                for (内側ループ) {
       if (条件) {                    method1(…);
         処理;                      }
       }                        }
     }                        }
   }
 }                            void Class::method1(…) {
                                if (!条件)
                                  return;
                                処理;
                              }

                     Before                        After

制御分の内側から順番にメソッド化していきます。
void Class::method() {       void Class::method() {
  for (外側ループ) {                for (外側ループ)
    for (内側ループ) {                method2(…);
      method1();             }
    }
  }                          void Class::method2(…) {
}                              for (内側ループ)
                                 method1(…);
                             }




                    Before                        After
void Class::method() {       void Class::method() {
   for (外側ループ) {                for (外側ループ)
     for (内側ループ) {                method2(…);
       if (条件) {              }
         処理;                  void Class::method2(…) {
       }                        for (内側ループ)
     }                            method1(…);
   }                          }
 }                            void Class::method1(…) {
                                if (!条件)
                                  return;
                                処理;
                              }

                     Before                        After

制御文ごとにメソッド化され、ネストが1段階となりました。
for (int i = 0; i < 5; ++i) {
     if (a[i] == num) {
       return true;
     }
   }
   return false;
                                              Before


   return find(&a[0], &a[5], num) != &a[5];




                                               After

検索などのループは、標準の関数を利用しましょう。STLのfindに変更しました。
totalAge    = 0;
  totalSalary = 0;
  for (int i = 0; i < 5; ++i) {
    totalAge    += ages[i];
    totalSalary += salarys[i];
  }
                                     Before


  totalAge = 0;
  for (int i = 0; i < 5; ++i)
    totalAge += ages[i];
  totalSalary = 0
  for (int i = 0; i < 5; ++i)
    totalSalary += salarys[i];           After

ループの内側で2つのことを扱うと、メソッド化が困難です。ループを分割します。
totalAge = 0;
   for (int i = 0; i < 5; ++i)
     totalAge += ages[i];
   totalSalary = 0;
   for (int i = 0; i < 5; ++i)
     totalSalary += salarys[i];
                                                    Before


   totalAge    = accumulate(&ages[0], &ages[5], 0);
   totalSalary = accumulate(&salarys[0], &salarys[5], 0);




                                                     After

配列の合計はSTLのaccumulate関数で求められます。
totalAge    = accumulate(&ages[0], &ages[5], 0);
  totalSalary = accumulate(&salarys[0], &salarys[5], 0);




                                                     Before


 int Class::totalAge() {
   return accumulate(&ages[0], &ages[5], 0);
 }
 int Class::totalSalary() {
   return accumulate(&salarys[0], &salarys[5], 0);
 }                                                    After

さらにメソッド化をしてみました。
totalAge    = 0;
    totalSalary = 0;
    for (int i = 0; i < 5; ++i) {
      totalAge    += ages[i];
      totalSalary += salarys[i];
    }
                                                      Before


  int Class::totalAge() {
    return accumulate(&ages[0], &ages[5], 0);
  }
  int Class::totalSalary() {
    return accumulate(&salarys[0], &salarys[5], 0);
  }                                                    After

Martin Fowlerの「Split Loop」というリファクタング例になります。
オブジェクト指向できていますか?
else句を使用しないこと

 ?早期リターンを活用する
 ?else if、switchの条件分岐を避ける
 ?Strategy、Stateなどのデザパタを活用
 ?三項演算子(?)を利用する



条件分岐を単純化するのが目的です。
int method() {                int method() {
   int result;                   if (!条件1)
   if (条件1) {                      return 10;
     if (条件2) {                  if (条件2)
       result = 20;                return 20;
     } else {                    return 30;
       result = 30;            }
     }
   } else {
     result = 10;
   }
   return result;
 }

                      Before                    After

左と右のコードは同等の処理を行います。早期リターンを使うと単純になります。
switch (actorType) {            class Actor {
 case PLAYER:                    public:
                                   virtual void update() = 0;
   updatePlayer();
                                 };
   break;
 case ENEMY:
   updateEnemy();                actor->update();
   break;
 case BULLET:
   updateBullet();
   break;
 }


                        Before                          After

「State/Strategyによるタイプコードの置き換え」というリファクタリングです。
int method() {              int method() {
   int result;                 return 条件 ? 20: 30;
   if (条件)                   }
     result = 20;
   else
     result = 30;
   return result;
 }




                    Before                      After

三項演算子(?)は使いすぎると、わかりにくくなりますが、単純なケースでは有用です。
if (x < 0) {
     x = 0;
   } else if (x > 640) {
     x = 640;
   }

                                            Before


   x = max(0, min(640, x));




                                             After

上記のようなif文は、よく見かけますが、STLのmin,max関数で書き直せます。
if (x < 0) {
     x = 0;
   } else if (x > 640) {
     x = 640;
   }

                                                   Before


   x = clamp(x, 0, 640);

 float clamp(float x, float bottom, float top) {
   return max(bottom, min(top, x));
 }
                                                    After

さらにclampという関数を作ってみます。C++であれば、関数テンプレート化しましょう。
オブジェクト指向できていますか?
すべてのプリミティブ型をラップ

  ?int、floatなどの基本型をラップする
  ?stringなども基本型の扱いとする
  ?得点、時間などの変数をクラス化する
  ?すべての状態変数をカプセル化する



すべてをオブジェクト化するのが目的です。intやfloatはオブジェクトではありません。
class Game {                class Game {
 private:                    private:
   int score;                  Score      score;
   int limitTime;              LimitTime time;
 };                          };

                             class Score {
                             private:
                                int score;
                             };
                             class LimitTime {
                             private:
                                int time;
                             };
                    Before                         After

得点や制限時間をクラス化します。「すべてがオブジェクト」になっていきます。
オブジェクト指向できていますか?
1行につきドットは1つまで

 ?直接の友人にだけ話かける
 ?友達の友達とは関連を持たない
 ?デメテルの法則に従う




余計なクラスとの結合を避けるのが目的となります。
myFriend.getYourFriend().method();




                                        Before




   myFriend.method();




                                         After

「保持しているもの、引数で渡されたもの、自ら作成したもの」だけを扱います。
オブジェクト指向できていますか?
寿限無、寿限無
 五劫の擦り切れ
 海砂利水魚の
 水行末 雲来末 風来末
 食う寝る処に住む処
 やぶら小路の藪柑子
 パイポパイポ
 パイポのシューリンガン
 シューリンガンのグーリンダイ
 グーリンダイの
 ポンポコピーのポンポコナーの
 長久命の長助

長ったらしい名前を付けなさいという意味ではありません。その逆です!
名前を省略しない

 ?省略したいほど長い名前がつく原因を考える
 ?命名困難なクラスやメソッドは要注意
 ?複数の責務を持っていると命名困難になる
 ?名前は1つか2つの単語だけ使う



名前が長くなるのは、設計に問題があるのではないか? という意図です。
void Game::updateAndDrawPlayer(Graphics& g) {
   playerPosition += Vector2(5.0f, 0.0f);
   g.drawImage(playerImage, playerPosition);
 }


                                                 Before


 void Game::updatePlayer() {
   playerPosition += Vector2(5.0f, 0.0f);
 }
 void Game::drawPlayer(Graphics& g) {
   g.drawImage(playerImage, playerPosition);
 }                                                After

2つのことを同時にやってしまうと、名前を付けるのが難しくなります。
void Game::updatePlayer() {
   playerPosition += Vector2(5.0f, 0.0f);
 }
 void Game::drawPlayer(Graphics& g) {
   g.drawImage(playerImage, playerPosition);
 }
                                               Before


 void Game::update() {
   player.update();
 }
 void Game::draw(Graphics& graphics) {
   player.draw(graphics);
 }                                              After

メソッドを分割し、さらにクラスの抽出を行いました。名前が単純になりました。
オブジェクト指向できていますか?
すべてのエンティティを小さくする

 ?50行を超えるクラスは作らない
 ?10ファイルを超えるパッケージは作らない
 ?単一の責務を持つ凝集度の高い設計とする




凝集度を高めれば、クラスやパッケージは小さくできる。という意図になります。
オブジェクト指向できていますか?
オブジェクト指向できていますか?
インスタンス変数は2つまで

 ?クラスは1つの状態変数に責任を持つ
 ?2つの変数を扱う調整役のクラスもある
 ?状態変数が増えるたびに凝集度が低下




1つの状態を管理するクラスと、2つの状態を調整する2種類のクラスが存在します。
class Player {            class Player {
 private:                  private:
   float x;                   Vector2 position;
   float y;                   Angle   angle;
   float angle;               Life    life;
   int    life;            };
 };                        class Angle {
                           private:
                              float angle;
                           };
                           class Life {
                           private:
                              int life;
                           };
                  Before                          After

状態変数が2つになるまで、段階的に変更していきます。
class Player {                 class Player {
 private:                       private:
   Vector2 position;              Transform pose;
   Angle   angle;                 Life      life;
   Life    life;                };
 };
                                class Transform {
                                private:
                                   Vector2 position;
                                   Angle   angle;
                                };




                       Before                          After

positionとangleは座標変換を扱うクラスとして、まとめられそうです。
class Player {            class Player {
 private:                  private:
   float x;                  Transform pose;
   float y;                  Life      life;
   float angle;            };
   int    life;
 };




                  Before                       After

状態変数が2つになりました。
class Player {                class Player {
 public:                       public:
   Player(float x,                Player(Transform& pose,
           float y,                      Life& life);
           float angle,         void update(Time& time);
           int   life);        private:
   void update(                   Transform pose;
     float time);                 Life      life;
 private:                      };
   float x;
   float y;
   float angle;
   int    life;
 };
                      Before                         After

右側のクラスは、すべてがオブジェクトと関連しています。抽象度が高くなるわけです。
オブジェクト指向できていますか?
ファーストクラスコレクション

 ?vector、listなども基本型としてラップする
 ?クラスにはコレクションを1つだけ持たせる
 ?もちろん配列も対象になる




汎用のコレクションクラスもプリミティブ型と同様に扱うという意図です。
class Game {
 private:
    std::list<Actor*>     actors;
    std::list<Particles*> particles;
 };

                                       Before


 class ActorManager {
    std::list<Actor*> actors;
 };
 class ParticleManager {
    std::list<Particle*> particles;
 };                                       After

2つのコレクションを1つのクラスで扱えば、おそらく複雑なクラスになるでしょう。
class Game {
 private:
    std::list<Actor*>     actors;
    std::list<Particles*> particles;
 };

                                       Before


 class Game {
 private:
    ActorManager     actors;
    ParticleMananger particles;
 };
                                        After

専用のクラスを作って、そちらに委譲してしまいます。
オブジェクト指向できていますか?
Getter Setterを使用しない

  ?Getter Setterなどのアクセッサを禁止
  ?極度のカプセル化を行う
  ?クラス外に振る舞いが漏れ出すのを防止
  ?「求めるな、命じよ」に従う



Getter/Setterを付けてしまうと、カプセル化が崩壊してしまうという意図です。
オブジェクト指向できていますか?
構造体
すべてのメンバ変数にGetter/Setterを持たせると、単なる構造体になってしまいます。
void Game::draw(Graphics& g) {
   Vector2 position = player.getPosition();
   Image& image     = player.getImage();
   g.drawImage(image, position);
 }

                                              Before


 void Game::draw(Graphics& g) {
   player.draw(g);
 }
 void Player::draw(Graphics& g) {
   g.drawImage(image, position);
 }                                             After

Getterで「求める」のではなく、オブジェクトに「命じよ」。自分のことは自分でやらせる。
void Game::update() {
   Vector2 position = player.getPosition();
   position += Vetcor2(5.0f, 2.0f);
   player.setPosition(position);
 }

                                              Before


 void Game::update() {
   player.move(Vetcor2(5.0f, 2.0f));
 }



                                               After

振る舞いが外に出てしまっています。オブジェクト自身に行動させます。
振る舞いが外に出てしまうと、コードの重复を生み出す原因になります。
状态は、すべてカプセル化され、メッセージのやりとりだけで、协调して动作する。
#1 1つのメソッドにつきインデントは1段階まで
#2 else句を使用しないこと
#3 すべてのプリミティブ型をラップする
#4 1行につきドットは1つまで
#5 名前を省略しない
#6 すべてのエンティティを小さくする
#7 1つのクラスにつきインスタンス変数は2つまで
#8 ファーストクラスコレクションを使用する
#9 Getter Setterを使用しない
実際にやってみた



エクササイズに挑戦した、ソースコードの解説をしました。(1000行程度のものです)
Jeff Bay




挑戦した人だけが、たどり着ける高みがあるのです。
オブジェクト指向が、いったい何であるかを理解した瞬间をカメラに収めました。
エクササイズまとめ

 ?騙されたと思って小さなプログラムで試す
 ?今までとは異なるアプローチが必要になる
 ?すべてのルールが普遍的に適用できない
 ?ルールを緩めてガイドラインとして利用する



ルールには、例外も出てきます。ルールの「目的や意図」を理解することが重要です。
Jeff Bay




Jeff Bayのエッセイの結びに書いてあった内容です。(抜粋してあります)
9つのルールに関连する、オブジェクト指向设计の原则を绍介しました。
オブジェクト指向
 設計の原則
 単一责任の原则
 オープン?クローズドの原則
 リスコフの置換原則
 依存関係逆転の原則
 インターフェース分離の原則




「単一責務の原則」と「依存関係逆転の原則」の2つだけ解説します。
単一责任の原则
                      Single Responsibility Principles


                     クラスを変更する理由は
                             1つ以上
                        存在してはならない




ひとつのクラスには1つの責任だけを持たせるという意味です。
凝集度
凝集度とは、1つのクラスがどれだけ、1つのことに集中しているか?ということです。
f1

          f1

          f1               f3     f4

          f1               f3     f4

                           f3     f4

     f2                    f3     f4

          f2

          f2



状態が1つの場合、凝集度は最高に。2つでも全メソッドで使用されれば問題なし。
f1         f2

                      f1

                      f1

                      f1

                 f1        f2

                      f2

                      f2

                      f2



凝集度が低い状態です。明らかに、2つの責務を扱っています。
f1         f2


                      f1

                      f2

                 f1        f2

                      f1

                      f2

                      f2

                      f1




メソッドが大きいと、凝集度が低いのか、高いのか判断できません。(大抵は低いはず)
f1         f2         f1         f2

                                 f1
           f1
                                 f1
           f2
                                 f1
      f1        f2

           f1               f1        f2

           f2                    f2
           f2
                                 f2
           f1
                                 f2



状態変数の振る舞いを観察しながら、細かくメソッド化をしていきます。
f1

     f1         f2             f1
           f1
                               f1
           f1
                               f1
           f1

      f1        f2
                          f2
           f2
                               f2
           f2
                               f2
           f2
                               f2


メソッドをグループ化して、クラスを抽出します。
f1

                                  f1

                                  f1

                                  f1
    o1     o2

     o1    o2
                             f2

                                  f2

                                  f2

                                  f2


抽出後の状態です。凝集度は最高の状態になっています。
大きな関数を多くの小さな関数へ分割する
    ことが、クラスを、より小さなクラスへと
    分割することにつながるのです。

「Clean Code 」からの引用です。メソッド化がクラス化への第1歩になります。
平均的なクラス           理想的なクラス

私見ですが、世の中の平均的なクラスは、大きすぎると思います。
小さな部品を组み合わせながら、复雑なシステムを构筑していきます。
再利用可能なクラスとは、ネジのように単纯なことを行う小さなクラスなのです。
依存関係逆転の原則
  The Dependency Inversion Principles


 上位のクラスは下位のクラスに
       依存してはならない
   どちらも「抽象」に依存する
疎結合
オブジェクトは、協調しつつも疎結合であることが望まれます。
class Sphere {               class Sphere {
 public:                      public:
   void draw(                   void draw(
     ID3D11Device& g);            ISphereRenderer& g);
 private:                     };
   Vector3 center;
   float radius;              class ISphereRenderer {
 };                           public:
                                virtual void draw(
                                  Vector3 center,
                                  float radius ) = 0;
                              };


                     Before                        After

直接、実装のクラスではなく、抽象インターフェースに依存させます。
void Sphere::draw(       void Sphere::draw(
   ID3D11Device& g) {       ISphereRenderer& g) {
   // 頂点バッファ作成              g.draw(center, radius);
   // インデックスバッファ作成        }
   // 球体の頂点データを計算
   // シェーダー作成
   // 頂点レイアウト作成
   // 描画コンテスト作成
            ?
            ?
            ?
   // なんとか描画する
 }

                 Before                        After

抽象インターフェースに依存させれば、実装の詳細に依存しなくなるわけです。
Sphere.cpp
       Sphere.cpp
                             ISphereRenderer.h




                             SphereRenderer.cpp




        DirectX                   DirectX


                    Before                        After

右図の青い「上向き」の矢印に注目してください。依存関係が逆転しています。
问题领域

                问题领域のクラス



              抽象インターフェース



   実装領域

                    実装クラス



   API
            Windows API, DirectX, OpenGL

问题领域と実装を分離すれば、それぞれのクラスの凝集度を高めることができます。
class ISphereRenderer {      class Sphere {
 public:                      public:
   virtual void draw(            class Renderer {
     Vector3 center,             public:
     float radius) = 0;             virtual void draw(
 };                                   Vector3 center,
                                      float radius) = 0;
 class Sphere {                  };
 public:                         void draw(
   void draw(                       Renderer& g);
     ISphereRenderer& g);     };
 };


                     Before                          After

抽象インターフェースをインナークラス化し完全に自己完結させた例です。(極端か?)
あなたの                          あなたの
    コード                           コード




                                  抽象



                                  実装




  レガシーコード                       レガシーコード

レガシーコードの分離にも役立ちます。不要な複雑さを持ち込まないようしましょう。
インターフェースと実装を分离する
             実装そのものではなく
             インターフェースに対してプログラミングせよ




抽象インターフェースは、多態性だけではなく、依存関係の分離の役割も果たします。
Jeff Bayの9つのルールを参考に作成した、コーディング規約の事例紹介です。
7つのコーディング规约
             お尻は掻いても
             クソース書くな

                          2012年改訂版
学生対象のゲーム制作プロジェクトで実際に採用した規約です。
复雑度
               10
               まで
复雑度とは、McCabeの循環的复雑度を意味します。(ほぼ制御文+1になります)
1メソッド
       20
   ステートメントまで
ステートメントとは実質的なプログラムの行数です。(コメントなどを除きます)
1クラス
   80
ステートメントまで
ネスト
  2
段階まで
setter
             使用禁止

Getterは最小限に、Setterのみ禁止にしました。
フィールド
            4つ
            まで
特に根拠がある数ではありません。フィールド数は、できるかぎり少なくしましょう。
tweet!




        ソースコードを
           愛
          せよ
ネタです。
≒
Jeff Bayのルールに比べれば、「ゆとり」仕様になっていませんか?
コーディング规约の调整

               理想       標準      妥協
  复雑度           5       10       15
  ネスト           1        2        4
  メソッド長         10      20       40
  クラス長          50      80       160



厳しすぎてもユルすぎてもダメです。まずは、無理なく守れる程度がよいと思います。
http://www.slideshare.net/MoriharuOhzu/ss-9224836



昨年のスライドで、コーディング規約の確認の方法や順守させる工夫を紹介しました。
抽象度
クラスやメソッドの行数を制限することにより、全体の抽象度を高める効果があります。
3つの極度
  極度の抽象化
  極度の分離
  極度の読みやすさ




極度の抽象化の考え方は、上記の本でも紹介されています。
オブジェクト指向できていますか?
まとめ

 ?コーディング規約の本質を知ることが重要
 ?例外もあるので、無理やり適用はしない
 ?ガイドラインとして活用するところから始める
 ?トレーニングとして研修に取り入れてみる



繰り返しになりますが、ルールの「目的や意図」を理解することが重要です。
クラス设计のポイントです。
小さい
   単純
   重複なし
クラス、メソッドのすべてに、このルールがあてはまります。
オブジェクト指向できていますか?
http://www.bennadel.com/resources/uploads/2012/ObjectCalisthenics.pdf

英文になりますが、Jeff Bayのオブジェクト指向エクササイズの全文が入手可能です。
http://www.slideshare.net/yojik/ss-1033616



狠狠撸Shareで紹介されている、オージス総研の方のスライドです。
http://www.slideshare.net/rdohms/your-code-sucks-lets-fix-it



こちらも狠狠撸Shareのスライドです。言語はPHPですが、参考になると思います。
オブジェクト指向できていますか?
最近では、オブジェクト指向と関数型のハイブリッド言语が注目を浴びています。
タイプの异なる3つの言语を3人の女性に例えました。ちょっと考えてみてください。
オブジェクト指向がすべてではありません。さまざまな言语から异なる発想を学びましょう。
普通のやつらの上を行け
                                 Paul Graham




Paul Grahamのエッセイのタイトルから引用しました。
コードと共に生き続ける
    Moriharu Ohzu

More Related Content

オブジェクト指向できていますか?