💖 Inheritance

🤔 มันคืออะไร ?

คำว่า Inheritance มีใช้อยู่ในหลายวงการเลย แต่ในวงการซอฟต์แวร์ใน Wikipedia ถูกเขียนไว้ว่า

Inheritance is the mechanism of basing an object or class upon another object (prototype-based inheritance) or class (class-based inheritance), retaining similar implementation. Also defined as deriving new classes (sub classes) from existing ones (super class or base class) and forming them into a hierarchy of classes. In most class-based object-oriented languages, an object created through inheritance (a "child object") acquires all the properties and behaviors of the parent object (except: constructors, destructor, overloaded operators and friend functions of the base class). Inheritance allows programmers to create classes that are built upon existing classes,[1] to specify a new implementation while maintaining the same behaviors (realizing an interface), to reuse code and to independently extend original software via public classes and interfaces. The relationships of objects or classes through inheritance give rise to a directed graph. Inheritance was invented in 1969 for Simula.[2]

😑 แค่อ่านก็ปวดกบาลละ แต่ก็ถอดหัวใจสำคัญของมันออกมาได้ว่า

Inheritance คือการสืบทอดคุณสมบัติ จาก Model A ไปยัง Model อื่นๆได้ ซึ่งมันจะช่วยให้เราเพิ่มความสามารถใหม่ๆเข้าไปได้โดยที่ไม่ต้องไปยุ่งกับโค้ดเก่าที่เคยเขียนไว้

🤨 ก็ยัง งง อยู่ดีขอตัวอย่างหน่อย

😢 ปัญหา

สมมุติว่าเราต้องเขียน โปรแกรมบัญชีธนาคาร ซึ่งบัญชีออมทรัพย์สามารถเก็บข้อมูล เงินในบัญชี และ เจ้าของบัญชี ได้ และอย่าลืมนะว่าเราต้อง ฝากเงินเข้าบัญชี ได้ด้วย ดังนั้นเราก็น่าจะได้ Model ออกมาประมาณนี้

public class SavingAccount
{
private double balance;
public double Balance { get => balance; }
public string OwnerName { get; set; }
public void Deposit(double amount)
{
if (amount > 0)
{
balance += amount;
}
}
}

ซึ่งบัญชีมันไม่ได้มีแค่ประเภทเดียวนะ เช่น บัญชีกระแสรายวัน ซึ่งมันก็ต้องเก็บข้อมูล เงินในบัญชี, เจ้าของบัญชี, ฝากเงินเข้าบัญชี ได้เหมือนกันด้วย ดังนั้นเราก็จะสร้าง Model ขึ้นมาอีกตัวหน้าตาประมาณนี้

public class CurrentAccount
{
private double balance;
public double Balance { get => balance; }
public string OwnerName { get; set; }
public void Deposit(double amount)
{
if (amount > 0)
{
balance += amount;
}
}
}

เราจะเห็นว่าโค้ด บัญชีออมทรัพย์ และ บัญชีกระแสรายวัน มันเหมือนกันเลย! ซึ่งถามว่าผิดไหม คำตอบคือไม่ผิดครับ แต่ถ้าเราปล่อยมันไว้แบบนี้เราจะมีปัญหาในอนาคต เช่น ถ้าบัญชีแต่ละประเภทต้องเก็บรหัสประชาชนเข้าไปด้วยล่ะ หรือ เรามาพบทีหลังว่าโค้ดมันเขียนผิดนะ แต่เราก็ copy ไปวางไว้ที่อื่นเรียบร้อยแล้ว

Needless Repetition นี่คือตัวอย่างการเขียนโค้ดที่ไม่ดี นั่นคือการ copy งานที่เหมือนๆกันไปใช้ในแต่ละที่ ซึ่งหลักในการออกแบบที่ดี เราไม่ควรจะทำงานเดิมซ้ำ Don't Repeat Yourself (DRY) ซึ่งเพื่อนๆสามารถศึกษาเรื่อง Bad Code ได้จากบทความ 👶 Code Smells

😄 วิธีแก้ปัญหา

เราสามารถนำหลักการ Inheritance มาช่วยในเรื่องนี้ได้ โดยการสร้าง Model ต้นแบบ ขึ้นมา แล้วทำการย้ายของที่ซ้ำกันไปไว้ในตัวต้นแบบตัวนั้น ซึ่งในตัวอย่างของที่เรากำลังทำอยู่มันคือ บัญชีธนาคาร ดังนั้นเราเลยสร้างคลาส BankAccount ขึ้นมาใหม่

public class BankAccount
{
}

แล้วทำการย้ายของที่มันซ้ำกันจาก บัญชีออมทรัพย์ กับ บัญชีกระแสรายวัน มาไว้ในตัวต้นแบบซะ

public class BankAccount
{
private double balance;
public double Balance { get => balance; }
public string OwnerName { get; set; }
public void Deposit(double amount)
{
if (amount > 0)
{
balance += amount;
}
}
}

สุดท้ายเราก็สั่งให้ บัญชีออมทรัพย์ และ บัญชีกระแสรายวัน ไปสืบทอดความสามารถมาจากตัวต้นแบบที่สร้างไว้

public class SavingAccount : BankAccount
{
}
public class CurrentAccount : BankAccount
{
}

เพียงเท่านี้ บัญชีออมทรัพย์ และ บัญชีกระแสรายวัน ก็จะมีความสามารถทุกอย่างที่ตัวต้นแบบมีนั่นเอง ดังนั้นลองเขียนโค้ดเล่นกับบัญชีทั้ง 2 ตัวของเราดู

var sa = new SavingAccount();
sa.OwnerName = "(Saving) Saladpuk";
sa.Deposit(500);
Console.WriteLine($"{sa.OwnerName}, has THB {sa.Balance}.");
var ca = new CurrentAccount();
ca.OwnerName = "(Current) Saladpuk";
ca.Deposit(700);
Console.WriteLine($"{ca.OwnerName}, has THB {ca.Balance}.");

Output (Saving) Saladpuk, has THB 500. (Current) Saladpuk, has THB 700.

😵 Inheritance คือไรกันแน่ ?

Inheritance มันมีหลายเรื่องอยู่ในนั้นเยอะเลย ดังนั้นมาค่อยๆทำความเข้าใจกันทีละเรื่องก่อนละกันนะ โดยสิ่งที่เราต้องรู้สิ่งแรกคือ

Inheritance (การสืบทอด)

✨ เราสามารถนำคลาสที่มีอยู่แล้ว มาต่อยอดความสามารถให้กับคลาสใหม่ได้

เราได้เห็นตัวอย่างโค้ดด้านบนไปแล้วว่า บัญชีออมทรัพย์ และ บัญชีกระแสรายวัน ได้ต่อยอดความสามารถจาก BankAccount มา เลยทำให้บัญชีทั้ง 2 ประเภทมี OwnerName, Balance และ Deposit() ทั้งๆที่ตัวมันเองไม่ได้มีโค้ดอะไรอยู่ด้านในเลย ซึ่งสิ่งนี้เราเรียกว่า การสืบทอด หรือ Inheritance นั่นเอง

Base class (คลาสแม่)

โดยคลาสที่เป็นต้นแบบเราเรียกมันว่า Base class (บางตำราจะเรียกว่า Super class, Parent class บลาๆ) ซึ่งในตัวอย่างนี้คลาส BankAccount เป็น base class ของ SavingAccount และ CurrentAccount นั่นเอง

Sub class (คลาสลูก)

ส่วนคลาสที่ไปสืบทอดความสามารถจากคนอื่นเราเรียกมันว่า Sub Class (บางตำราเรียกว่า Derived class, Child class บลาๆ) ซึ่งในตัวอย่างนี้คลาส SavingAccount และ CurrentAccount เป็น sub class ของ BankAccount นั่นเอง

Generalization

✨ ของที่อยู่ใน Base class จะถูกส่งลงมาให้ Sub class ส่วน sub class จะใช้มันได้หรือเปล่าขึ้นอยู่กับ accessibility ของ property พวกนั้น

เราจะเห็นว่าแม้ SavingAccount จะไม่ได้เขียนโค้ดอะไรไว้ข้างในเลย แต่เราก็จะสามารถเรียกใช้ Balance หรือเมธอต Deposit ได้ ตามโค้ดด้านล่าง

var sa = new SavingAccount();
sa.OwnerName = "(Saving) Saladpuk";
sa.Deposit(500);

เพราะสิ่งที่คลาสแม่มี คลาสลูกจะมีด้วย แต่จะใช้งานได้หรือเปล่าขึ้นอยู่กับ accessibility ที่ตัวแม่ตั้งไว้ เช่นตัวแปร balance มันเป็น private คลาสลูกจะไม่สามารถอ้างถึงได้นั่นเอง

Override (การเปลี่ยนพฤติกรรม)

อีกสิ่งหนึ่งที่เราได้จากการทำ Inheritance นั่นคือ เราสามารถทำให้การทำงานของ Sub class ทำงานแตกต่างกันกับ Base class ของมันได้ เช่น ตัวชัญชีนั้นจะต้องสามารถ ถอนเงิน ได้ ซึ่งจุดที่น่าสนใจของมันคือ

  • บัญชีออมทรัพย์ - ถอนเงินได้สูงสุดไม่เกินเงินที่มีอยู่ในบัญชี

  • บัญชีกระแสรายวัน - ถอนเงินได้เกินเงินที่มีอยู่ในบัญชี แต่ไม่เกินวงเงินที่ตั้งไว้

ดังนั้นเราก็จะทำการเขียนโค้ดถอนเงินไว้ที่ตัว BankAccount ที่เป็น Base class ของมันก่อน ซึ่งได้ประมาณนี้

public class BankAccount
{
...
public double Balance
{
get => balance;
protected set => balance = value;
}
public virtual void Withdraw(double amount)
{
if (amount <= balance)
{
balance -= amount;
}
}
}

คำสั่ง virtual เป็นคำสั่งพิเศษของภาษา C# เพื่อที่จะให้ Sub class สามารถไปแก้ไขการทำงานของเมธอตนั้นๆได้ และผมได้แก้ให้ตัวแปร Balance สามารถเข้าไปแก้ไขผ่าน Sub class ได้เท่านั้น

เพียงเท่านี้เราก็จะสามารถถอนเงินได้ละ ส่วน บัญชีกระแสรายวัน เราก็ไปเขียนเพิ่มให้มันสามารถถอนเงินได้เกินเงินที่มี อยู่ในบัญชี ซึ่งสมมุติว่าวงเงินกู้ได้ไม่เกิน 1,000 ละกัน ดังนั้นเราก็จะได้โค้ดออกมาตามนี้

public class CurrentAccount : BankAccount
{
private double credit = 1000;
public override void Withdraw(double amount)
{
if (amount <= Balance + credit)
{
Balance -= amount;
}
}
}

ลองทดสอบกันดู

static void Main(string[] args)
{
var sa = new SavingAccount();
sa.OwnerName = "(Saving) Saladpuk";
sa.Deposit(500);
sa.Withdraw(700);
Console.WriteLine($"{sa.OwnerName}, has THB {sa.Balance}.");
var ca = new CurrentAccount();
ca.OwnerName = "(Current) Saladpuk";
ca.Deposit(700);
ca.Withdraw(1000);
Console.WriteLine($"{ca.OwnerName}, has THB {ca.Balance}.");
}

Output (Saving) Saladpuk, has THB 500. (Current) Saladpuk, has THB -300.

เรียบร้อยแล้ว บัญชีออมทรัพย์ ถอนเงินได้ไม่เกินเงินที่มีอยู่ในบัญชี แต่ในขณะที่ บัญชีกระแสรายวันถอนเกินเงินที่มีอยู่ได้นั่นเอง

🤔 ทำ Inheritance ไปทำไม ?

กลับมาที่บัญชีธนาคารเหมือนเดิม ซึ่งนอกจากการฝากเงินแล้ว เรายังต้องทำให้มัน ปิดบัญชี ได้ด้วย ซึ่งถ้าเราใช้ความสามารถของ Inheritance เราเลยสามารถไปเขียนเมธอตในการปิดบัญชีไว้ที่ Base class เพียงที่เดียว แล้วเจ้า Sub class ของมันก็จะได้รับความสามารถนี้ไปด้วยทันทีนั่นเอง (ขอเขียนแบบย่อๆนะ)

public class BankAccount
{
...
private bool isClosed;
public bool IsClosed { get => isClosed; }
public void CloseAccount()
{
isClosed = true;
}
}

ความสัมพันธ์แบบ Is a

เมื่อไหร่ก็ตามที่เราใช้ Inheritance ปุ๊ป เจ้าพวก Sub class ทั้งหลายจะมีความสัมพันธ์ที่เรียกว่า "Is a" ทันที หมายความว่า เราก็จะมองว่า บัญชีออมทรัพย์ และ บัญชีกระแสรายวัน มันเป็น บัญชีธนาคาร ประเภทหนึ่งทันที ซึ่งมันจะมีบทบาทที่สำคัญในเรื่องของการนำไปใช้กับ Polymorphism ในบทถัดไปอย่างมาก

ข้อควรระวัง อย่าใช้ Inheritance เพราะเราขี้เกียจเขียนโค้ดซ้ำๆ เพราะมันจะได้ความสัมพันธ์แบบ Is A เข้าไปด้วย (เดี๋ยวไปดูความร้ายแรงของมันในบทของ Polymorphism เอาละกัน)

จงทำ Inheritance เมื่อ Class พวกนั้นมันเป็นประเภทเดียวกันจริงๆ จากมุมมองของ Abstraction เท่านั้น

ลำดับชั้น

ในการทำ Inheritance มันจะมีลำดับชั้นความสัมพันธ์กันว่าใครเป็น คลาสแม่ คลาสลูก กันเสมอ ซึ่งจากโค้ดตัวอย่างด้านบนทั้งหมดก็สามารถเอามาเขียนเป็นแผนภาพ UML ง่ายๆได้ประมาณนี้

โดยเราจะเห็นว่า BankAccount เป็นคลาสแม่ ซึ่งมีลูก 2 ตัวคือ SavingAccount และ CurrentAccount

แต่มันยังไม่จบเพียงเท่านี้เพราะทุกคลาสจริงๆมันจะสืบสอดมาจากคลาสที่ชื่อว่า Object อีกที ดังนั้นแผนภาพที่แท้จริงคือ

ซึ่งจากรูปนี้คลาส BankAccount ก็เป็นคลาสลูกของคลาส Object อีกต่อหนึ่งนั่งเอง เลยทำให้มันมีความสามารถต่างๆของคลาสแม่ติดมาด้วยเสมอยังไงล่ะ เช่น เราใช้คำสั่ง .ToString() ได้เลยนั่นเอง

static void Main(string[] args)
{
var acc = new BankAccount();
acc.OwnerName = "Saladpuk";
acc.Deposit(500);
var result = acc.ToString();
Console.WriteLine(result);
}

Output demo.BankAccount

หมายเหตุ: โดยปรกติ .ToString() ที่เขียนไว้ใน Base Class จะเป็นการแสดงชื่อเต็มๆโดยขึ้นต้นจาก Name space แล้วต่อด้วยชื่อคลาสของเรานั่นเอง

หมายเหตุ ใน C# เราสามารถทำการสืบทอดได้ทีละ 1 คลาสเท่านั้น หรือพูดง่ายๆคือ มีแม่ได้เพียงคนเดียว นะจ๊ะ แต่ในหลักการเรื่องนี้มันมีอีกหลายแบบเลย เช่น มีแม่ได้มากกว่า 1 ตัว (Multiple Inheritance) แบบลูกครึ่ง (Hybrid inheritance) ซึ่งเรื่องเหล่านั้นลองไปหาอ่านดูเอาเองต่อนะจ๊ะ

แนะนำให้อ่าน ใครที่สนใจอยากรู้การแปลงโค้ดให้เป็นภาพที่เข้าใจได้ง่ายๆ สามารถศึกษาเพิ่มเติมได้ที่บทความตัวนี้เลย 👶 UML พื้นฐาน

🎥 วีดีโอประกอบความเข้าใจ

ทฤษฎี

ตัวอย่างการใช้งาน

ตัวอย่างโค้ดทั้งหมด

BankAccount
SavingAccount
CurrentAccount
Main
BankAccount
public class BankAccount
{
private bool isClosed;
private double balance;
public bool IsClosed { get => isClosed; }
public double Balance
{
get => balance;
protected set => balance = value;
}
public string OwnerName { get; set; }
public void Deposit(double amount)
{
if (amount > 0)
{
balance += amount;
}
}
public virtual void Withdraw(double amount)
{
if (amount <= balance)
{
balance -= amount;
}
}
public void CloseAccount()
{
isClosed = true;
}
}
SavingAccount
public class SavingAccount : BankAccount
{
}
CurrentAccount
public class CurrentAccount : BankAccount
{
private double credit = 1000;
public override void Withdraw(double amount)
{
if (amount <= Balance + credit)
{
Balance -= amount;
}
}
}
Main
static void Main(string[] args)
{
var sa = new SavingAccount();
sa.OwnerName = "(Saving) Saladpuk";
sa.Deposit(500);
sa.Withdraw(700);
Console.WriteLine($"{sa.OwnerName}, has THB {sa.Balance}.");
var ca = new CurrentAccount();
ca.OwnerName = "(Current) Saladpuk";
ca.Deposit(700);
ca.Withdraw(1000);
Console.WriteLine($"{ca.OwnerName}, has THB {ca.Balance}.");
}