🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

2回答

2101閲覧

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

namatomato

総合スコア5

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

0グッド

2クリップ

投稿2019/09/29 23:33

編集2019/10/05 04:35

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

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

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

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

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

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

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

C#

1 class Monster 2 { 3 //★モンスターごとに初期化が必要なフィールド 4 //攻撃、防御など複数の戦闘用パラメーター 5 //習得中の技 6 //場所ごとの遭遇率 7 //etc... 8 9 10 //時間帯と曜日により点数を返すメソッド 11 public int GetScore(TimeZone timeZone,Week week) 12 { 13 //省略 14 } 15 16 17 //危惧している点:分岐ごとの処理メソッドが沢山増える 18 private GetScoreByMoning() 19 { 20 //省略 21 } 22 23 private GetScoreByNone() 24 { 25 //省略 26 } 27 28 private GetScoreByNight() 29 { 30 //省略 31 } 32 33 } 34 35 class MonsterList : List<Monster> 36 { 37 //List内のMonsterの点数を合計する 38 public int GetTotalScore() 39 { 40 //省略 41 } 42 43 } 44 45 class Contest 46 { 47 //MonsterListのTotalScoreによりGiftを返す 48 public Gift GetGift(MonsterList monsters) 49 { 50 //省略 51 } 52 53 54 } 55 56 public enum Gift 57 { 58 //省略 59 } 60 61 public enum Week 62 { 63 //省略 64 } 65 66 public enum TimeZone 67 { 68 //省略 69 }

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

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

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

※追記2

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

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

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

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2019/09/29 23:40

開放閉鎖原則についての理解が間違っています。ここでは説明しきれないので、複数の情報をあたって自分で調べるのが良いと思います。この概念が理解できてからでないと、質問の本旨である設計の話はできません。
hihijiji

2019/09/30 01:36

現在のクラス設計を提示してください。
izmktr

2019/09/30 03:16

いくつもの階層に渡る機能、ってのが全くピンとこないんですが、具体的にどういうものですか? 例えばポケモンとかいっぱいいますけど、同一階層としての多数のパラメータはありますが、 いくつもの階層に渡る機能なんてありませんよね?
namatomato

2019/09/30 03:44

>理解が間違っています 既に複数の説明を見た上での解釈なので、差し支えなければ正しい理解を提示している記事や書籍等を紹介いただけると幸いです。 私の見た中では、「修正に対して閉じている」=既存のコードを変更せずに、「拡張に対して開いている」=モジュールが拡張可能である。 という説明から、enumと条件式による機能拡張を否定して、strategyパターンの推奨をしている方が多いと感じたのですが・・・
Zuishin

2019/09/30 05:20

> 変更の理由ごとにクラスを分割すること これが開放だというのが間違いということです。新たにクラスの追加ができるよう設計するのが開放であり、変更のたびに設計し直すことではありません。
Zuishin

2019/09/30 05:28

例えばソートを開発する時には、何をどのようにソートするかを考えるわけですが、int 配列でも string リストでもその他未来に追加される新たなクラスでも扱えるように設計された仕様を「拡張に対し開いている」と言います。 この質問の例で言うなら、どんなキャラクターが後から追加されても他の部分は変更無しで使えるように設計することです。 キャラクターを追加するたびに増築することではありません。
Zuishin

2019/09/30 05:48

ジェネリクスは開放に対して強力な武器となりますが、それと混同されるとよろしくありません。なのでもう少し補足すると、例えば int 配列と string リストの双方のソートが必要になったとき、それぞれを別々に開発するのではなく、それらの祖となるクラスを開発し、そこから子クラスを分岐させるのは拡張に対して開いています。祖となるクラスはただのインターフェースや抽象クラスではなく、どのようなクラスにでも適用できる汎用的なソートを実装するものです。子孫クラスはそのコードを実際に使いながら特定の状況に最適化します。
Zuishin

2019/09/30 05:50

やっぱり書ききれないので、その複数の説明を読み直してください。また、その説明がどこにあるのかを質問に追記すると、間違っている情報や誤解を生みそうな表現は指摘してもらえるかもしれません。
Youbun

2019/09/30 09:10

>>例えばゲームを設計してるとして、キャラクターが1つ増やすとします。 このゲームは、キャラクターごとにいくつもの階層に渡る機能を別に実装する必要があるとします。 ・全キャラクター共通の変数・関数を親クラスとして持つ →キャラクターごとに必要な処理を関数(クラス)として持つ →処理を呼ぶときは、if文でキャラ種別ごとに処理を呼ぶ 大雑把ですが、これで「原則を守りながら」クラス設計やってるといえると思います。 大事なのが、 ・全キャラ共通の部分を親として持つ ・別の処理の実装が必要なら別の関数として持って、共通の部分変更することが無いようにする ということです。これで、他キャラに影響を与える変更をしない設計ができます。 「拡張=処理ごとにクラスいっぱい作る!」ではなく 共通の部分を破壊しなければ、関数を追加するだけでも拡張といえるので クラス追加しなきゃ!や、分岐をいっぱい作らなきゃ!とか変なことは考えずに、 とりあえず原則を理解した気になって、自分なりに効率の良い実装経験を積んでいけば 自然と原則を守った実装ができると思います。。。 原則って追求しだしたらきりがないので根を詰めすぎないでください
papinianus

2019/10/01 00:52

他の方の繰り返しになりますが具体例をお願いします。 個人的にはその多数の変更をするやつは継承で解決できるのか疑わしいと考えます。それほど異なるのであれば、インターフェイスが適切ではないですか?魔王とスライム(ゲーム知識浅くてすみません)の共通の祖先なんてobjectクラスと同義ではないでしょうか?
Zuishin

2019/10/01 22:21 編集

GetScoreBy... をたくさん作るところが拡張に対して閉じています。この仕様だと、新しい計算方法を導入しようと思えばメソッドを増やさなければいけません。メソッドを増やすためにはクラスを編集しなければいけません。 この場合はキャラクターにスコアを計算させるのではなく、スコアを計算するための専用のクラスを作るべきです。スコア計算用のクラスは条件が増えた場合には編集して計算方法を修正してください。修正しても他のクラスに影響を与えないよう設計することを「修正に対して閉じている」と言います。この場合は継承して新しいクラスを作るべきではありません。それをすると他の部分を直さなくてはならなくなるからです。変更の理由はありますが、変更のために新しいクラスを作ってはいけないということです。 伝わる伝わらないの問題ではなく、理解できていないようにしか見えません。
fana

2019/10/02 05:47

私くらいの雑魚になると「いくつもの階層」という言葉の意味から既にわからないという…「階層」というのはこの提示されたコードで言えば何のことを指すのでしょう?
namatomato

2019/10/03 11:59

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

2019/10/03 12:06

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

2019/10/03 12:12

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

2019/10/20 15:25

「キャラクターごとの条件ごと」にクラスを作成すれば拡張に対して開いていますか?
Zuishin

2019/10/20 15:34 編集

閉じていますね。各キャラクターをそれぞれのクラスにすべきではないと思います。
namatomato

2019/10/24 01:44

条件A→キャラクター 条件B→曜日 条件C→時間帯 A×B×C通りのスコア評価クラスを作成するということでしょうか。
Zuishin

2019/10/24 01:47 編集

わざと言ってるのかと思い始めました。
namatomato

2019/10/24 03:29

申し訳ありません。故意に間違えてるわけではありません。 条件の分類の認識は正しいでしょうか。 少なくともこの場合A×B×C通りの異なるメソッドが必要になると思います。 スコア評価用クラスは単一でしょうか、または複数でしょうか? A×B×Cなので100を超えると思われるメソッドを単一のクラスに実装するとは考えにくいので、複数のクラスになると思うのですが、A×B×Cの組み合わせごと以外の分け方となると正直思いつきません。
guest

回答2

0

ベストアンサー

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

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

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

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

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

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

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

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

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

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

投稿2019/10/05 07:48

tor4kichi

総合スコア769

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

namatomato

2019/10/10 01:43

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

2019/10/10 07:37

一応Yesです。 スコアに関する仕様が変わるならスコアを管理するシステムも(必要に応じて)変更があって然るべきです。あくまでスコアに無関係なところで修正が発生しないように修正の影響範囲を限定化できていることが設計する上で大切だと思います。 質問文の解放閉鎖原則の解釈を踏まえるなら、「スコア計算それ自体もゲームに追加された新しい仕様だと考えて、クラスを分けるべきだ」と表現したら伝わるでしょうか。
namatomato

2019/10/20 15:48

キャラクターが増えるごとに「スコア評価システム」など、キャラクターに関連する部分を一斉に変更するか、キャラクターに関連する部分が増えるごとに既存のキャラクターにインターフェースを追加することになるかの2択を仕様によって選択することになるのでしょうか。 「新キャラクターのスコア評価クラス」←新たに作成 「キャラクターとスコア評価クラスをマッチングさせるクラス」←修正が発生 少なくともキャラクターが増えると、スコア評価に関してだけでも上記のことが発生するという理解で問題ないでしょうか。その場合キャラクターが追加された時、どこの修正が必要で何を追加するかは、やはりドキュメント等で管理することになるのでしょうか。お教えいただいた「マスターデータ」、「ゲーム データベース」といった検索ワードで概念は掴めたのですが、ゲーム関連は中々具体的な設計例を見る機会が無いので上手い方法があるのか、それともある程度は仕方ないことなのか把握することが難しいと感じました。
tor4kichi

2019/12/10 12:18

一段落目についてはその通りと思いました。2択固定かはわかりませんが、各所にコピペコードを散在させておくか、共通化して局所化させるか、ということですね。分散させておいても修正こそたくさん必要になりますが、影響範囲を小さくできるため変更に頑強でもあります。また共通化して局所化させれば修正は小さく済みますが影響範囲が広くなりますよね。 「」から始まるそれ以降については、キャラごとに新しいスコア計算式を割り振る必要があるなら、評価クラス作成とマッチングさせる部分の修正が必要と思いますが…それはなかなか仕様変更が大変そうですね。 あくまでオススメしたいのは、可能な限り共通化・普遍化したスコア計算式を予め設計した上で、各キャラクターのIDで紐付いた「スコア値計算用係数を収めるデータベース」に対する更新を行うのみで(スコア関係に関しては)キャラ仕様追加完了、という風にスコア評価とキャラとの間における影響を最小化したいね、ということを回答本文の方でも伝えています。 ドキュメント化はプロジェクトごとによると思います。もしドキュメントを作ることをできるだけ小さく済ませたいと感じられるようでしたら、「ドメイン駆動設計」やその辺縁に広がる設計思想について触れていくことが近道になるやもしれません。例えば「スコア評価システム」という「名付け」をしてコミュニケーションを密にしたいという発想はドメイン駆動設計が元になっています。共通ワードがしっかり出来ているとドキュメントを書くべきところもポイントを絞って書くだけで済むのではないかと思います。 サンプルはなかなか見つからないですね。レベルデザイナー向けのデータベース作成という視点で探してみるとか、あとはRPGツクールといったデータベースありきなゲーム制作ツールで実際に作ってみると肌感覚が身につくかもしれません。 サーバー無しのクライアントのみのゲームであれば、データベースといってもCSVやエクセル、jsonで作って読み込んだものをクラスとしてマッピングするだけでも一応のマスターデータとしては使えますから、入り口はそういうやり方もありかと思います。
guest

0

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

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

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/05 15:10

izmktr

総合スコア2856

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

namatomato

2019/10/10 03:49

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

2019/10/10 05:15

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問