👑 OOP + Power of Design

🧐 บทปิดท้ายแห่งการหักมุม! ที่จะเผยพลังที่แท้จริงของการออกแบบ

ผมเชื่อว่าหลายๆคนน่าจะได้เห็นตัวอย่างการนำ OOP ไปใช้งานไปใช้ในบทก่อนหน้าแล้วล่ะ และก็อาจจะคิดว่านั่นคือ การนำ OOP มาใช้งานได้อย่างมีประสิทธิภาพแล้วชิมิ? ... เสียใจด้วยเพราะนั่นคือ หลุมพรางในการนำ OOP ไปใช้ต่างหาก แต่ก็ไม่ใช่เรื่องแปลกถ้าเราจะเข้าใจผิด เพราะมันเป็นเรื่องที่คนส่วนใหญ่เข้าใจผิดอันดับต้นๆการนำ OO ไปใช้เลยก็ว่าได้ ดังนั้นในบทความนี้เราจะมาแก้ไขข้อผิดพลาดพวกนั้นกัน โดยนำหลัก การออกแบบ เข้ามาช่วยเพื่อให้เราเข้าถึงแก่นแท้มันกันครัช

แนะนำให้อ่านก่อนอ่านบทนี้ สำหรับใครยังไม่ได้อ่านบทความก่อนหน้าให้รีบไปอ่านด่วนจะได้เข้าใจบทความนี้มากยิ่งขึ้น ซึ่งสามารถไปอ่านได้จากลิงค์นี้เบย 📝 ลองเขียน OOP ดูดิ๊

ขอขอบคุณ ตอนแรกว่าจะไม่ได้เขียนบทความนี้แล้ว แต่ได้รับแรงบันดาลใจจากเพื่อนๆชาว developer เพราะหลายๆท่านกลัวว่าเพื่อนๆที่อ่านบทความนั้นแล้วจะเอาไปใช้งานจริงๆเลยนั่นเอง และผมก็เขียนเพลินจนลืมนึกถึงคนที่พึ่งศึกษา OOP ใหม่ๆด้วย ดังนั้นบทความนี้เลยถือกำเหนิดขึ้นเป็นภาคจบที่สมบูรณ์(มั๊ง) ของคอร์ส Object-Oriented Programming นี้ครับ

🙏 ขอขอบคุณท่าน Bee Yodrak และเพื่อนๆใน Programmer Thai Blood ด้วยนะครับที่ช่วยชี้แนะในจุดที่ผมลืม หรือ มองข้ามไปมากครับ ❤️

🔥 เก็บตก

จากรอบก่อนมันมีเรื่องที่ลืมทำให้ดูอยู่ 2 อย่าง ดังนั้นเราจะมาทำโจทย์เพิ่มกันนิดนุงนะ

🧐 โจทย์ 05

เนื่องด้วยอะไรมาดลใจก็ไม่รู้ทำให้บริษัทคิดว่า ถ้าตัวละคร นั่งพัก จนครบ 10 วินาที จะทำให้ค่าพลังชีวิตของตัวละครเพิ่มขึ้นฟรีๆเลย ซึ่งแต่ละตัวละครจะแตกต่างกันตามนี้

  • นักดาป (Swordman) - เมื่อนั่งจนครบเวลาพลังชีวิตจะเพิ่มขึ้น 20 หน่วย

  • พระ (Acolyte) - เมื่อนั่งจนครับเวลา พลังชีวิตจะเพิ่มขึ้น 11 หน่วย

  • เด็กฝึกหัด (Novice) - ไม่ว่าจะนั่งแค่ไหนก็ตาม พลังชีวิตก็จะไม่เพิ่มเด็ดขาด

แล้วเราจะออกแบบมันยังไงดี ?

🧒 แก้โจทย์

ก่อนที่จะไปออกแบบเรากลับมาดูว่าตัวอย่างที่แล้ว Models เราเป็นยังไงบ้างกันก่อน

ซึ่งจากรูปจะเห็นว่าใน Character นั้นมีเรื่อง การนั่ง หรือเมธอด Sit อยู่แล้ว ซึ่งปรกติการนั่งมันจะไม่ได้เพิ่มพลังชีวิตอะไรอยู่แล้ว ดังนั้นคลาส Novice ไม่ต้องทำอะไรก็ได้ แต่พวกคลาส Swordman กับ Acolyte เราต้องการให้มัน ทำงานต่างจากเดิม ตามรูป

ซึ่งในกรณีนี้เราสามารถทำได้เลย โดยการไปแก้ไขการทำงานของเมธอด Sit ในคลาสลูก ที่เราอยากให้มันทำงานต่างจากเดิมนั่นเอง ซึ่งในภาษา C# ถ้าเราอยากจะให้คลาสลูกมีการทำงานที่ต่างจากคลาสแม่ได้เราจะใช้ virtual keyword กำกับไว้นั่นเอง ดังนั้นไปจัดกันเบย

public class Character
{
public virtual void Sit()
{
Console.WriteLine("Sit");
}
}

ส่วนคลาสลูกที่ต้องการทำงานต่างจากเดิมก็ไปทำสิ่งที่เรียกว่า override การทำงานของคลาสแม่นั่นเอง ตามนี้

public class Swordman : Character
{
public override void Sit()
{
base.Sit(); // ไปเรียก Sit ของคลาสแม่
Console.WriteLine("+20 HP");
}
}
public class Acolyte : Character
{
public override void Sit()
{
// ไม่อยากใช้ Sit ของแม่ ก็สร้างของตัวเองใหม่ก็ได้
Console.WriteLine("Sit");
Console.WriteLine("+20 HP");
}
}

จากโค้ดด้านบนเวลาที่เราไปทำงานเราก็จะยังสามารถใช้ Polymorphism ได้ตามปรกติ แถมตอนที่เรียกใช้งานเมธอด Sit มันก็จะไปทำงานกับ data type ที่แท้จริงของมันอีกด้วย ตามรูปเบย

🧐 โจทย์ 06

บ่อยครั้งที่ทีมพัฒนาเกมด้วยกันเองหลงไปสร้าง object จากคลาส Character ขึ้นมา ซึ่งมันไม่ใช่ Novice, Swordman และ Acolyte ใดๆทั้งสิ้นเลย ซึ่งคนในทีมไม่อยากให้เจ้าคลาสนั้นมันถูกเอาไปสร้าง object ได้ เราจะแก้ไงดี ?

🧒 แก้โจทย์

การก็แค่เปลี่ยนเจ้าคลาสนั้นให้กลายเป็นสิ่งที่เรียกว่า abstract class ซะซิ หรือพูดง่ายๆคือเรามองว่าเจ้าคลาส Character นั้นมันเป็นแค่ concept เท่านั้น ไม่สามารถเอาไปใช้งานได้จริงๆยังไงล่ะ (อ่านเรื่อง abstract class ต่อได้จากบทความนี้ Abstract Class) ดังนั้นเราก็จะได้โค้ดออกมาเป็นแบบนี้ขอรับ

public abstract class Character
{
...
}

🔥 หลุมพรางแห่งการออกแบบ

หลังจากเจอโจทย์เข้าไป 6 ข้อ เราก็จะได้คลาสที่เป็น OOP ออกมาทำงานได้เรียบร้อยละ แต่อย่างที่เกริ่นไปว่า ทั้งหมดที่ทำให้ดูมันเป็น หลุมพราง ที่คนส่วนใหญ่จะเข้าใจและใช้กันผิดบ่อยมากในการเขียน OOP นั่นเอง ... หักมุมไหมละ? ผมเชื่อว่าหลายๆคนก็อาจจะยัง งงๆ ด้วยว่า มันผิดยังไง? ตูก็ออกแบบอย่างนี้เหมือนกันนะ บลาๆ ดังนั้นตรงจุดนี้ขอเฉลยก่อนเลยว่า "มันผิดในแง่ของเหตุผลที่เราใช้ Inheritance นั่นเอง" เพราะหัวใจหลักของ Inheritance คือการมองความสัมพันธ์ในสิ่งที่เรียกว่า "IS A" นั่นเอง

ความสัมพันธ์แบบ "IS A" เป็นมุมมองในการมองความสัมพันธ์ของ Model ว่า มันเป็นหนึ่งในตระกูลนั้นหรือเปล่า และ มันจะต้องเป็น type นั้นตั้งแต่เกิดจนตาย นั่นเอง

ถ้าอ่านถึงตรงนี้ก็ยัง งงๆ อยู่ก็ไม่เป็นไร ลองดูตัวอย่างกันก่อนละกันว่ามันควรจะออกแบบยังไงกันดีกว่า

😱 หลุมพรางข้อที่ 1

ในจุดนี้เราลองเอา Models ทั้งหมดมากางออกให้ชัดๆกันก่อน

ซึ่งเราจะเห็นว่าคลาส Novice มันสืบทอดมาจาก Character แต่ว่ามันไม่ได้มีอะไรต่างจาก Character เลย!! ดังนั้นนี่คือ การทำ Inheritance ที่ไม่เหมาะสม เพราะผมก็สามารถนำ Character ไปสร้าง Novice ได้เหมือนกันยังไงล่ะ!!

😱 หลุมพรางข้อที่ 2

จำกันได้ไหมว่าตัวละครแต่ละตัวมันมีความสามารถที่ไม่เหมือนกันนั่นคือ

  • นักดาป (Swordman) - มีท่าโจมตีพิเศษ

  • พระ (Acolyte) - สามารถรักษาตัวเองและเพื่อนๆได้

แต่ลองคิดดูนะว่าถ้าเราเอา นักดาป หรือ พระ เปลี่ยนรูป (Polymorphism) ไปเป็น Character ตามโค้ดด้านล่างแล้วล่ะก็ เราก็จะไม่สามารถเรียกเมธอด SuperAttack หรือ Heal ได้อีกเลย นอกจากเราจะทำการ cast มันกลับมา

Character character1 = new Swordman();
character1.SuperAttack(); // error เพราะคลาสแม่ไม่รู้จัก
Character character2 = new Acolyte();
character2.Heal(); // error เพราะคลาสแม่ไม่รู้จัก

😱 หลุมพรางข้อที่ 3

จากที่ออกแบบมา ถ้าเราอยาก เพิ่มความความสามารถอื่นๆเข้าไปล่ะ เช่น นักดาปมีท่าโจมตีพิเศษแบบที่ 2 ล่ะ? หรือ พระสามารถเพิ่มความพลังโจมตีให้เพื่อนๆได้ล่ะ? เราจะออกแบบยังไงดีไม่ให้เราต้องไปแก้คลาสเดิมที่เรามีอยู่ ?

🔥 พลังแห่งการออกแบบที่แท้จริง

ถ้าไปคิดดีๆก็จะมีคำถามปวดตับอีกเยอะ ดังนั้นเพื่อไม่ให้เป็นการเสียเวลาเราลองมาแก้ปัญหาทั้งหมดนั่นกันเลยดีกว่า โดยสิ่งแรกที่ผมมองก่อนก็คือเรื่อง ตัวละคร ซึ่งเราลองมาตั้งคำถามดูว่า เด็กฝึกหัด, นักดาป และ พระ มันต่างกันตรงไหน? . . . คำตอบคือมันต่างกันที่ โซนสีเหลือ เท่านั้นแหละ

ดังนั้นที่ผมจะทำก่อนก็คือสร้าง Model ที่ชื่อว่า Character สำหรับ โซนสีขาว เพราะมันเหมือนกัน เลยสามารถใช้ Model ร่วมกันได้หมดนั่นเอง เลยได้ผลลัพท์ออกมาเป็นแบบรูปด้านล่าง

ดังนั้นคำถามถัดไปคือ แล้วเจ้าเหลืองๆที่ไม่เหมือนเพื่อนพวกนั้นคืออะไร? ... ในมุมมองของผมมันคือ ความสามารถ หรือ Skill นั่นเอง ซึ่งผมมองว่าของพวกนี้มันไม่ได้มีแค่นี้หรอกมันจะ มาเพิ่มขึ้นเรื่อยๆ แน่นอน

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

  • นักดาป กับ อัศวิน - ใช้ Bash และ Magnum Break ได้

  • พระ กับ นักบวช - ใช้ Heal และ Divine Protection ได้

  • Crusader - ใช้ได้หมดเลย (Bash, Magnum Break, Heal และ Divine Protection)

ดังนั้นเมื่อเรามองแล้วจริงๆความสามารถหรือเจ้า Skill มันเป็น ของคนละประเภทกัน กับตัวละคร เพราะมันจับคู่กับตัวละครได้เยอะมาก ซึ่งลักษณะความสัมพันธ์แบบนี้เราเรียกว่า "HAS A"

ความสัมพันธ์แบบ HAS A

ลักษณะความสัมแบบ "HAS A" มันจะอยู่ในรูปของการ ถือครอง เช่น

  • นักดาป มี Bash และ Magnum Break

  • พระ มี Divine Protection

ดังนั้นในการออกแบบของที่เป็น "HAS A" เราจะใช้ Composition หรือไม่ก็ Aggregation นั่นเอง (ไม่รู้เรื่องช่างมันอ่านต่อไปเรย) ดังนั้นสิ่งแรกที่เราต้องทำคือใช้ Abstraction แปลงเจ้า ความสามารถ หรือ Skill ให้กลายมาเป็น Model เสียก่อน ซึ่งผมก็จะได้ออกมาเป็นแบบนี้

public class Skill
{
public string Name { get; set; }
public int EffectOnHP { get; set; }
public int EffectOnAttack { get; set; }
public bool IsRequiredTarget { get; set; }
}

และทำการเชื่อม Model ทั้งสองตัวเข้าด้วยกันด้วยความสัมพันธ์แบบ HAS A เลย ซึ่งตัวละครหนึ่งตัวสามารถมี Skill ได้หลายชนิด ทำให้ได้ผลลัพท์ตามรูป

จากรูปด้านบนเลยทำให้ตัวละครเรามีได้หลาย skill ขึ้นอยู่กับว่าเราจะยอมให้มันมี skill อะไรบ้างนั่นเอง

ดังนั้นเราก็จะได้โค้ดจากที่ออกแบบไว้เป็นตามนี้

public abstract class Character
{
public IEnumerable<Skill> Skills { get; set; }
...
}

หมายเหตุ IEnumerable ในภาษา C# ก็คือ Collection นั่นเอง

คำเตือน 😤 Skills ยังไม่ได้ทำ Encapsulation นะ ทำให้ดูหลายครั้งแล้วลองไปหัดทำต่อเอาละกัน

🤔 แล้วจะเรียกใช้ Skill ยังไง?

วิธีการใช้ skill แบบก่อนที่จะแก้ให้เป็นโครงสร้างแบบนี้ มันมีการใช้ 2 แบบจำได้ไหม? นั่นก็คือ

  • ใช้ได้เลยไม่ต้องมีเป้าหมาย

  • ต้องเลือกเป้าหมายก่อนที่จะใช้ เช่น รักษาให้ตัวเอง หรือ รักษาให้เพื่อน

ดังนั้นถ้าเราจะใช้ skill ในรอบนี้ก็ทำเช่นเคยนั่นก็คือ เพิ่มเมธอด ให้กับคลาส Character งุยล่ะ ตามนี้เลย

public abstract class Character
{
public void Spell(Skill skill) { }
public void Spell(Skill skill, Character target) { }
...
}

เพียงเท่านี้เราก็จะ สามารถรองรับอาชีพใหม่ๆ และ สกิลใหม่ๆ ในอนาคตแล้ว เพียงแค่ใช้ 2 Model นี้เท่านั้นนั่นเอง

แถมไม่เพียงเท่านั้นจริงๆแล้วคลาส Character ของเรามันยังสามารถรองรับให้เราใส่พวก ศตรู แบบต่างๆเข้าไปได้ด้วยนะ

🤔 แล้วใส่หมวกยังไงอ่ะ ?

เรื่องสุดท้ายละคือหมวกที่ยังทำไม่ได้ ซึ่งหมวกก็จะเป็นลักษณะของความสัมพันธ์แบบ HAS A เช่นเคย แต่ในรอบนี้เราจะไม่สามารถทำใส่ในคลาส Character ได้แล้ว เพราะว่าไรรู้ป่าวววววว? ... พวก ศตรู ทั้งหลายมันใส่หมวกไม่ได้เหมือนตัวผู้เล่นยังไงล๊าาาาาา ดังนั้นภาพด้านล่างเลยไม่ควรทำ ... แล้วเราจะทำไงดีหว่า ???

วิเคราะห์กันต่อนิสสส ตัวผู้เล่นกับตัวศตรูใช้คลาสเดียวกัน พวกศตรูมันใส่หมวกไม่ได้ แต่ตัวผู้เล่นใส่หมวกได้!! นั่นแสดงว่ามันคือตัวละครที่มีความสามารถพิเศษที่ไม่เหมือน Character แต่ยังคงเป็น Character นั่นเอง ดังนั้นนี่แหละจุดที่เราจะใช้ Inheritance เข้ามานั่นเองงงงงงง จัดเบย

โอ้วววว เรียบร้อยงอดแงมตามท้องเรื่อง (จะได้ไปนอนซะที ฮ่าๆ) ซึ่งทั้งหมดที่ทำให้ดูนี้เราก็จะสามารถนำ Models ต่างๆไปประกอบกันจนสุดท้ายเราก็สร้าง ตัวละคร และ ศตรู ที่มี skill หลายๆแบบแตกต่างกันได้หมดละ โดยที่โค้ดของเราสามารถเพิ่มสิ่งใหม่ๆเข้าไปได้โดยที่เราไม่ต้องไปแก้ไขโค้ดเดิมนั่นเอง ซึ่งตรงกับหลักในการออกแบบพื้นฐานที่ชื่อว่า Open & Close Principle นั่นเอง

❓ คำถามทิ้งท้าย

ก่อนจะจบบทเรียนตัวนี้ขอทิ้งท้ายคำถามไว้ให้คิดกันต่อนิดหน่อยละกัน จะได้ได้ลองรีดจักระเค้นเน็นออกมาใช้กันบ้าง

  • ตัวละครของเรา และ พวกศตรูแต่ละตัวมันจะมี skill ติดตัวมาไม่เหมือนกัน แล้วเราจะออกแบบยังไงให้รองรับของพวกนั้นกันดีนะ ?

  • ตอนที่ตัวละครของเรา Level Up เราจะได้โบนัสพิเศษทำให้พลังโจมตี หรือ พลังชีวิตเพิ่มขึ้นนิดหน่อยด้วย ซึ่งแต่ละตัวละครเมื่อ Level Up มันจะได้โบนัสไม่เท่ากัน แล้วเราจะออกแบบของพวกนี้ยังไงดีนะ ?

🎯 บทสรุป

จะเห็นว่าการนำ OOP มาใช้ควบคู่กับ หลักในการออกแบบ นั้นจริงๆมันทรงพลังมาก จากความวุ่นวายทั้งหมดที่เคยมีก็มลายหายไปเหลือเพียงแค่ Model ที่ SIMPLE แต่ดันรองรับการทำงานในอนาคตเพียบเลย ดังนั้นต่อให้มีอาชีพมาอีกเป็น 100ๆ หรือมีสกิลใหม่ๆเข้ามา ถ้าของพวกนั้นยังอยู่ในร่าง Model นี้ได้ เราก็ไม่ต้องไปแก้โค้ดเลยนั่นเอง

ตัวอย่างผมตกเรื่องไหน อยากให้เสริมเรื่องไหน หรือ ไม่เห็นด้วย สามารถแนะนำติชมได้หมดครับ ผมน้อมรับเอาไปปรับปรุงเสมอ เจอกันได้ที่ Facebook Mr.Saladpuk ครัช (จิ้มไปติดตามโลดจะได้ไม่พลาดอัพเดทใหม่ๆ)

แนะนำให้อ่าน หลักในการออกแบบที่จะช่วยให้เราทำงานได้ง่ายขึ้น ในตอนที่เราเจอปัญหาคล้ายๆกับเคสตัวอย่างอันนี้นั่นคือ

ซึ่งถ้าเพื่อนๆสนใจอยากเรียนรู้หลักในการออกแบบปัญหาที่เรามักจะเจอกันบ่อยๆในวงการซอฟต์แวร์แล้วล่ะก็ สามารถศึกษาได้จากลิงค์ตัวนี้เบยครัช

แนะนำให้อ่าน หลักในการออกแบบขั้นพื้นฐานที่จะช่วยเป็นแนวทางในการออกแบบ Object-Oriented concept นั้นมีหลายตัว ซึ่งหนึ่งในนั้นมีชื่อว่า SOLID Design Principles ถ้าเพื่อนๆสนใจศึกษาเพิ่มเติมก็สามารถกดจากลิงค์ด้านล่างไปอ่านได้เบยครัช