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

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

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

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

オブジェクト指向

オブジェクト指向プログラミング(Object-oriented programming;OOP)は「オブジェクト」を使用するプログラミングの概念です。オブジェクト指向プログラムは、カプセル化(情報隠蔽)とポリモーフィズム(多態性)で構成されています。

意見交換

クローズ

10回答

2443閲覧

has-a関係と多態性をどのように両立させればよいか

Sugatani

総合スコア0

C#

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

オブジェクト指向

オブジェクト指向プログラミング(Object-oriented programming;OOP)は「オブジェクト」を使用するプログラミングの概念です。オブジェクト指向プログラムは、カプセル化(情報隠蔽)とポリモーフィズム(多態性)で構成されています。

1グッド

1クリップ

投稿2023/12/23 05:51

編集2023/12/23 10:31

1

1

テーマ、知りたいこと

オブジェクト指向プログラミングにおいて、包含したクラスで多態性をどう実現するべきかどうかについて質問があります。

背景、状況

例としてキャラクターを表すクラスを設計を設計しました。
このキャラクターは、自身の持ち物を管理するInventoryクラスを包含(has a)しています。

CSharp

1public class Inventory 2{ 3 private List<Item> _itemList = new List<Item>(); 4 public ReadOnlyCollection<Item> ReadOnlyItemList => _itemList.AsReadOnly(); 5 6 public void AddItem(Item item) 7 { 8 _itemList.Add(item); 9 } 10 11 public void RemoveItem(Item item) 12 { 13 _itemList.Remove(item); 14 } 15} 16 17public class Character 18{ 19 private Inventory _inventory = new Inventory(); 20 21 public void AddItem(Item item) 22 { 23 _inventory.AddItem(item); 24 } 25 26 public void RemoveItem(Item item) 27 { 28 _inventory.RemoveItem(item); 29 } 30}

また、アイテムを出し入れする宝箱など内部にインベントリを持つエンティティを表すクラスも設計することにしました。
宝箱も内部インベントリを管理するInventoryクラスを包含(has a)しています。

CSharp

1public class TreasureBox 2{ 3 private Inventory _inventory = new Inventory(); 4 5 public void AddItem(Item item) 6 { 7 _inventory.AddItem(item); 8 } 9 10 public void RemoveItem(Item item) 11 { 12 _inventory.RemoveItem(item); 13 } 14}

この時、キャラクタークラスと宝箱クラスの間でポリモーフィズムを実現したいと考えました。
どちらも同じくインベントリを持つため、インベントリに関するメソッドを外部に向けて公開するインターフェイスを作成し、
キャラクタークラスと宝箱クラスに適用します。

CSharp

1public interface IInventoryHolder 2{ 3 public void AddItem(Item item); 4 public void RemoveItem(Item item); 5} 6 7public class Character : IInventoryHolder {} 8 9public class TreasureBox : IInventoryHolder {}

しかしながら、このようにインターフェイスを使用して多態性を実現する方法が適切かどうか疑問に思いました。
キャラクターと宝箱という異なるクラスに対してどちらもインベントリを保持しているという点からインターフェイスを共通させることが適切か否かが判断できません。

まとめ

上記の例において多態性を表現するコーディングは適切なのでしょうか?
また、そもそもオブジェクト指向の作法に則る場合、包含されたクラスでどのように多態性を表現するべきなのでしょうか?

ams2020👍を押しています

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

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

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

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

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

回答10

#1

fana

総合スコア11954

投稿2023/12/23 09:30

編集2023/12/23 09:33

素人意見ですが,

CSharp

1public class Character : IInventoryHolder {} 2public class TreasureBox : IInventoryHolder {}

が「適切かどうか」というよりは,そうする 必要があるか否か ではないでしょうか?

「アイテムを追加/削除できるやつ」(この文脈だと「インベントリを所持しているやつ」か?)という存在を取り扱う処理 みたいなのが実際に存在するのか?という.
(そういう場面が無いならば,そもそも IInventoryHolder interface 自体が不要ということでは)

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

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

#2

Sugatani

総合スコア0

投稿2023/12/23 10:28

編集2023/12/23 10:33

回答ありがとうございます。
必要性が不明瞭な機能の実装は不要であるという視点は確かに抜けていました。

ただ、この質問をした理由として多態性を実現したい状況に遭遇したからという理由があります。

例えばマップ上の全てを対象として特定のアイテムが存在するかどうかを判定したい場合、
IInventoryHolderインターフェイスにHasItemメソッドを実装し、foreachなどで回すことで対処するとします。

CSharp

1public interface IInventoryHolder 2{ 3 public bool HasItem(IItem item); 4}

上記の場合はIInventoryHolderを実装する意味があると思うのですが、
IInventoryHolderを実装せずとも、Characterごと・TreasureBox(インベントリを持つエンティティ?)ごとに判定して
その結果から特定のアイテムが存在するかを判定することは出来ると思ます。

私が悩んでいるのはインベントリというクラスを包含(保持)しているという点でしか共通点のないオブジェクトに対して
その共通点を抽出してインターフェイスとして実装することの是非なんです。

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

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

#3

ozwk

総合スコア13551

投稿2023/12/23 14:10

名前が悪そうです
IInventoryHolderという名前をIContainerぐらいにしておけば「Inventryクラスのインスタンスを持つ」という実装に立ち入った名前から「何か物を出し入れできるもの」という機能だけ指定した名前になりますし、可能性があるかはわかりませんがInventryインスタンス1つだけで所持アイテム管理するようなもの以外が出てきても同じように扱えます。例えばスロットA,Bに入れたアイテムを消費してスロットCに生成物を入れる生産系のエンティティなど

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

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

#4

fana

総合スコア11954

投稿2023/12/23 16:34

IInventoryHolderを実装せずとも、Characterごと・TreasureBox(インベントリを持つエンティティ?)ごとに判定して
その結果から特定のアイテムが存在するかを判定することは出来ると思ます。

じゃあ,何故その方法でやらないのか?
例えば,

マップ上の全てを対象として特定のアイテムが存在するかどうかを判定したい

という処理の実装箇所が,容易に全ての Character と TreasureBox にアクセスできて,
且つ,実際のところアイテムを保持できる存在はキャラクタと宝箱くらいしか存在しない,みたいな場合であれば,IInventoryHolder なる interface を設けてまでちょっとした処理を共通化するのは大仰すぎると思えるかもしれません.

逆に,その処理箇所が,Character とか TreasureBox とかいう具体的な型を扱いたくないような場所であるとか,
アイテムを保持できる存在が他にもたくさんあるとかいう場合であれば,IInventoryHolder を設けるのが良いと判断するかもしれません.

多態を用いたいかどうか,というのは,なんかそういう必要性の判断から来るんじゃないかな,と.

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

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

#5

Sugatani

総合スコア0

投稿2023/12/23 16:53

確かにIInventoryHolderという名称は確かに曖昧でもっと具体的な役割を示す名称にするべきだと思います。名は体を表すという言葉通り、名付けの時点でその役割をしっかりと決定するべきですね。

また、多態を用いたいかどうかは必要性の判断からくるという意見で気づかされましたが、私は多態性は実装し得だという思考があるようです。

同じ処理だから共通化して関数に切り出す事に関してはアンチパターンになりえると考えていても、インターフェイスに切り出すのは抽象化なのでいくら切り出しても問題がないという思考です。

多態性を実現しておけば後々楽ができるであろうし、
実際インターフェイスや多態性を多用することはあまり問題がないのではと考え、他の実装方法より優先的に多用してしまいました。

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

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

#6

Long

総合スコア5

投稿2023/12/24 17:05

インターフェイスは設計概念の一つです
例えば戦闘で攻撃を行う際の処理を考えるとします
これを実装するために必要なパラメータとして、まず挙げられるのが体力と攻撃力の設定です
また、ダメージの計算処理も用意する必要があります
これらは敵と味方で共通化できそうな仕様なので、基底クラスでまとめるものとします
すると、以下のような設計になるでしょう

C#(12.0)

1using System; 2 3public abstract class Status 4{ 5 protected int hp,power; 6 7 protected Status(int hp,int power) 8 =>(this.hp,this.power)=(hp,power); 9} 10 11public abstract class Fighter(int hp,int power):Status(hp,power) 12{ 13 public void Attack(Fighter target) 14 =>target.hp-=base.power; 15} 16 17public class Player(int hp,int power):Fighter(hp,power); 18 19public class Enemy(int hp,int power):Fighter(hp,power);

ここでインターフェイスとして機能するのは二つの抽象クラスです
抽象クラスはサブクラスのインスタンスとセットで実体を持つため、インスタンス毎の固有のパラメータを表すのに役立ちます
このようにインターフェイス型を用いずとも、インターフェイスは定義できることが分かります
インターフェイス型を用いることがインターフェイスを設計することではないということです

C#ではこの他にも、静的クラスというクラス定義が存在します
例えば、アイテムのインベントリ管理は以下のように表すこともできるでしょう

C#(12.0)

1using System; 2using System.Collections.Generic; 3using System.Linq; 4 5public abstract class Status 6{ 7 public readonly ItemList items=new(); 8} 9 10public abstract class Fighter:Status; 11 12public class Player:Fighter; 13 14public class Item(string name) 15{ 16 public string Name=>name; 17} 18 19public sealed class ItemList 20{ 21 List<Item> items; 22 public int Max=>items.Count; 23 24 public Item this[int index]=>items[index]; 25 26 public ItemList(List<Item> items=null) 27 =>this.items=items ?? new(); 28 29 public void Push(Item item)=>items.Add(item); 30 31 public void Pop(Item item)=>items.Remove(item); 32 33 public override string ToString()=>string.Join("\n",items.Select(item=>"Item:"+item.Name)); 34} 35 36public sealed class ItemBox 37{ 38 public readonly ItemList items; 39 40 public ItemBox(params Item[] items) 41 =>this.items=new(new(items)); 42} 43 44public static class ItemCommand 45{ 46 public static void ItemGet(this Player player,ItemBox box,Item item) 47 { 48 box.items.Pop(item); 49 player.items.Push(item); 50 } 51 52 public static void ItemRemove(this Player player,Item item) 53 =>player.items.Pop(item); 54}

この実装を用いて、アイテムの取得と破棄を表すコードを書いたとします

C#(12.0)

1using System; 2using System.Collections.Generic; 3using System.Linq; 4 5public class Program 6{ 7 public static void Main() 8 { 9 ItemBox box=new(new("A"),new("B"),new("C")); 10 Player player=new(); 11 player.ItemGet(box,box.items[default]); 12 player.ItemGet(box,box.items[box.items.Max-1]); 13 Console.WriteLine("PlayerItem\n"+player.items+"\n"); 14 player.ItemRemove(player.items[player.items.Max-1]); 15 Console.WriteLine("ItemBox\n"+box.items+"\n"); 16 Console.WriteLine("PlayerItem\n"+player.items); 17 } 18}

このコードからは以下の結果が得られます

PlayerItem Item:A Item:C ItemBox Item:B PlayerItem Item:A

これらは簡易的な実装なので、どのような設計を目指すかによって実際のコードはより複雑化するでしょう
ただ、重要なことはポリモーフィズムを体現するためにインターフェイスをどう扱うかではなく、どう表現するかです
そのためにインターフェイス型が必要ならば使うまでであり、代替手段があるならばそれを検討してみることによって、実装がスマートになるかもしれません

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

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

#7

fana

総合スコア11954

投稿2023/12/25 04:57

後々楽ができる

というのが今現在明確に見えているなら良いのではないかと.
近い将来にそれを使う想定があるから→用意しとく っていう.

後々楽ができるであろうし

の「であろう」の強さの問題かな,と.
で,そこがちょっと弱いようなら,強くなってからやるのでもいいよね,っていうのが私の意見かな.

ま,とにかく

オブジェクト指向の作法

とかいう謎の話を考えるのではなく,「俺が 必要だから こうする」でいいんじゃないかな.
他のやり方との比較とか良し悪しみたいな話はあるだろうけど,それは「作法的にどうの」とかじゃなく具体的理由で考える.

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

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

#8

DanDan244

総合スコア60

投稿2023/12/26 01:49

アイテムを保持するという観点で、キャラクターと宝箱を同じインターフェイスを実装するのは少し仰々しい気がします
これを実装するとアイテムを保持するものすべてに IInventoryHolder を実装する必要があるという観念にとらわれます

分析レベルだとそこまで考えるもどうかと思います
設計レベルで考えるなら、IInventoryHolder を使って、第三者が何か振舞いをおこすかどうかを考えた方がよい気がします

例えばシステムが毎日キャラクターと宝箱に1アイテムずつ追加を行う
みたいな要件があったとすれば、このインターフェイスを使って、キャラクターと宝箱を抽象化するのもよいと思いますが、
なかなかキャラクターと宝箱を同列に扱う振舞いはないんじゃないかなと思います

ケースバイケースではありますが、私が実装する場合はこうした実装はしないかなと思います

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

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

#9

yussanstar

総合スコア15

投稿2023/12/26 02:10

編集2023/12/26 02:13

他の方がおっしゃるように、実装をするかどうかに関しては、後々楽かどうかよりも、
今それが必要かどうかで判断すべきだと思います。
「YAGNI(You Aren't Gonna Need It)」の原則という言葉もあります。

今回はその実装が必要であるとして、インターフェースを用いることが適切かどうかですが、
インターフェースの持つ「依存関係逆転」というメリットを軸に考えてはいかがでしょうか。

私が悩んでいるのはインベントリというクラスを包含(保持)しているという点でしか共通点のないオブジェクトに対してその共通点を抽出してインターフェイスとして実装することの是非なんです。

質問者さんはこうおっしゃっていますが、これがちょっと違うと思っていて、
インターフェースを導入することのメリットは、内部実装に関係なく、機能の外側だけを共通化できるという点です。

例えば、今はCharacterとTreasureBoxで共通のInventoryというものを持っていますが、
今後の仕様変更等で「やはりこれら2つが持つInventoryは性質が違う!」という話になったとします。
その結果、CharacterはCharacterInventory、TreasureBoxはTreasureBoxInventoryを持つという設計に変更されたとします。

インターフェースの本領が発揮されるのはこのような場合です。
中の実装がどんなに変わっても、外側から見ればIInventoryHolderのAddItemなどの関数を呼べばいいだけですから、内部実装を気にする必要が全くないわけです。

例えばですが、以下のような全オブジェクトにアイテムを与える処理があった場合、インターフェースの内容に変更がない限り、この処理自体は全く書き換えずに済みます。

C#

1foreach (var obj in mapObjects) 2{ 3 if (obj is IInventoryHolder invObj) 4 { 5 invObj.AddItem(commonItem); 6 } 7 }

これがいわゆる、依存性逆転の原則(SOLIDの原則のD)というやつです。

共通点があるからインターフェースとして切り出すというよりは、
内部の共通点の有無に関わらず、持っておいて欲しい関数があるからインターフェース化するというイメージではないでしょうか。

このような内部実装の変更に耐性のあるコードが書けるというメリットが活かせそうなら、
インターフェース化を検討していいのではないでしょうか。

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

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

#10

TokoToko123

総合スコア25

投稿2023/12/26 14:11

質問中の例においては、インターフェイスを実装する意味は薄いかもしれません
インスタンスを限定的に公開するだけでも解決します

C#

1public class Character 2{ 3   public readonly Inventory _inventory = new Inventory(); 4} 5 6public class TreasureBox 7{ 8 public readonly Inventory _inventory = new Inventory(); 9}

これでインスタンスは固有となるので、あとはフィールドからメソッドを呼び出すだけです
HasItem(Item item)も、内部でreturn ReadOnlyItemList.Contains(item);と実行するような単純な実装なら、Inventoryに含めても問題はないかもしれません
いずれにせよ、どのような多態性を目指すかによるでしょう
単に共通要素をリストアップするだけなら、アクセス手段を統一するのも一手です

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

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

最新の回答から1ヶ月経過したため この意見交換はクローズされました

意見をやりとりしたい話題がある場合は質問してみましょう!

質問する

関連した質問