2K Views
September 27, 24
スライド概要
CEDEC2012の講演資料です。
https://cedec.cesa.or.jp/2012/program/PG/C12_P0084.html
オブジェクト指向できていますか? 真のオブジェクト指向が身に付くコーディング規約 日本工学院八王子専門学校 大圖 衛玄
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 実際にあった伝説のコード。シンプルな2Dゲームなのに1万行ありました。 調べてみると、ステージごとに、まるごとコピペで作成した神クラスが5つ。 1ゼウス2千行、5ゼウスで、合計1万行となっていました。 Stage5
理想的な オブジェクト指向の 世界とは?
小さな大量のオブジェクトが・・・(次に続く)
お互いメッセージを送りながら協調し複雑なシステムを構築する。(次に続く)
各クラスは、1つの機能に集中し、最小限のインターフェースで構成されています。
コーディング規約で 理想的な オブジェクト指向の 世界を目指す 本セッションのゴールです。
命名規則やスペースの数、{}位置などのコーディング規約のお話ではありません。
オブジェクト指向 エクササイズ Jeff Bay ソフトウェア設計を改善する 9つのステップ
第5章に書かれているJeff Bay氏によるエッセイになります。
読後の衝撃は忘れられません。理想的なオブジェクト指向の姿が理解できました。
Jeff Bayが冒頭で紹介している、7つのコード品質特性です。
Extreme 必然的にオブジェクト指向になってしまう、Extremeなコーディング規約です!
メソッドにつきインデントは1段階 1つのメソッドごとに制御文を1つに制限 if、for、whileごとにメソッド化する 制御文の内側をメソッド化する 早期リターンを活用しインデントを浅くする 制御文のネストをなくすルールです。小さく、単純なメソッドを作成するのが目的です。
void Class::method() { for (外側ループ) { for (内側ループ) { if (条件) { 処理; } } } } void Class::method() { for (外側ループ) { for (内側ループ) { method1(…); } } } void Class::method1(…) { if (!条件) return; 処理; } Before 制御分の内側から順番にメソッド化していきます。 After
void Class::method() { for (外側ループ) { for (内側ループ) { method1(); } } } Before void Class::method() { for (外側ループ) method2(…); } void Class::method2(…) { for (内側ループ) method1(…); } After
void Class::method() { for (外側ループ) { for (内側ループ) { if (条件) { 処理; } } } } void Class::method() { for (外側ループ) method2(…); } void Class::method2(…) { for (内側ループ) method1(…); } void Class::method1(…) { if (!条件) return; 処理; } Before 制御文ごとにメソッド化され、ネストが1段階となりました。 After
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]; ループの内側で2つのことを扱うと、メソッド化が困難です。ループを分割します。 After
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); } Martin Fowlerの「Split Loop」というリファクタング例になります。 After
else句を使用しないこと 早期リターンを活用する else if、switchの条件分岐を避ける Strategy、Stateなどのデザパタを活用 三項演算子(?)を利用する 条件分岐を単純化するのが目的です。
int method() { int result; if (条件1) { if (条件2) { result = 20; } else { result = 30; } } else { result = 10; } return result; } int method() { if (!条件1) return 10; if (条件2) return 20; return 30; } Before 左と右のコードは同等の処理を行います。早期リターンを使うと単純になります。 After
class Actor { public: virtual void update() = 0; }; switch (actorType) { case PLAYER: updatePlayer(); break; case ENEMY: updateEnemy(); break; case BULLET: updateBullet(); break; } actor->update(); Before 「State/Strategyによるタイプコードの置き換え」というリファクタリングです。 After
int method() { int result; if (条件) result = 20; else result = 30; return result; } int method() { return 条件 ? 20: 30; } 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 { private: int score; int limitTime; }; class Game { private: Score score; 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); } 2つのことを同時にやってしまうと、名前を付けるのが難しくなります。 After
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 { private: float x; float y; float angle; int life; }; class Player { private: Vector2 position; Angle angle; Life life; }; class Angle { private: float angle; }; class Life { private: int life; }; Before 状態変数が2つになるまで、段階的に変更していきます。 After
class Player { private: Vector2 position; Angle angle; Life life; }; class Player { private: Transform pose; Life life; }; class Transform { private: Vector2 position; Angle angle; }; Before positionとangleは座標変換を扱うクラスとして、まとめられそうです。 After
class Player { private: float x; float y; float angle; int life; }; class Player { private: Transform pose; Life life; }; Before 状態変数が2つになりました。 After
class Player { public: Player(float x, float y, float angle, int life); void update( float time); private: float x; float y; float angle; int life; }; Before class Player { public: Player(Transform& pose, Life& life); void update(Time& time); private: Transform pose; Life life; }; 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; }; 2つのコレクションを1つのクラスで扱えば、おそらく複雑なクラスになるでしょう。 After
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 f3 f4 f2 f2 f2 状態が1つの場合、凝集度は最高に。2つでも全メソッドで使用されれば問題なし。
f1 f2 f1 f1 f1 f2 f1 f2 f2 f2 凝集度が低い状態です。明らかに、2つの責務を扱っています。
f1 f2 f1 f2 f2 f1 f1 f2 f2 f1 メソッドが大きいと、凝集度が低いのか、高いのか判断できません。(大抵は低いはず)
f1 f2 f1 f2 f1 f1 f1 f2 f1 f2 f1 f1 f2 f2 f2 f1 f2 f2 f1 f2 状態変数の振る舞いを観察しながら、細かくメソッド化をしていきます。
f1 f1 f2 f1 f1 f1 f1 f1 f1 f2 f1 f2 f2 f2 f2 f2 f2 f2 メソッドをグループ化して、クラスを抽出します。
f1 f1 f1 f1 o1 o1 o2 o2 f2 f2 f2 f2 抽出後の状態です。凝集度は最高の状態になっています。
大きな関数を多くの小さな関数へ分割する ことが、クラスを、より小さなクラスへと 分割することにつながるのです。 「Clean Code 」からの引用です。メソッド化がクラス化への第1歩になります。
平均的なクラス 理想的なクラス 私見ですが、世の中の平均的なクラスは、大きすぎると思います。
小さな部品を組み合わせながら、複雑なシステムを構築していきます。
再利用可能なクラスとは、ネジのように単純なことを行う小さなクラスなのです。
依存関係逆転の原則 The Dependency Inversion Principles 上位のクラスは下位のクラスに 依存してはならない どちらも「抽象」に依存する
疎結合 オブジェクトは、協調しつつも疎結合であることが望まれます。
class Sphere { public: void draw( ID3D11Device& g); private: Vector3 center; float radius; }; class Sphere { public: void draw( ISphereRenderer& g); }; class ISphereRenderer { public: virtual void draw( Vector3 center, float radius ) = 0; }; Before 直接、実装のクラスではなく、抽象インターフェースに依存させます。 After
void Sphere::draw( ID3D11Device& g) { // 頂点バッファ作成 // インデックスバッファ作成 // 球体の頂点データを計算 // シェーダー作成 // 頂点レイアウト作成 // 描画コンテスト作成 ・ ・ ・ // なんとか描画する } void Sphere::draw( 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 { public: virtual void draw( Vector3 center, float radius) = 0; }; class Sphere { public: void draw( ISphereRenderer& g); }; Before class Sphere { public: class Renderer { public: virtual void draw( Vector3 center, float radius) = 0; }; void draw( Renderer& g); }; 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 SlideShareで紹介されている、オージス総研の方のスライドです。
http://www.slideshare.net/rdohms/your-code-sucks-lets-fix-it こちらもSlideShareのスライドです。言語はPHPですが、参考になると思います。
https://amzn.asia/d/eom4zI0
最近では、オブジェクト指向と関数型のハイブリッド言語が注目を浴びています。
タイプの異なる3つの言語を3人の女性に例えました。ちょっと考えてみてください。
オブジェクト指向がすべてではありません。さまざまな言語から異なる発想を学びましょう。
普通のやつらの上を行け Paul Graham Paul Grahamのエッセイのタイトルから引用しました。
コードと共に生き続ける Moriharu Ohzu