継承、構成、集約

いくつかの新しいトピック、コンセプト、プログラミングツールを扱うことに決めたことがよくあります。私はインターネット上のさまざまなサイトで次々と記事を読みました。 また、トピックが複雑な場合、これらの記事を読んで理解に一歩近づかないかもしれません。 そして、突然、洞察を与え、すべてのパズルがまとめられた記事があります。 そのような記事を他の記事と区別するものを判断することは困難です。 正しく選択された単語、最適なプレゼンテーションロジック、またはより関連性の高い例。 私の記事がC#の新しい単語または最高の教育記事であると判明するふりはしません。 しかし、おそらく誰かにとっては、議論される概念を理解し、記憶し、正しく適用し始めることができるものになるでしょう。



オブジェクト指向プログラミング言語では、クラス間の相互作用を整理する3つの方法があります。 継承とは、継承クラスに親クラスのすべてのフィールドとメソッドがあり、原則としていくつかの新しい機能または/およびフィールドが追加される場合です。 継承は「is」という言葉で記述されます。 乗用車は車です。 彼が彼の相続人になるならば、それは自然です。



```class Vehicle { bool hasWheels; } class Car : Vehicle { string model = "Porshe"; int numberOfWheels = 4 }```
      
      





関連付けとは、あるクラスがフィールドの1つとして別のクラスを含む場合です。 関連付けは、「has」という言葉で説明されます。 車にはエンジンがあります。 彼がエンジンの相続人にならないのは当然です(ただし、状況によってはこのようなアーキテクチャも可能です)。



関連付けの2つの特定のケースが区別されます:構成と集約。



構成とは、エンジンが車とは別に存在しない場合です。 車の作成時に作成され、車によって完全に制御されます。 典型的な例では、エンジンインスタンスは自動車デザイナーで作成されます。



 ``` class Engine { int power; public Engine(int p) { power = p; } } class Car { string model = "Porshe"; Engine engine; public Car() { this.engine = new Engine(360); } } ```
      
      





集約とは、エンジンインスタンスがコード内の別の場所で作成され、パラメーターとしてカーデザイナーに渡される場合です。



 ``` class Engine { int power; public Engine(int p) { power = p; } } class Car { string model = "Porshe"; Engine engine; public Car(Engine someEngine) { this.engine = someEngine; } } Engine goodEngine = new Engine(360); Car porshe = new Car(goodEngine); ```
      
      





クラス間の相互作用を整理する特定の方法の利点については議論がありますが、抽象的な規則はありません。 開発者は、基本ロジック(「is」または「has」)に基づいて何らかの方法を選択しますが、これらの方法が与えるおよび課す可能性と制限も考慮します。 これらの機能と制限を確認するために、例を作成してみました。 コードがコンパクトなままであるようにシンプルであるだけでなく、3つのメソッドすべてを1つのプログラム内で適用できるように十分に開発されています。 そして、最も重要なことは、この例をできるだけ抽象的なものにしようとしたことです。すべてのオブジェクトとインスタンスは理解可能であり、具体的なものです。



簡単なゲーム、タンクバトルを書きましょう。 2つの戦車が遊んでいます。 彼らは健康をゼロに落とした人を交互に撃ち、失います。 ゲームにはさまざまな種類のシェルとアーマーがあります。 ダメージを与えるためには、まず敵の戦車を攻撃し、次に敵の鎧を突破する必要があります。 アーマーが破損していない場合、損傷はありません。 ゲームのロジックは、「石paper紙」の原則に基づいています。つまり、あるタイプの鎧は特定のタイプのシェルには十分耐性がありますが、他のシェルは不十分です。 さらに、鎧をよく貫通するシェルは小さな「鎧」ダメージを与え、逆に、最も「致命的な」シェルは鎧を貫通する可能性が低くなります。



銃の簡単なクラスを作成しましょう。 口径とバレル長の2つのプライベートフィールドがあります。 ダメージは口径に依存し、一部は装甲を突破する能力に依存します。 バレル長から-精度。



 ``` public class Gun { private int caliber; private int barrelLength; } ```
      
      





銃のコンストラクターも作成します。



 ``` public Gun(int cal, int length) { this.caliber = cal; this.barrelLength = length; } ```
      
      





他のクラスから口径を取得するためのメソッドを作成しましょう:



 ``` public int GetCaliber() { return this.caliber; } ```
      
      





ターゲットをヒットするためには、ターゲットをヒットすることとアーマーを突破することの2つのことを行う必要があることを覚えていますか? したがって、最初の攻撃は銃が担当します。ヒットです。 そのため、ブール変数メソッドIsOnTargetを作成します。これは、ランダム変数(サイコロ)を取り、結果を返します。ヒットするかどうか:



 ``` public bool IsOnTarget(int dice) { return (barrelLength + dice) > 100; } ```
      
      





銃のクラス全体は次のとおりです。



 ``` public class Gun { private int caliber; private int barrelLength; public Gun(int cal, int length) { this.caliber = cal; this.barrelLength = length; } public int GetCaliber() { return this.caliber; } public bool IsOnTarget(int dice) { return (barrelLength + dice) > 100; } } ```
      
      





ここでシェルを作成します-これは継承を適用​​する最も明らかなケースですが、その中での集約も適用可能です。 シェルには独自の特性があります。 一部の仮想シェルは存在しません。 したがって、クラスを抽象化します。 彼を文字列フィールド「タイプ」にします。



砲弾は砲用に作られています。 特定の銃。 ある口径の発射体は、別の口径の大砲を発射しません。 したがって、発射物フィールドリンクを銃のインスタンスに追加します。 コンストラクタを作成します。



 ``` public abstract class Ammo { Gun gun; public string type; public Ammo(Gun someGun, string type) { gun = someGun; this.type = type; } } ```
      
      





ここでは集計を適用しました。 どこかに銃が作成されます。 次に、銃へのポインタを持つシェルがこの銃用に作成されます。



特定の種類のシェルは、抽象シェルの相続人になります。 相続人は単に親のメソッドを継承できますが、オーバーライドすることもできます。つまり、親メソッドとは異なる動作をします。 しかし、発射体には多くのメソッドが必要であることは確かです。 発射物はダメージを与えなければなりません。 GetDamageメソッドは、単に3倍した口径を返します。 一般に、発射体の損傷は口径に依存します。 しかし、このメソッドは子クラスで再定義されます(鎧をよく貫通するシェルは、通常「鎧」のダメージが少ないことを覚えておいてください。子クラスでメソッドを再定義できるようにするには、virtualという言葉を使用します。



 ``` public virtual int GetDamage() { //TO OVERRIDE: add logic of variable damage depending on Ammo type return gun.GetCaliber()*3; } ```
      
      





発射体は、鎧を貫通する必要があります(または少なくとも貫通しようとします)。 一般的に、装甲を貫通する能力は口径にも依存します(たとえば、初期速度などですが、複雑にすることはありません)。 したがって、メソッドは口径を返します。 つまり、大まかに言って、発射体はその口径と同じ厚さの装甲を貫通できます。 このメソッドは、子クラスではオーバーライドされません。



 ``` public int GetPenetration() { return gun.GetCaliber(); } ```
      
      





さらに、便利なデバッグとコンソール出力の整理のために、ToStringメソッドを追加することは理にかなっています。これにより、単純にシェルの種類と口径を確認できます。



 ``` public override string ToString() { return $" " + type + "    " + gun.GetCaliber(); } ```
      
      





次に、抽象的なシェルを継承するさまざまなタイプのシェルを作成します:高爆発性、累積、サブキャリバー。 高爆発物は最大のダメージを与え、累積-少ない、準口径-さらに少ない。 子クラスにはフィールドがなく、ベースシェルのコンストラクターを呼び出し、それに銃と文字列型を渡します。 GetDamage()メソッドは子クラスでオーバーライドされます。デフォルトと比較して損傷を増加または減少させる係数が追加されます。



高爆発性(デフォルトの損傷):



 ``` public class HECartridge : Ammo { public HECartridge(Gun someGun) : base(someGun, "") { } public override int GetDamage() { return (int)(base.GetDamage()); } } ```
      
      





累積(デフォルトのダメージx 0.6):



 ``` public class HEATCartridge : Ammo { public HEATCartridge(Gun someGun) : base(someGun, "") { } public override int GetDamage() { return (int)(base.GetDamage() * 0.6); } } ```
      
      





サブキャリバー(デフォルトのダメージx 0.3):



 ``` public class APCartridge : Ammo { public APCartridge(Gun someGun) : base(someGun, "") { } public override int GetDamage() { return (int)(base.GetDamage() * 0.3); } } ```
      
      





オーバーライドされたGetDamageメソッドは、基本クラスメソッドも呼び出すことに注意してください。 つまり、メソッドをオーバーライドすることにより、baseキーワードを使用してデフォルトのメソッドにアクセスする機能も保持されます)。



そのため、シェルには集約(基本クラスの銃)と継承の両方を使用しました。

次に、戦車の装甲を作成します。 ここでは継承のみが適用されます。 鎧には厚さがあります。 したがって、抽象鎧クラスには、厚さフィールドと、子クラスの作成時に定義されるタイプ文字列フィールドがあります。



 ``` public abstract class Armour { public int thickness; public string type; public Armour(int thickness, string type) { this.thickness = thickness; this.type = type; } } ```
      
      





ゲーム内の鎧は、それらが壊れているかどうかを判断します。 したがって、予約のタイプに応じて、子会社で再定義されるメソッドは1つだけになります。



 ``` public virtual bool IsPenetrated(Ammo projectile) { return projectile.GetDamage() > thickness; } ```
      
      





そして、それらが壊れているかどうかは、どの発射体が到着したかによって異なります。デフォルトの場合、どの口径の場合です。 したがって、このメソッドは発射物のインスタンスを取得し、ブール値の結果(破損しているかどうか)を返します。 いくつかのタイプのアーマー-抽象アーマーの相続人を作成しましょう。 コードの種類は1つだけです。ロジックはシェルとほぼ同じです。 同種の装甲は、高爆発性のシェルをよく保持しますが、不十分です-サブキャリバー。 したがって、高い貫通力を持つサブキャリバーの発射体が到着すると、計算では装甲が薄くなります。 など:各タイプのアーマーには、特定の発射体に対する抵抗係数の独自のセットがあります。



 ``` public class HArmour : Armour { public HArmour(int thickness) : base(thickness, "") { } public override bool IsPenetrated(Ammo projectile) { if (projectile is HECartridge) { // ,      return projectile.GetPenetration() > this.thickness * 1.2; } else if (projectile is HEATCartridge) { // ,     return projectile.GetPenetration() > this.thickness * 1; } else { // ,     return projectile.GetPenetration() > this.thickness * 0.7; } } } ```
      
      





ここでは、ポリモーフィズムがもたらす驚異の1つを使用します。 このメソッドはあらゆる発射物を受け入れます。 署名は、子クラスではなく基本クラスを示します。 しかし、メソッド内では、どのような種類のシェルが飛んだか、つまりどのタイプかがわかります。 これに応じて、このロジックまたはそのロジックを実装します。 シェルに継承を適用​​せず、シェルのタイプの3つの一意のクラスを作成しただけの場合、アーマーを突破するための別のテストを手配する必要があります。 ゲーム内のシェルのタイプと同数のオーバーロードメソッドを記述し、到着したシェルの種類に応じてそのうちの1つを呼び出す必要があります。 これもかなりエレガントですが、この記事のトピックには関係ありません。



これでタンクを作成する準備ができました。 タンクには継承はありませんが、構成と集約があります。 もちろん、戦車には名前があります。 戦車には銃があります(集合体)。 私たちのゲームでは、各移動の前に戦車が鎧を「変更」できると仮定します-いずれかの種類の鎧を選択します。 このため、戦車には鎧の種類のリストがあります。 戦車には弾薬庫があります。これは、戦車の設計者(作曲!)で作成された砲弾で満たされる砲弾のリストです。 戦車は健康状態になり(命中すると減少します)、戦車には現在選択されている鎧と現在選択されている発射物があります。



 ``` public class Panzer { private string model; private Gun gun; private List<Armour> armours; private List<Ammo> ammos; private int health; public Ammo LoadedAmmo { get; set; } public Armour SelectedArmour { get; set; } } ```
      
      





戦車設計者が多かれ少なかれコンパクトであり続けるために、適切な厚さの3種類の装甲を追加し、弾薬パックに3種類のそれぞれのシェルを10個充填する2つの補助的なプライベートメソッドを作成します。



 ``` private void AddArmours(int armourWidth) { armours.Add(new SArmour(armourWidth)); armours.Add(new HArmour(armourWidth)); armours.Add(new CArmour(armourWidth)); } private void LoadAmmos() { for(int i = 0; i < 10; i++) { ammos.Add(new APCartridge(this.gun)); ammos.Add(new HEATCartridge(this.gun)); ammos.Add(new HECartridge(this.gun)); } } ```
      
      





戦車設計者は次のようになります。



 ``` public Panzer(string name, Gun someGun, int armourWidth, int h) { model = name; gun = someGun; health = h; armours = new List<Armour>(); ammos = new List<Ammo>(); AddArmours(armourWidth); LoadAmmos(); LoadedAmmo = null; SelectedArmour = armours[0]; //  -   }```
      
      





ここで再び多型の可能性を使用していることに注意してください。 リストにはAmmoデータタイプ(親シェル)があるため、私たちの弾薬はあらゆるタイプのシェルを保持します。 継承されず、独自の種類のシェルを作成した場合、シェルの種類ごとに個別のリストを作成する必要があります。



戦車のユーザーインターフェイスは、装甲の選択、銃の装填、射撃の3つの方法で構成されています。



アーマーを選択:



 ``` public void SelectArmour(string type) { for (int i = 0; i < armours.Count; i++) { if (armours[i].type == type) { SelectedArmour = armours[i]; break; } } } ```
      
      





銃を充電します。



 ``` public void LoadGun(string type) { for(int i = 0; i < ammos.Count; i++) { if(ammos[i].type == type) { LoadedAmmo = ammos[i]; Console.WriteLine("!"); return; } } Console.WriteLine($", , " + type + " !"); } ```
      
      





冒頭で述べたように、この例では、常に念頭に置いておく必要のある抽象的な概念から遠く離れようとしました。 したがって、私たちと一緒の発射物の各インスタンスは、戦闘の前に戦闘パックに入れられた物理的な発射物と同じです。 その結果、シェルは最も不適切な瞬間に終了する可能性があります!



撮影するには:



 ``` public Ammo Shoot() { if (LoadedAmmo != null) { Ammo firedAmmo = (Ammo)LoadedAmmo.Clone(); ammos.Remove(LoadedAmmo); LoadedAmmo = null; Random rnd = new Random(); int dice = rnd.Next(0, 100); bool hit = this.gun.IsOnTarget(dice); if (this.gun.IsOnTarget(dice)) { Console.WriteLine("!"); return firedAmmo; } else { Console.WriteLine("!"); return null; } } else Console.WriteLine(" "); return null; } ```
      
      





ここで-より詳細に。 まず、銃が装填されているかどうかを確認するチェックがあります。 第二に、銃身から飛び出した砲弾はこの戦車にはもはや存在せず、大砲や弾薬庫にはありません。 しかし、物理的にはまだ存在しています-それは目標に向かって飛ぶ。 そして、ヒットすると、アーマーの貫通力とターゲットへのダメージの計算に参加します。 したがって、このシェルを新しい変数Ammo firedAmmoに保存します。 次の行では、この発射体はこの戦車には存在しなくなるため、発射体の基本クラスにIClonableインターフェイスを使用する必要があります。



 ``` public abstract class Ammo : ICloneable ```
      
      





このインターフェイスには、Clone()メソッドの実装が必要です。 ここにあります:



 ``` public object Clone() { return this.MemberwiseClone(); } ```
      
      





これですべてが非常に現実的になりました。ショットが発射されると、サイコロが生成され、銃はIsOnTargetメソッドでヒットを計算し、ヒットがある場合、Shootメソッドは発射物のインスタンスを返し、それが見つからない場合はnullを返します。



戦車の最後の方法は、敵の砲弾が命中したときの挙動です。



 ``` public void HandleHit(Ammo projectile) { if (SelectedArmour.IsPenetrated(projectile)) { this.health -= projectile.GetDamage(); } else Console.WriteLine("  ."); } ```
      
      





再びそのすべての栄光の多型。 シェルが飛んでくる。 どれでも。 選択された装甲と発射体のタイプに基づいて、装甲は突破されるかされません。 ピアスされた場合、特定のシェルタイプのGetDamage()メソッドが呼び出されます。



すべて準備完了です。 コンソール(または非コンソール)出力を記述するだけで、ユーザーインターフェイスが提供され、プレイヤーの交互の動きが実装されます。



まとめると。 継承、構成、および集約を使用するプログラムを作成しましたが、違いを理解し、思い出したことを願っています。 ポリモーフィズムの可能性を積極的に活用しました。まず、子クラスのインスタンスを親のデータ型を持つリストに結合できる場合、そして次に、親インスタンスをパラメーターとして取得するメソッドを作成することで、その中で子のメソッドが呼び出されます テキストの過程で、可能な代替実装-継承を集約に置き換える-について言及しましたが、普遍的なレシピはありません。 実装では、継承により、ゲームに新しい詳細を簡単に追加できました。 たとえば、新しいタイプの発射物を追加するには、次のもののみが必要です。





同様に、別の種類のアーマーを追加するには、このタイプを記述し、ユーザーインターフェイスにアイテムを追加するだけです。 他のクラスまたはメソッドの変更は必要ありません。



以下にクラスの図を示します。







ゲームの最終コードでは、テキストで使用されたすべての「マジックナンバー」が個別の静的構成クラスに配置されます。 コードの任意のフラグメントから静的クラスのパブリックフィールドにアクセスでき、そのインスタンスを作成する必要はありません(不可能です)。 これはどのように見えるかです:



 ``` public static class Config { public static List<string> ammoTypes = new List<string> { "", "", "" }; public static List<string> armourTypes = new List<string> { "", "", "" }; //   - ,    ,      public static int _gunTrashold = 100; //       public static int _defaultDamage = 3; //      public static double _HEDamage = 1.0; public static double _HEATDamage = 0.6; public static double _APDamage = 0.3; //   // : //     ,      -  1.2 public static double _HArmour_VS_HE = 1.2; //     ,      -  1.0 public static double _HArmour_VS_HEAT = 1.0; //     ,      -  0.7 public static double _HArmour_VS_AP = 0.7; //   //     ,      -  1 public static double _Armour_VS_HE = 1.0; //     ,      -  0.8 public static double _Armour_VS_HEAT = 0.8; //     ,      -  1.2 public static double _Armour_VS_AP = 1.2; //   //     ,      -  0.8 public static double _SArmour_VS_HE = 0.8; //     ,      -  1.2 public static double _SArmour_VS_HEAT = 1.2; //     ,      -  1 public static double _SArmour_VS_AP = 1.0; } ```
      
      





そして、このクラスのおかげで、クラスとメソッドをさらに深くすることなく、ここでのみパラメーターを変更して、さらに調整することができます。 たとえば、サブキャリバーの発射体が強すぎることが判明した場合、Configの1桁を変更します。

すべてのゲームコードはここで見ることができます



All Articles