Skip to content

บทที่ 6 — Inheritance + Polymorphism (interface, abstract, sealed)

← บทที่ 5 | สารบัญ | บทที่ 7: Collections + Generics →

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 แม้ตัวแปรจะประกาศเป็น Animal

key:

  • : 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 classInterface
สืบทอดได้หลายตัว (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, ShapeIShape, 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

  1. ลืมใส่ virtual ตอนออกแบบ แล้วเพิ่มทีหลัง = breaking change (ทำให้โค้ดที่ใช้อยู่พัง) — สำคัญมากสำหรับ library API ที่คนอื่นใช้
  2. method ที่ inherit ไม่ override → behavior ของ base — อาจ surprise
  3. Diamond problem (ปัญหาที่ class ลูกสืบทอด 2 ทางจาก class แม่เดียวกัน จนไม่รู้ว่าจะใช้ method ของสายไหน) — C# ไม่เจอปัญหานี้เพราะ class สืบทอดได้แม่เดียว (single inherit) แต่ถ้า default interface method จากหลาย interface ชนกัน ต้อง implement แบบ explicit เอง
  4. new keyword hide → confused ทุกครั้ง
  5. base.M() ใน override ลืม → base logic หาย
  6. 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 — ลงมือทำ

  1. สร้าง abstract Vehicle + subclass Car, Bike, Truck — แต่ละตัวมี MaxSpeed() abstract
  2. interface IAppLogger + 2 implementation: ConsoleLogger, FileLogger
    • ตั้งชื่อ IAppLogger ไม่ใช่ ILogger เพราะ ILogger ชนกับ Microsoft.Extensions.Logging.ILogger ที่มีใน .NET
  3. ใช้ DI — class OrderProcessor(IAppLogger logger) รับ logger ใด ๆ ก็ได้
    • class X(Y param) = primary constructor (C# 12+) ที่เรียนในบท 5 — ถ้าลืม ดูตัวอย่างในบท 5 หัวข้อ 4
  4. ลองทำ Strategy pattern: IShipping กับ implementation 3 ตัว — แล้วเปลี่ยนรูปแบบ shipping ตาม country
  5. ลองทำ inheritance chain ลึก 4 ชั้น (A → B → C → D) แล้วลอง override + base call — มี trace ยังไงบ้าง

สรุปบทที่ 6

  • : BaseClass = inherit (single inheritance), implement หลาย interface
  • virtual + override = polymorphism
  • abstract = สัญญา + shared code
  • interface = capability — ใช้สำหรับ DI / Strategy / API contract
  • sealed = หยุด inheritance
  • Composition > Inheritance ในกรณีส่วนใหญ่
  • ใช้ is pattern matching ที่สุดสำหรับ casting

→ บทที่ 7: Collections + Generics