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

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

新規登録して質問してみよう
ただいま回答率
85.40%
C#

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

デザインパターン

デザインパターンは、ソフトウェアのデザインでよく起きる問題に対して、解決策をノウハウとして蓄積し再利用出来るようにした設計パターンを指します。

Q&A

解決済

4回答

2509閲覧

デメテルの法則 尋ねるな命じろ

Oremiku

総合スコア2

C#

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

デザインパターン

デザインパターンは、ソフトウェアのデザインでよく起きる問題に対して、解決策をノウハウとして蓄積し再利用出来るようにした設計パターンを指します。

2グッド

7クリップ

投稿2024/06/03 22:26

編集2024/06/06 12:00

実現したいこと

仙塲大也 著 『良いコード悪いコードで学ぶ設計入門』で、「getter/setterを多用すると良くない」という旨の記述がありましたが、具体的にどのような実装を心がければ良いのかピンときません。
また、メソッドの正しい実装方法がよくわかりません。

発生している問題・分からないこと

例えば、攻撃キャラが防御キャラに攻撃するとき、攻撃キャラの攻撃力、レベル、魔法威力と、防御キャラの防御力、HPを使ってダメージ計算をする必要があるとします。攻撃キャラは攻撃のみ、防御キャラは防御のみ可能です。
僕は、攻撃キャラと防御キャラは別概念と考え、別々のクラスとして設計しました。しかし、その場合攻撃キャラ防御キャラどちらかにダメージ計算メソッドを実装する必要があり、ダメージ計算メソッドを持つクラスは、もう一方のgetterを使わなければいけなくなってしまいそうです。また、攻撃キャラと防御キャラの属性をどちらも使った計算のため、そもそもダメージ計算メソッドは攻撃キャラと防御キャラどちらが持っておくべきものなのかもわかりません。
今回の例だけでなく、他の例でも応用可能な考え方等ありましたら併せてご教授お願い致します。

該当のソースコード

特になし

試したこと・調べたこと

  • teratailやGoogle等で検索した
  • ソースコードを自分なりに変更した
  • 知人に聞いた
  • その他
上記の詳細・結果

「委譲を行う」、「引数を渡さず尋ねるだけで結果が帰ってくるようにする」等の記述がネットに沢山転がっていましたが、今回の例で活用できる考え方なのかイマイチわかりません。

補足

技術レベルは以下の通りです。↓

学生で大規模開発経験なし。
簡単なボードゲーム、jspやサーブレットを用いた簡単なwebサイト等の開発経験はあります。
OracleのJavaSilver取得済みです。
クラス設計、オブジェクト指向等に関しては全くの初心者です。上記の『いいコード悪いコード』やネットの記事等で勉強を進めている状態です。

ご丁寧な回答、本当にありがとうございました!
全て参考にさせて頂きます。

odamio, ams2020👍を押しています

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

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

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

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

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

dodox86

2024/06/04 02:06

質問者であるOremikuさんのプログラミング経験を前提条件として記載した方ががよいかと思います。 ほとんどプログラミング(含むコーディング、開発)経験が無いままに、抽象的な設計思想の本や説明を読んでもピンとこないことが多いです。 ありがちな例では「オブジェクト指向ではgetter/setterを使ってオブジェクトの内部のデータ、手続きを隠ぺいする」を、「getter/setterを使えばオブジェクト指向になる」と安易に解釈していると思われる初心の方のコードを見かけます。まぁ、そういう過程を経て理解されていくものだとも思いますけれども。 質問には若葉マークが付いていないことですし、質問者さんがそこまで初心者ではないというのであれば、尚更、前提として書いておくといただく回答の内容も変わってくるかと思います。
Oremiku

2024/06/04 02:51

ご指摘ありがとうございます。 確かにその通りですね。補足に記載させていただきます。
actorbug

2024/06/04 21:18

質問のタイトルですが、「デメルテルの法則」ではなく「デメテルの法則」です。原著での記述もそうなっていました。(初版、第2刷)
pecmm

2024/06/04 23:01

タグに `Java Persistence API` が付いてますが、これDB関連のもので質問とは特に関連がないと思われます。 C# とか Java もあんまり直接は関係ない気はしますが……getter/setterというとその辺りの特有の話に見えてしまうということなのかな。 一度タグ見直しをオススメします。
Oremiku

2024/06/04 23:56

・デメルテルの法則(誤)→デメテルの法則(正) ・無関係のタグ を修正しました。 ご指摘ありがとうございます。
guest

回答4

0

ベストアンサー

デメテルの法則は 最小知識の原則(Principle of Least Knowledge) で理解した方がよいです。
getter/setter については、「意味もなくなんでもかんでもgetter/setterで公開するな」くらいの解釈に留めましょう。

最小知識説明のサンプル

この件で攻撃/防御だけで論じるのはかなり難しいので、戦闘ルーチンを導入します。
例えばドラクエ的ゲームならば次のようなものです

戦闘ルーチン

  1. 戦闘は以下のようなフェーズを含むターンの繰り返しである
    1. 行動の決定フェーズ
      1. 味方のキャラクター毎に、ユーザが今ターンの行動を入力する
        1. 行動によっては、その行動のターゲットも入力する (攻撃対象やアイテム使用対象など)
      2. 今ターンの敵の行動を決定する
      3. 素早さを加味し、今ターンの行動順序を決定する
    2. 行動の実行フェーズ
      1. 決定された行動順に、味方/敵キャラクターの行動を実行する
      2. ターンの途中であっても、味方または敵が全滅した時点で戦闘終了

戦闘ルーチンが知らない方がよい例

戦闘ルーチンにおいて、「味方/敵キャラクターの行動を実行」箇所に記述するべきことは
行動の種類ごとの細かな分岐(if文とかswitch文とか)内に具体的な計算式(ダメージ計算、回復量計算、異常状態の判定etc...)をぜんぶ書くのではなく
たとえば攻撃であればせいぜい「適切なダメージ計算式に攻撃側オブジェクトと防御側オブジェクトを渡す」つまり「ダメージ計算を委譲する」です。いっそダメージ計算からHP増減までの「攻撃処理を委譲する」でもよいでしょう。
戦闘ルーチンが全ての計算式の詳細までを知っているのは明らかに「知りすぎ」であり、最小知識の原則に反します。
ですが適切な移譲先を判断するために「尋ねる」くらいはしても良いかもしれません(その処理がシンプルに記述できるのならば)。

戦闘ルーチンが知らなければいけない例

戦闘ルーチンでは例えば決定済みの行動が行えない場合にどう解決するか?のような制御を行う必要があります。

例)
味方Aが敵aを攻撃, 味方Bが敵aを攻撃 の順序で行動決定していて、
味方Aが敵aを戦闘不能にした場合、味方Bの攻撃は …… 空振りするのか? 生き残った敵bをターゲットに変えるのか?等を決める

そのために戦闘ルーチンにとって、攻撃の結果として防御側が戦闘不能になったかどうか、というのは必須の知識となります。
必須の知識はどこかから取得せねばなりません。攻撃処理を委譲したなら戦闘不能に陥ったかどうかを戻り値で取得してもよいですし、結果としてキャラクターに戦闘不能であるかを尋ねてもよいです。
また戦闘不能以外にも攻撃ターゲットから外れる手段(例:不可視になったり隠れたりする魔法やスキル)があるならば、それらを細かく記述し判別するのでなく
「実行の瞬間に攻撃ターゲットとして有効なままか」を尋ねる処理を用意した方が、より最小知識にできて記述がシンプルになるかもしれません。

……以上のように考えれば、getter/setterがどうのや尋ねてはいけないとかではなく
「最小知識とはなにか」を考えるのがが重要だと理解しやすいと思います。

尋ねるな命じろ(Tell, Don't Ask) を理解する別の例

本筋ではないかもしれませんが……

今回のような攻撃オブジェクト/防御オブジェクトの関連が存在しない
単一オブジェクトの処理であれば、話が超単純で分かりやすいです。

例)
Shapeの面積が必要になるたびに

if (shape.type == Shape.Circle) { areaSize = shape.r * shape.r * Math.PI; } else if (shape.type == Shape.Triangle) { areaSize = shape.w * shape.h / 2; } else ...

みたいな計算をするのは明らかに尋ねすぎで、
各Shapeの種類ごとに面積計算処理を持っておいて必要な所で「面積を計算せよ」と命令を1つするだけの方が良いでしょう。
(Shapeの種類が違っても1つの命令で済むように、クラス継承やインターフェースの統一化など言語ごとのやり方がありますが、典型的に shape.getAreaSize() のようなメソッド呼び出し1つで済むような感じ)

投稿2024/06/04 22:13

編集2024/06/11 04:07
pecmm

総合スコア612

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

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

Oremiku

2024/06/06 11:49

すごくわかりやすいです。ご丁寧な回答ありがとうございます。
guest

0

(※念のための注意書き:この回答の記述者は玄人ではありません)


攻撃キャラ防御キャラどちらかにダメージ計算メソッドを実装する必要があり

そのどちらでもない「ダメージ計算をするやつ」という何か(型)みたいのがある形とかでも良いのでは.
例えば攻撃手段毎にダメージ計算が変わるならば「攻撃手段」という型がダメージ計算処理を行っても良いのでは.

以下,素人考え:

class 攻撃手段 { //生成時に「攻撃キャラ」側のパラメタ値を記憶させる ctor( 攻撃キャラのパラメタ群 ){ 引数を保持しておく } //ダメージ計算してダメージ量を返す int CalcDamage( 防御キャラのパラメタ群 ) { ここで「攻撃キャラ」「防御キャラ」双方のパラメタ群が全て揃うので計算できる } }
class 攻撃キャラ { //攻撃に用いる攻撃手段を生成する(引数とかはよくわからん) 攻撃手段 Create攻撃手段( ??? ){ 自身のパラメタ値を保持する攻撃手段を生成して返す } } class 防御キャラ { //攻撃を受ける処理 ??? attacked( 攻撃手段 ) { 自身のHP -= 攻撃手段.CalcDamage(自身のパラメタ群); } }

getterを使わなければいけなくなってしまいそうです

単純にパラメタ値を1つ1つ取り出すのか,その他(ちょっとした計算をした結果を返すとか?)なのか,やり方はあるのでしょうけども,あなたが「何かしらの情報を取り出す」メソッドとかプロパティとかいうのを今現在必要としているのであれば,それを素直に設けて使うことの何が問題なのですか?
それは

多用すると良くない

とかいう話の「多用」に該当するのですか? しないのですか? その線引きはどこですか?
仮に該当するとしたら「何がどう」良くないのですか? 「誰にとって」良くないのですか?
「あなた自身が これこれこういう理由で この実装は良くないと考えている」みたいな話がごっそりと抜けているように思います.


これは単なる初心者の雑魚丸出しな考えですが,
自身の中にまともに存在していない何らかの良し悪しの基準に基づいて良いコードを書く,というのはかなり無理な話だと思います.
どこかの誰かが提唱している今現在の自分では理解できない謎の基準になんとなく盲目的に従おうとしても意味無いですよ.
ひとりでやっているなら「自分にとっての」,複数人なら「その人たちにとっての」,「現時点における良し悪し」というのを考えれば良いのではないでしょうか.

投稿2024/06/04 01:30

編集2024/06/04 05:44
fana

総合スコア11893

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

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

Oremiku

2024/06/04 02:37

>>「あなた自身が これこれこういう理由で この実装は良くないと考えている」みたいな話がごっそりと抜けているように思います. ご指摘ありがとうございます。 getter/setterの多用が良くないといわれている理由として、本来ならばそのクラスだけで完結する処理を別のクラスに実装してしまうといった事態を誘発してしまうからだと、僕は解釈しています。 学生なのでまともな複数人開発経験がなく、憶測でしかないのですが、例えば今回の機能を複数人で実装する場合、getterを実装してしまうと、ほかのメンバーがそのgetterを使って、本来あるべきでない場所にメソッドを作ってしまい、その結果低凝集なシステムに陥ってしまうのではないかと考えました。 解決策として、getterの抽象操作をもつインターフェースともたないインターフェースを作り、getterが必要なクラスはgetterの抽象操作をもつインターフェースの型で、必要でないクラスは抽象操作を持たないインターフェースの型でインスタンス化することで回避する方法を思いつきましたが、強制力が無く、結局間違い一つでバグの原因になってしまう気がしました。 そこで、実務ではどのように解決しているのかを知りたいと考え質問しました。 >>どこかの誰かが提唱している今現在の自分では理解できない謎の基準になんとなく盲目的に従おうとしても意味無いですよ. これについては全くその通りだと思います。実際、見当違いの方向へ行って泥沼にハマってしまっている感覚です。
fana

2024/06/04 04:44

> getter/setterの多用が良くないといわれている理由として、本来ならばそのクラスだけで完結する処理を別のクラスに実装してしまうといった事態を誘発してしまうからだと、僕は解釈しています。 その解釈の正否はわからないですが,今現在問題としているダメージ計算処理というのは「そのクラスだけで完結 しない (と考えている)処理」なのではありませんか? ……という気がします.
fana

2024/06/04 05:04

「いろいろと懸念事項などが脳裏をよぎるのだけども,どうしたらいいのかわからん」みたいな状態なのでしょうか. (馬鹿すぎて何の参考にならん気もしますが)私はそういう状態のときには とりあえず全て無視して好き勝手に(最も愚直と思うやり方で)やったならば,実際のところ何がどれだけくっちゃくちゃになってしまうのか? というのを実際にちょっとやってみる という感じです. 頭で考えていてもらちが明かない場合,「良くない(かもしれない)」コードというのを目の前に具現化させてみる. それで実際に「良く無さ(に起因する被害?)」を経験してみれば,課題点がより明確化される(…かもしれない),みたいな. (実際やってみたら「別に思ってたほど悪いことなんてないな」とか思って,ほぼそのまま使うこともある.)
fana

2024/06/04 05:18

あと,今回みたいな「理解しきれない指針のような話」が存在する場合,あえて > 無視して好き勝手に やってみるときには,意図的にその話の逆を行く感じでやってみると良いかも. 「うっせぇな,俺は getter 丸出しでやるんだよ!」みたいな感じで.
Oremiku

2024/06/04 05:42

>>やってみるときには,意図的にその話の逆を行く感じでやってみると良いかも. おっしゃる通り、「失敗して初めてアンチパターンを知覚できる」というのは大変腑に落ちますね。 少し失敗に対して過敏になりすぎていたかもしれません。参考にさせていただきます。
fana

2024/06/04 05:49

話の流れと関係ないですが,キャラからパラメタ値をgetしない雰囲気の疑似コードみたいなのを追加してみました. 両キャラが自身のメソッドの内部で「攻撃手段」に自分のパラメタ値を提供する感じ.
Oremiku

2024/06/05 00:03

拝見させていただきました。 ありがとうございます。
guest

0

回答ではありません

あくまで個人的な見解です。

setter/getterの位置づけについて

デザインパターンとはこういう課題にはこういう解決策が多いというのをパターン化して命名し、模式的に書いて説明してあるだけのものです。
そしてsetter/getterはmutator(setter)/accessor(getter)という分類をしてみたり、絡めて説明したりもしますが所謂GoFパターンではないと思います。
setter/getterはデザインパターンよりももっと基本的なものなので、何の条件もなく使用するか否かを決める必要があるのであれば、単に趣味嗜好の範疇だと個人的に思っています(議論するのも馬鹿らしい)。

個人的な趣味嗜好をあえて言えば、抽象化するメリットがあるものはsetter/getter必須で、他は実装言語やテストの都合や気分でそのプロジェクトの(実装)方針を決めればいいのではないかと思っています。

当該書籍については読んでいないので知りませんが、何かしらの具体的な想定をしていて、それに対する筆者の考えが記載されているのではないかと思います。同じように考えている人には想定される場面はすぐに思い浮かぶでしょう。一文だけ抜き出して勝手に一般化し、それを吟味・評価することほど無意味なことはないと思います。

仕様から見た個人的見解

攻撃側の「攻撃力、レベル、魔法威力」と防御側の「防御力、HP」が「ダメージ計算」に必要なら、その「ダメージ計算」の役割を持つクラスに攻撃側が持つべきインターフェースと防御側が持つべきインターフェースを渡して、ダメージ計算すればいいだけなのでは?
ここで攻撃側、防御側がaccessorという理解です。このaccessorを作るのが冗長であれば必須とは思いません。
今の条件で言えることはここまでで、UMLで表記すればこんな感じかと思います。

text

1' plantuml用のコード 2@startuml 3interface 攻撃側 { 4攻撃力取得() 5レベル取得() 6魔法威力取得() 7} 8interface 防御側 { 9防御力取得() 10HP取得() 11} 12class 攻撃キャラ implements 攻撃側 { 13+攻撃力取得() 14+レベル取得() 15+魔法威力取得() 16+他のメソッド1() 17} 18class 防御キャラ implements 防御側 { 19+防御力取得() 20+HP取得() 21+他のメソッド2() 22} 23class ダメージ計算 <<utility>>{ 24{static}+ダメージ取得(攻撃側, 防御側) 25} 26攻撃側 <.. ダメージ計算 27防御側 <.. ダメージ計算 28@endum

以下に貼り付けると色々いじれるかも
https://www.plantuml.com/plantuml/uml/

イメージ説明

良い設計/悪い設計についての個人的見解

誰の目にも明らかなものを除き、良い設計か悪い設計かなんて、作った後の結果(金銭的な)や、その後の仕様変更の結果(金銭的な)で決まるものです。なので世にある良い悪いの基準のほとんどは、個人的にはただの好みだと思いますよ。優秀な人には良い設計でも、大半の人にはよく分からない設計とか、想定外の変更で全面作り直しとか、難しいこと考えて色々な変更に柔軟に対応できるようにしたけど変更がなかったとか、結果を見れば悪い設計ということもあるでしょう。結果は未来にならないと分からず、その予測は実際博打であり、それを他人に聞いて委ねるというのは、朝聞いた占いの結果を信じるのと大差ありません。予測の確度を上げるためにはより具体的な情報を元に実際の損益を分かつ人とともに真剣かつ具体的に検討するべきです。

なお、意思疎通のためにOOやDDDや各種開発プロセスの見識を深めることはとても大事です。それらが不要と言ってるわけではないのでお間違いなく。ただ分野によらず、経験不足過ぎて書いてあることが分からない本を読んでも、大した成果は望めないと思います。

投稿2024/06/04 18:22

退会済みユーザー

退会済みユーザー

総合スコア0

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

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

Oremiku

2024/06/06 11:57

やはりインターフェースの型でインスタンス化する手法は有効なのですね。 ご回答ありがとうございます。
退会済みユーザー

退会済みユーザー

2024/06/06 12:42

インターフェースの型は抽象化されてるのでインスタンス化はできません。 一言で言えば適切に質問してくださいというだけの話です。 現状の質問で推測を交えずに可能な範囲の答え方(回答ではない)をしたらこうなったというだけの話ですよ。 書籍を適切に引用し、そこから自分で適切な例を挙げ、さらに適切な質問に繋げれば自ずと知りたい内容が回答になるはずです。 今回pecmmさんの回答は、あなたが書いていることのほとんどを無視し、断片のキーワードだけから想像を膨らませて回答してくださっています。本来それはあなたが書くべき内容であり、その内容に対する対話から(推測なしに)演繹的に導出されるのが正しい回答になります。
guest

0

オブジェクト指向プログラミングでよく言われるのは、getter/setter を多用するのは結局 private 変数を public 変数にしていることと同じことなので良くないということが第一にあると思います。ただ元から public 変数を前提とした設計を行うなら別に関係のない話です。一般的にはクラス型のオブジェクト指向プログラミングでは、基底クラス「キャラクター」にダメージ計算メソッドを備えて、攻撃キャラクターと防御キャラクターの双方に継承または委譲するような方法が取られるのが一般的ではないでしょうか。

オブジェクト同士の一般の相互作用をするようなコードは getter/setter がないと書けません。なので、本質的に getter と setter 必要な場面ですから、気にしなくて良いと思います。ようはオブジェクト指向プログラミングするべきでないコードということです。

継承が良いのか、委譲が良いのか、はたまた別の手段が良いのかは、そのプログラミング言語によると思います。ポリモルフィズムの実現方法にはいろいろあるからです。いずれにせよ、まずは攻撃も防御も行える「キャラクター」クラスを作って、攻撃と防御を実装してから、攻撃キャラクターと防御キャラクターに実装を分けることを考えるべきです。

投稿2024/06/03 23:55

編集2024/06/04 00:14
Paalon

総合スコア263

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

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

Oremiku

2024/06/04 05:33

ご回答ありがとうございます。 >オブジェクト同士の一般の相互作用をするようなコードは getter/setter がないと書けません。 例えばAクラスとBクラスで相互作用をさせるため、Bクラスにgetterを実装することになった場合、 他のプロジェクトメンバーがBクラスのgetterを使用し、あるべき場所とは別の場所にロジックを記述することで低凝集に陥ってしまうというケースがあるのではないかと思ってしまいます(エンジニア未経験なのであくまで推測ですが)。こういったケースは一般的にどのように防止されるのでしょうか。 ぱっと思いつくのは、getterの抽象操作をもつインターフェースともたないインターフェースを作り、getterが必要なクラスはgetterの抽象操作をもつインターフェースの型で、必要でないクラスは抽象操作を持たないインターフェースの型でインスタンス化することで回避する方法です。しかし、強制力が無く、結局間違い一つでバグの原因になってしまう気がしました。 お手数ですが重ねてご回答宜しくお願い致します。
Paalon

2024/06/04 14:11

基本的に、実装した機能の利用者が使わなさそうなものは名前空間の奥に押し込んでおくのです。それでも、機能の内部のコードを再利用したくなったらそのロジックをあとから付け加えられるようにしておく必要があります。じゃないと硬直的になります。あるべき場所とは別のところに機能があるときはリファクタリングをします。高凝集もよくありません。プログラムの機能同士はできる範囲で疎結合にしておかないと、あっちを立てればこっちが立たずみたいなプログラムになりがちです。
Paalon

2024/06/04 14:15

私が疑似コードを書くならこんな感じです。 ``` class Character { hp = 100 attack!(character Character) { character.hp -= 50 } } ```
Oremiku

2024/06/06 11:51

ご回答ありがとうございます! 参考にさせて頂きます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.40%

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

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

質問する

関連した質問