質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.12%

開放閉鎖原則によるクラス設計

受付中

回答 2

投稿 編集

  • 評価
  • クリップ 2
  • VIEW 331

namatomato

score 0

オブジェクト指向による開発の設計指針の一つに「開放閉鎖原則」というものがあると聞きました。
これは変更の理由ごとにクラスを分割することで、新たな仕様追加には新たなクラスを増やすことで対応するというものだと解釈しました。
しかし、一つの要素が追加されるごとに多くの異なる実装が必要な場合はどうやって適用すべきなのでしょうか。

例えばゲームを設計してるとして、キャラクターが1つ増やすとします。
このゲームは、キャラクターごとにいくつもの階層に渡る機能を別に実装する必要があるとします。

こういった場合に、開放閉鎖原則を守りながらクラス設計をするにはどうしたらよいでしょうか?

※追記1
至らぬ点が多々あり、申し訳ございません。

「これは変更の理由ごとにクラスを分割することで、新たな仕様追加には新たなクラスを増やすことで対応するというものだと解釈しました。」
これは解釈以前に内容が伝わる文章になっていませんでした。
変更の理由=新たな仕様追加時には新たなクラスを追加するような設計にするという意味です。

想定しているケースの簡単な例を記載します。拙いコードで申し訳ありませんが、ご確認下さい。

前提条件
・キャラクターを採集して点数を競うイベントがある
・点数の計算式はキャラクターごとに異なる
・点数の計算式は時間帯や曜日によっても異なる

 class Monster
    {
        //★モンスターごとに初期化が必要なフィールド
        //攻撃、防御など複数の戦闘用パラメーター
        //習得中の技
        //場所ごとの遭遇率
        //etc...


        //時間帯と曜日により点数を返すメソッド
        public int GetScore(TimeZone timeZone,Week week)
        {
            //省略
        }


        //危惧している点:分岐ごとの処理メソッドが沢山増える
        private GetScoreByMoning()
        {
            //省略
        }

        private GetScoreByNone()
        {
            //省略
        }

        private GetScoreByNight()
        {
            //省略
        }

    }

    class MonsterList : List<Monster>
    {
        //List内のMonsterの点数を合計する
        public int GetTotalScore()
        {
            //省略
        }

    }

    class Contest
    {
        //MonsterListのTotalScoreによりGiftを返す
        public Gift GetGift(MonsterList monsters)
        {
            //省略
        }


    }

    public enum Gift
    {
        //省略
    }

    public enum Week
    {
        //省略
    }

    public enum TimeZone
    {
        //省略
    }

問題として考えている点は主に2つになります。
・キャラクター固有の特徴を表すフィールドの初期化が複雑で膨大になる
・条件分岐ごとのメソッドにより、メソッド数が膨大になる

回避策として、例えばMonsterAに対してMonsterAScoreクラスを作成して処理を委譲することを考えたのですが、そうするとキャラクターが増えるごとにいくつものクラスを作成しなければいけなくなります。

どのようにアプローチすればよいでしょうか。

※追記2

開発者が知っておくべきSOLIDの原則
ソフトウェア原則[1] - OCP(Open-Close Principle)
Laravelチップシリーズ 2:SOLIDの世界1

以上のリンク先から、変更の理由=キャラクターの追加と設計段階で予想されるなら、キャラクターをそれぞれ共通のインターフェースを持った別のクラスにすることで既存のクラスの修正を行わなくてよいのが開放閉鎖原則のメリットだと考えておりました。
リンク先の内容が全て或いは一部誤っているのか、記載の内容はあっていて私の理解だけが誤っているのか自分には判断できません。
申し訳ありませんが、こちらのご指摘からお願いします。

「階層」とはオブジェクトの所持関係のことです。追記1で記述したコードの場合、キャラクタークラスのフィールドのオブジェクトが更に別のフィールドを持ち、その初期化の内容がキャラクターごとなら追記1の通り初期化が複雑で膨大になるのでは?と考えていました。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • namatomato

    2019/10/03 20:59

    キャラクターが増える度にScoreクラス等キャラクターが関連するクラスを修正するということでしょうか?
    すいません。確かに理解出来ていませんでした。
    この原則はどういったメリットがあるのでしょうか。

    キャンセル

  • Zuishin

    2019/10/03 21:06

    キャラクターが関連する一連のクラスを修正したのでは拡張に対して開いているとは言えません。

    キャンセル

  • Zuishin

    2019/10/03 21:12

    キャラクターではなく条件と書いた意味をよく考えて、もう一度調べてきてください。

    キャンセル

回答 2

+1

クラス単体、またはその派生でのみ考えてしまうと、本来分けるべき振る舞いを分けられなくなってしまうことになる、というのが根っこの問題に見えます。

そもそも「キャラが自身についてどう評価されるか具体的な処理方法を知っている」ことが設計として健全な状態なのかという点が気になります。

個人的には質問文で例示されたキャラクラスは多くを知り過ぎている設計(責任を持ち過ぎている、単一責任原則を満たせていない)と感じます。ですので、キャラが自分の評価方法を知らなくて済む方法を考えるべきだと思いました。

そこで「キャラごとのスコアの合計値を算出する機能」はキャラ自身ではなく、(「ゲームの仕様」という関心分野からの必要に応じて、)「スコア評価システム」といった機能が持つべきものとしてみるのはどうでしょうか。

例えば「スコア評価システム」にキャラID・曜日・時間帯等を与えることで、スコア評価処理のためにキャラデータをデータベース等から読み取っていき、評価値を決定する計算式を通した上でスコアを決定する。といった具合です。

(キャラデータはキャラID・曜日・時間帯といったデータを表にまとめて、それをゲームで利用できる形にして読み込み、利用していきます。詳しくは「マスターデータ」、「ゲーム データベース」といった単語で検索してみてください)

大切なのは「スコア評価システム」と「キャラごとのデータ」というように、データと処理を別々に分けることで管理がしやすくなるということです。

あとはスコア評価システム内でどう計算していくか、ということになります。キャラIDごと、あるいは別途キャラ属性を付与して、IDごと属性ごとにスコア計算の式を切り替えて実装する、などゲームの作りに合わせて調整していくといいのかなと思います。

(個人的には計算式は一つに統一してキャラ毎のパラメータの変化だけでスコア評価を表現できたほうがシンプルにまとまりそうだなと思います。参考までに。)

クラスや派生といった言語機能、言語表現に囚われず、ここまで示してきたように「データと処理を分ける」ことが解放閉鎖原則を満たした設計に近づくヒントになるのかなと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/10/10 10:43

    回答ありがとうございます。
    キャラクターをデータと処理で分けるとのことですが、キャラ属性とそれに応じて処理を切り替えるアプローチだと、キャラ属性が増えるごとにスコア評価システムのクラスに修正が発生するように思えるのですが、そうはならないのでしょうか?

    キャンセル

  • 2019/10/10 16:37

    一応Yesです。

    スコアに関する仕様が変わるならスコアを管理するシステムも(必要に応じて)変更があって然るべきです。あくまでスコアに無関係なところで修正が発生しないように修正の影響範囲を限定化できていることが設計する上で大切だと思います。

    質問文の解放閉鎖原則の解釈を踏まえるなら、「スコア計算それ自体もゲームに追加された新しい仕様だと考えて、クラスを分けるべきだ」と表現したら伝わるでしょうか。

    キャンセル

+1

ざっと調べただけなので、間違っているかもしれません

仕様変更に新たなクラスで対応する、ってのは多分、派生クラスを作って対応しましょう、だと思います。

class Monster{}

class Slime: Monster{}


こうやってモンスターの種類に応じてクラスを作るわけですね。
これなら新しいモンスターを作っても派生クラスを増やしていけばいいです。

ただ正直なことを言うと、この設計は時代遅れ感があります。
今回のケースだと、個々に派生クラスを作らずとも、派生したい関数を関数オブジェクトにして、
それぞれに対し、個別に関数を割り当てる、という設計をします。

var slime = new Monster();
/* 炎なら半減、雷なら2倍、これを関数化してもいい */
slime.OnDamage = (d) => d.element == d.Fire ? d.damage / 2 : d.element == d.Thunder ? :d.damage * 2 : d.damage;

この手の文献は、問題点を正確に把握し、どういうゴールを目指すかは参考にしてもいいのですが、
具体的な解決手段に関しては、今ではもっといい方法があったりするので、
無理に一つの文献を追いかけ続ける必要もないかなと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/10/10 12:49

    回答ありがとうございます。
    このケースだとslimeはオブジェクトを作成したい場合はfactoryクラスを用いるイメージでしょうか。
    その場合、slimeのダメージ判定が複雑なら更にSlimeDamageFactoryを作っていくといったアプローチになるのでしょうか。

    キャンセル

  • 2019/10/10 14:15

    GoFのパターン、よくわからないんで、そのへんの単語で聞かれてもわからないですね
    MonsterのHPやらATKやらはどうやってデータとして持つかを考えると、
    エクセル(csv)やDBになるんじゃないでしょうか?
    ダメージ計算は関数名を書いてリフレクションする、スクリプトを読み込めるようにするなど考えられます

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.12%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る