C# OOPの理解
継承とインターフェースは、オブジェクト指向プログラミング(OOP)の不可欠な要素です。 ティム・コーリーは、ビデオ"Inheritance vs Interfaces in C#:オブジェクト指向プログラミング"では、どのような場合に継承を使用し、どのような場合にインターフェイスを選択するかについて詳しく説明しています。
この記事は、ティム・コーリーのビデオの包括的なガイドとして役立ちます。 ビデオで提供されている主要な概念、例、コードの説明を分解し、継承とインターフェースの違いや、それぞれを使用するタイミングを強調します。
はじめに
(0:00)のTimは、まず継承とインターフェースを区別することの重要性を強調します。 彼は、最良の結果を得るために、それぞれの概念をいつ使うべきかを理解する必要性を強調しています。 彼の目的は、間違った単一継承の使い方から始まり、それを修正する例を通して、これを実証することである。
プロジェクトの作成
(1:08)で、ティムは.NET 5を使ってシンプルなコンソール・アプリケーションを作成します。彼はこのプロジェクトを "OODemoApp "と名付け、第一の目標は生産可能なコードを作成することではなく、コンセプトを実証することであると説明しています。
継承の理解
Timは(1:55)で継承の基本を掘り下げています。 彼は継承を、基本クラスのプロパティとメソッドが派生クラスに継承されるメカニズムであると定義しています。 彼は、継承は単にコードの再利用や共有のために使うのではなく、論理的な"is-a"関係を確立するために使うべきだと強調している。
キーポイント
- Is-a関係:派生クラスがベースクラスの型であることを保証します。
- 共通のロジック:継承されたクラスは、プロパティやメソッドのシグネチャだけでなく、共通のロジックを共有しなければなりません。
クラス例:レンタカー
(7:52)のTimは、継承の基本的な概念を説明するためにRentalCarクラスを作成します。 このクラスは、フロリダ州マイアミのレンタカー会社のレンタカーを表しています。
public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}public class RentalCar
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public int NumberOfPassengers { get; set; }
public void StartEngine()
{
Console.WriteLine("Turn key to ignition setting");
Console.WriteLine("Turn key to on");
}
public void StopEngine()
{
Console.WriteLine("Turn key to off");
}
}不正確な継承の落とし穴を実証する
ティムは(10:15)で、継承の不適切な使用がどのような問題につながるかを説明しています。 継承の使い方を誤ると、管理や拡張が難しいコードになりかねないことを強調している。 彼は、コードを共有するためだけに継承を使用しないようアドバイスしている。
車とトラックの型に列挙型を追加する
(10:45)のTimは、車種の列挙を追加している。彼はEnums.csという名前の新しいクラスファイルを作成し、すべての列挙型を一箇所にまとめています。 この列挙は、異なる車のスタイルを区別するのに役立ちます。
// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}// Enums.cs
public enum CarType
{
Hatchback,
Sedan,
Compact
}次に、RentalCar クラスに車のタイプを指定するプロパティを追加します。
public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}public class RentalCar : RentalVehicle
{
public CarType Style { get; set; }
// Other properties and methods
}トラックへの拡大
(12:27)でティムが説明しているように、このレンタル業者はトラックを追加することを決定しました。 彼は、親クラス RentalVehicle を継承する RentalTruck クラスを作成します。
public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}public class RentalTruck : RentalVehicle
{
public TruckType Style { get; set; }
// Other properties and methods
}次に、トラック・タイプの新しい列挙型を定義する。
// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}// Enums.cs
public enum TruckType
{
ShortBed,
LongBed
}異なるプロパティタイプを扱う
Timは(15:28)で、2つのプロパティが同じ名前を共有しているからといって、それらが同じであるとは限らないことを強調しています。 彼は、異なる列挙型(自動車はCarType、トラックはTruckType)を意味する可能性のあるStyleプロパティでこれを説明しています。
ボートを紹介する
レンタル業者がボートをレンタルすることになりました。 Timは、RentalBoatクラスを作成することで、これを処理する方法を示します。 当初は、ボートは車やトラックといくつかの特性を共有できるため、何とかなりそうです。
public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}public class RentalBoat : RentalVehicle
{
// Properties and methods specific to boats
}帆船を扱う
ヨットにはエンジンがないため、ヨットの紹介が課題となります。 19:57)のティムは、このシナリオにおける継承の限界を説明している。
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}インターフェイスで問題を解決する
Tim は、StartEngine と StopEngine メソッドを基底クラスで仮想化し、これらのメソッドを使用しない派生クラスでオーバーライドできるようにすることを提案しています。
public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}public abstract class RentalVehicle
{
// Common properties
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}呼び出されるべきではないメソッドを処理する
Timは(21:56)で、継承されたクラスに呼び出されるべきでないメソッドを持つことの落とし穴について説明している。 エンジンを持たないRentalSailboatクラスの例では、RentalVehicleクラスからStartEngineメソッドとStopEngineメソッドを継承しています。 これらのメソッドが意図せずに呼び出された場合、適用できないことを示す例外を投げる必要があるため、この状況は問題につながる可能性があります。
public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}public class RentalSailboat : RentalVehicle
{
public override void StartEngine()
{
throw new NotImplementedException("I do not have an engine to start");
}
public override void StopEngine()
{
throw new NotImplementedException("I do not have an engine to stop");
}
}継承の限界を認識する
(24:06)のティムは、継承が論理的な意味をなさなくなったとき、いかに複雑で厄介なコードベースになるかを強調している。 たとえば、ヨットをエンジン付きのRentalVehicleとして扱うべきではありません。これは、継承の限界と、より良い設計アプローチの必要性を示しています。
インターフェイスでより良いデザインへ
これらの問題に対処するために、ティムはインターフェイスを使ったより良いデザインを提案しています。 彼は、改善されたアプローチを示すために、"BetterOODemo "という新しいコンソール・アプリケーション・プロジェクトを作成することから始めます。
インターフェイスの定義
Timは、すべてのレンタルに共通するプロパティをカプセル化するIRentalインターフェイスを紹介します。
public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}public interface IRental
{
int RentalId { get; set; }
string CurrentRenter { get; set; }
decimal PricePerDay { get; set; }
}陸上車両の基本クラスを作成する
ティムは次に、陸上車両用の基本クラスを作成し、車両レンタルという概念を車両自体から切り離します。
public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}public abstract class LandVehicle
{
public int NumberOfPassengers { get; set; }
public virtual void StartEngine()
{
Console.WriteLine("Engine started");
}
public virtual void StopEngine()
{
Console.WriteLine("Engine stopped");
}
}ベースとなる車両クラスの名前を LandVehicle に変更することで、Tim は適切な車両のみがエンジン関連のメソッドを継承するようにします。
自動車クラスとトラッククラスの実装
Timは、LandVehicleを継承し、IRentalインタフェースを実装するCarクラスとTruckクラスを作成します。
public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}public class Car : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public CarType Style { get; set; }
}
public class Truck : LandVehicle, IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
public TruckType Style { get; set; }
}この設計では、各クラスがその型に関連するプロパティとメソッドのみを持つようにし、明確な関係分離を維持します。
コードの重複を避ける
Timは(31:41)で、不必要なコードの重複を避けることの重要性について述べています。 IRentalインターフェイスは複数のクラスで同じプロパティを必要とするが、これはDRY(Don't Repeat Yourself)原則の違反とは見なされない。
レンタル帆船クラスを実装する
(35:09)のTimは、LandVehicleを継承する代わりにIRentalインターフェースを実装することで、RentalSailboatクラスを個別に処理する方法を説明しています。 このアプローチは、不適切な継承に伴う落とし穴を避けるのに役立ちます。
public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}public class Sailboat : IRental
{
public int RentalId { get; set; }
public string CurrentRenter { get; set; }
public decimal PricePerDay { get; set; }
// Additional properties and methods specific to sailboats
}さまざまなレンタルを管理する
Timは、トラック、ヨット、車などのさまざまなレンタルタイプを保存するためにIRentalインターフェイスを利用して、さまざまなタイプのレンタルを管理するためのリストを設定します。
List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};List<IRental> rentals = new List<IRental>
{
new Truck { CurrentRenter = "Truck Renter" },
new Sailboat { CurrentRenter = "Sailboat Renter" },
new Car { CurrentRenter = "Car Renter" }
};この設計により、レンタルのループ処理やRentalIdのような共通プロパティへのアクセスが可能になります。
foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}foreach (var rental in rentals)
{
Console.WriteLine($"Renter: {rental.CurrentRenter}, Price Per Day: {rental.PricePerDay}");
}特定の型へのキャスト
異なるレンタルタイプの特定のプロパティやメソッドにアクセスするために、Timはisキーワードを使用してオブジェクトをそれぞれのタイプにチェックしてキャストする方法を示しています。
foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}foreach (var rental in rentals)
{
if (rental is Truck truck)
{
Console.WriteLine($"Truck Style: {truck.Style}, Passengers: {truck.NumberOfPassengers}");
}
else if (rental is Sailboat sailboat)
{
// Access sailboat-specific properties
}
else if (rental is Car car)
{
// Access car-specific properties
}
}インターフェイスによる柔軟性
ティムは、インターフェイスを使用することで、将来の変更に柔軟に対応できることを強調しています。 例えば、戦車やテレビのような新しいタイプのレンタルを追加しても、既存の構造が崩れることはありません。
継承の使いすぎを避ける
Timは、コード共有のために継承を使いすぎると、複雑で融通の利かないコードベースになる可能性があることを忠告しています。 その代わりに、"is-a"関係を論理的な範囲を超えて引き伸ばすことなく、望ましい結果を達成するためにインターフェースとコンポジションを活用することを勧めている。
結論
ティム・コーリーによるOOPにおける継承とインターフェースの説明は、保守可能で柔軟なコードを作成するための明確な道筋を提供します。 よくある落とし穴を紹介し、洗練されたデザインのインターフェイスを提供することで、開発者がアプリケーションを効果的に構成するために、十分な情報に基づいた決定を下せるようにする。
これらのコンセプトをより深く掘り下げ、実際に使用している様子を見るには、ティムの完全なビデオをご覧ください。 チャンネルは、プログラミング・チュートリアルの宝庫です。 お見逃しなく!

