โหมดมืด
บทที่ 6 — Inheritance + Polymorphism (interface, abstract, sealed)
OOP มี 3 ขาหลัก:
- encapsulation (การห่อหุ้มข้อมูล — เรียนในบทก่อน)
- inheritance (การสืบทอด — ให้ class ลูกรับพฤติกรรมจาก class แม่)
- polymorphism (การที่ object ต่างชนิดตอบสนอง method เดียวกันได้ต่างกัน — เช่น เรียก
Speak()บนDogกับCatได้ผลต่างกัน ทั้งที่โค้ดเรียกเหมือนกัน)
บทนี้สอน 2 ขาที่เหลือ — บวกกับ interface (สัญญาว่า type ต้องมี method อะไรบ้าง)
1. Inheritance
Inheritance (การสืบทอด) ให้ class ลูกรับ field/method จาก class แม่มาใช้ต่อ แล้วเพิ่ม/แก้ได้ — ใช้ : ตามด้วยชื่อ class แม่ และ virtual/override เพื่อให้ลูกเขียนทับพฤติกรรมของแม่:
csharp
public class Animal
{
public string Name { get; set; } = "";
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal // : = inherit
{
public override void Speak() => Console.WriteLine("Woof!");
}
public class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow!");
}
Animal a = new Dog { Name = "Rex" };
a.Speak(); // Woof! — polymorphism: C# เลือก method ของ Dog (ไม่ใช่ Animal) เพราะ object จริงคือ Dog แม้ตัวแปรจะประกาศเป็น Animalkey:
: BaseClass= สืบทอด (inherit)virtual= method ที่ลูก override (เขียนทับ) ได้override= เขียนทับ method ของ base (class แม่)- C# = single inheritance (สืบทอดได้แม่เดียว) แต่ implement (ทำตามสัญญา) ได้หลาย interface
2. base keyword
base ใช้อ้างถึง class แม่จากภายใน class ลูก — เรียก constructor ของแม่ (: base(...)) หรือเรียก method เดิมของแม่ที่ถูก override (base.Method()) เพื่อต่อยอดแทนที่จะเขียนใหม่ทั้งหมด:
csharp
public class Animal
{
public string Name { get; }
public Animal(string name) => Name = name; // base ctor มีพารามิเตอร์
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal
{
public Dog(string name) : base(name) { } // ส่งค่าให้ base ctor
public override void Speak()
{
base.Speak(); // เรียก method ของ base
Console.WriteLine("Woof!");
}
}💡 ถ้า base class ไม่มี ctor หรือมีแต่ ctor ว่าง compiler จะเรียก
: base()ให้อัตโนมัติ ไม่ต้องเขียนเอง — ใส่ก็ต่อเมื่อต้อง ส่งค่า เข้าไป
3. Abstract class
csharp
public abstract class Shape
{
public string Color { get; set; } = "";
// abstract method — subclass ต้อง implement
public abstract double Area();
// virtual method — subclass จะ override หรือไม่ก็ได้
public virtual string Describe() => $"{Color} shape, area={Area()}";
}
public class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
}
public class Square : Shape
{
public double Side { get; init; }
public override double Area() => Side * Side;
}
// var s = new Shape(); // ❌ — abstract สร้างไม่ได้
var c = new Circle { Color = "red", Radius = 5 };
Console.WriteLine(c.Describe());abstract = สัญญา + รหัสร่วม:
- abstract method = ต้อง implement
- virtual method = ทางเลือก override
- normal method = ใช้ได้เลย
4. Interface
csharp
public interface IShape
{
double Area();
string Color { get; }
}
// หมายเหตุ: ตัวอย่างนี้ใช้ CircleImpl เพื่อไม่ชนกับ Circle ของ abstract class ในหัวข้อ 3
// ในโปรเจกต์จริงตั้งชื่อ Circle ได้ปกติ — แค่อย่าให้อยู่ใน namespace/file เดียวกัน
public class CircleImpl : IShape
{
public double Radius { get; init; }
public string Color { get; init; } = "red";
public double Area() => Math.PI * Radius * Radius;
}ความต่าง interface vs abstract class:
| Abstract class | Interface | |
|---|---|---|
| สืบทอดได้หลายตัว (multiple inherit) | ❌ (ได้ 1) | ✅ (ได้หลาย) |
| Constructor (ตัวสร้าง) | ✅ | ❌ |
| Instance field (ตัวแปรเก็บข้อมูลในแต่ละ object) | ✅ | ❌ (ไม่มีเลย) |
| Static field (ตัวแปรร่วมของ interface ทั้งหมด — ไม่ใช่ของแต่ละ object) | ✅ | ✅ (C# 8+ — ใช้งานน้อยมากในทางปฏิบัติ) |
| Instance state (สถานะของแต่ละ object) | ✅ | ❌ |
| Default implementation ของ method | ✅ | ✅ (C# 8+) |
| Static abstract member | ❌ | ✅ (C# 11+ — ใช้กับ generic math เขียน operator ที่ใช้ได้กับหลายชนิดตัวเลข, ฟีเจอร์ขั้นสูง ข้ามได้ตอนนี้) |
| ธรรมเนียมการตั้งชื่อ | Animal, Shape | IShape, IDisposable (ขึ้นต้นด้วย I) |
กฎตัดสินใจ:
- "is-a relationship (เป็นชนิดหนึ่งของ...) + มีโค้ดร่วมกัน" → abstract class
- "can-do capability (ทำสิ่งนี้ได้)" → interface
- ถ้าลังเล → interface (ยืดหยุ่นกว่า)
5. Default Interface Methods (C# 8+)
csharp
public interface IGreeter
{
string Name { get; }
// default implementation
string Greet() => $"Hello, {Name}!";
}
public class English : IGreeter
{
public string Name { get; init; } = "";
// ไม่ต้อง implement Greet — ใช้ default
}
// ⚠️ ต้องเรียกผ่านตัวแปรชนิด interface เท่านั้น — default method ไม่ปรากฏบน class
IGreeter g = new English { Name = "Alice" };
Console.WriteLine(g.Greet()); // "Hello, Alice!"
// new English().Greet(); // ❌ compile errorใช้สำหรับ:
- เพิ่ม method ใน interface โดยไม่ทำให้ของเดิม (implementation เก่า) พัง
- Mixin pattern (รูปแบบการผสมความสามารถเข้าไปในหลาย class ผ่าน interface — เช่น เพิ่ม
IPrintable,ISerializableให้ class ต่างชนิดโดยไม่ต้อง inherit จาก class เดียวกัน)
⚠️ อย่าใช้แทน abstract class — interface ที่มี logic เยอะ = ออกแบบผิดทาง
6. Polymorphism — runtime dispatch
csharp
List<Shape> shapes = new()
{
new Circle { Radius = 5 },
new Square { Side = 4 }
};
foreach (var s in shapes)
Console.WriteLine(s.Area()); // dispatch ตาม runtime typeที่ runtime เห็น CLR ดูว่า s ชี้ Circle หรือ Square → เรียก override ที่ตรง
นี่คือ polymorphism — code ที่ "ทำงานกับ Shape" ใช้กับ subclass ได้หมด
7. sealed — หยุดสาย inheritance
csharp
public class A { public virtual void M() {} }
public class B : A { public sealed override void M() {} } // override แต่ "ปิด" override ต่อ
public class C : B { /* M ที่นี่ override ไม่ได้ */ }ใช้:
- หยุด inheritance ที่ไม่ออกแบบมาให้ extend
- JIT optimize ดีขึ้น
8. new modifier — Hide (อย่าใช้!)
new บน method (ต่างจาก override) เป็นการ "ซ่อน" method ของแม่ ไม่ใช่เขียนทับจริง — ทำให้พฤติกรรมขึ้นกับ type ของตัวแปร (ไม่ใช่ object จริง) ซึ่งสับสนและมักเป็น bug จึงควรหลีกเลี่ยง:
csharp
public class A { public void M() => Console.WriteLine("A"); }
public class B : A { public new void M() => Console.WriteLine("B"); }
A x = new B();
x.M(); // A — เพราะ M ของ A ไม่ virtual → static binding (เลือก method จากชนิดตัวแปร ไม่ใช่ object จริง)new keyword "ซ่อน" method ของ base — มัก confused → ใช้ virtual/override แทน
9. Casting
csharp
// ใช้ Animal/Dog แบบ parameterless จากหัวข้อ 1 (ไม่ใช่ Dog(string name) ที่มี constructor รับพารามิเตอร์จากหัวข้อ 2)
Animal a = new Dog();
// 1. is (type check)
if (a is Dog dog) // pattern matching cast
dog.Speak(); // Dog.Speak() = "Woof!" — เรียก method ที่มีใน Dog
// 2. as (cast, null ถ้าไม่ใช่)
Dog? d = a as Dog;
d?.Speak();
// 3. explicit cast
Dog d2 = (Dog)a; // throw InvalidCastException ถ้าไม่ใช่
// 4. type test
Type t = a.GetType();
Console.WriteLine(t.Name); // Dog💡 ใช้
isแบบ pattern matching ที่สุด — ปลอดภัยที่สุด
10. Common Pattern — Strategy
ตัวอย่างการใช้ polymorphism จริง — "Strategy pattern" ให้สลับอัลกอริทึม (เช่นวิธีชำระเงิน) ผ่าน interface ตัวเดียว โดย caller ไม่ต้องรู้ว่าเบื้องหลังเป็น implementation ไหน:
csharp
public interface IPaymentStrategy
{
bool Charge(decimal amount); // return true = สำเร็จ, false = ล้มเหลว
}
public class StripePayment : IPaymentStrategy
{
public bool Charge(decimal amount) { /*...*/ return true; }
}
public class PayPalPayment : IPaymentStrategy
{
public bool Charge(decimal amount) { /*...*/ return true; }
}
public class CheckoutService
{
private readonly IPaymentStrategy _payment;
public CheckoutService(IPaymentStrategy payment) => _payment = payment;
public bool Checkout(decimal total) => _payment.Charge(total);
}
// ใช้งาน:
var svc = new CheckoutService(new StripePayment());
bool ok = svc.Checkout(299.00m);📌 ในโปรเจกต์จริงที่ต้องเรียก network (async) จะเปลี่ยน return type เป็น
Task<bool>และใช้await— สอนในบทที่ 11 (Async + Tasks)
สลับ (swap) implementation (ตัวที่ทำงานจริง) ได้โดยไม่กระทบ caller (ผู้เรียกใช้) — เป็นรากของ Dependency Injection (DI — การฉีดสิ่งที่ต้องพึ่งพาเข้ามาให้จากภายนอก) แนวคิดเริ่มต้นจากตรงนี้ ส่วนการใช้งาน DI จริง ๆ กับ framework (เช่น services.AddScoped) อยู่ในส่วน dotnet/ (ASP.NET Core)
11. Composition over Inheritance
csharp
// interface ที่เขียนเองสำหรับ log — ไม่ต้องพึ่ง NuGet หรือ DI
public interface IAppLogger
{
void Log(string message);
}
public class ConsoleLogger : IAppLogger
{
public void Log(string message) => Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {message}");
}
// ❌ ไม่ดี — inheritance เพื่อ reuse (สืบทอด Dog แค่เพื่อเพิ่ม log)
public class LoggingDog : Dog
{
public LoggingDog(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine($"[{DateTime.Now}] speaking");
base.Speak();
}
}
// ✅ ดีกว่า — composition (ฉีด logger เข้าไปใน SmartDog ตรง ๆ)
// ใช้ชื่อ SmartDog เพื่อไม่ชนกับ Dog ในตัวอย่างก่อนหน้า
public class SmartDog
{
private readonly IAppLogger _logger;
public SmartDog(IAppLogger logger) => _logger = logger;
public void Speak()
{
_logger.Log("speaking");
Console.WriteLine("Woof!");
}
}
// ใช้งาน:
var dog = new SmartDog(new ConsoleLogger());
dog.Speak(); // [10:30:05] speaking \n Woof!Rule: ใช้ inheritance สำหรับ "is-a" จริง ๆ ส่วน reuse อื่น ๆ ใช้ composition
12. Abstract class vs Interface — Decision tree
คำถามยอดฮิต: เมื่อไหร่ใช้ abstract class เมื่อไหร่ใช้ interface? — โดยสรุป interface เน้นสัญญา/พฤติกรรม (implement ได้หลายตัว) ส่วน abstract class แชร์ state/logic ร่วมได้ ตารางตัดสินใจด้านล่างช่วยเลือก:
13. Common Pitfalls
- ลืมใส่
virtualตอนออกแบบ แล้วเพิ่มทีหลัง = breaking change (ทำให้โค้ดที่ใช้อยู่พัง) — สำคัญมากสำหรับ library API ที่คนอื่นใช้ - method ที่ inherit ไม่ override → behavior ของ base — อาจ surprise
- Diamond problem (ปัญหาที่ class ลูกสืบทอด 2 ทางจาก class แม่เดียวกัน จนไม่รู้ว่าจะใช้ method ของสายไหน) — C# ไม่เจอปัญหานี้เพราะ class สืบทอดได้แม่เดียว (single inherit) แต่ถ้า default interface method จากหลาย interface ชนกัน ต้อง implement แบบ explicit เอง
newkeyword hide → confused ทุกครั้งbase.M()ใน override ลืม → base logic หาย- Interface ใหญ่เกินไป (
IUserServiceมี 50 method) → ผิดหลัก ISP- ISP (Interface Segregation Principle — หลักการแยก interface) = ควรแยก interface ให้เล็กเฉพาะเรื่อง อย่ายัดทุกอย่างไว้ตัวเดียว เพราะถ้า interface ใหญ่เกิน class ที่ implement จะต้องเขียน method ที่ตัวเองไม่ได้ใช้ตามไปด้วย
- ISP เป็น 1 ใน 5 หลักของ SOLID (ชุดแนวทางออกแบบ OOP ที่ดี 5 ข้อ ย่อมาจาก Single responsibility, Open/closed, Liskov substitution, Interface segregation, Dependency inversion) — บทนี้แนะนำแค่ ISP เพราะเกี่ยวกับ interface โดยตรง ส่วนอีก 4 ข้อจะเจอเพิ่มเติมเมื่อฝึกออกแบบโปรเจกต์ใหญ่ขึ้น
🛠️ Checkpoint 6 — ลงมือทำ
- สร้าง abstract
Vehicle+ subclassCar,Bike,Truck— แต่ละตัวมีMaxSpeed()abstract - interface
IAppLogger+ 2 implementation:ConsoleLogger,FileLogger- ตั้งชื่อ
IAppLoggerไม่ใช่ILoggerเพราะILoggerชนกับMicrosoft.Extensions.Logging.ILoggerที่มีใน .NET
- ตั้งชื่อ
- ใช้ DI — class
OrderProcessor(IAppLogger logger)รับ logger ใด ๆ ก็ได้class X(Y param)= primary constructor (C# 12+) ที่เรียนในบท 5 — ถ้าลืม ดูตัวอย่างในบท 5 หัวข้อ 4
- ลองทำ Strategy pattern:
IShippingกับ implementation 3 ตัว — แล้วเปลี่ยนรูปแบบ shipping ตาม country - ลองทำ inheritance chain ลึก 4 ชั้น (
A → B → C → D) แล้วลอง override + base call — มี trace ยังไงบ้าง
สรุปบทที่ 6
: BaseClass= inherit (single inheritance), implement หลาย interfacevirtual+override= polymorphismabstract= สัญญา + shared codeinterface= capability — ใช้สำหรับ DI / Strategy / API contractsealed= หยุด inheritance- Composition > Inheritance ในกรณีส่วนใหญ่
- ใช้
ispattern matching ที่สุดสำหรับ casting