description |
---|
🧐 บทปิดท้ายแห่งการหักมุม! ที่จะเผยพลังที่แท้จริงของการออกแบบ |
ผมเชื่อว่าหลายๆคนน่าจะได้เห็นตัวอย่างการนำ OOP ไปใช้งานไปใช้ในบทก่อนหน้าแล้วล่ะ และก็อาจจะคิดว่านั่นคือ การนำ OOP มาใช้งานได้อย่างมีประสิทธิภาพแล้วชิมิ? ... เสียใจด้วยเพราะนั่นคือ หลุมพรางในการนำ OOP ไปใช้ต่างหาก แต่ก็ไม่ใช่เรื่องแปลกถ้าเราจะเข้าใจผิด เพราะมันเป็นเรื่องที่คนส่วนใหญ่เข้าใจผิดอันดับต้นๆการนำ OO ไปใช้เลยก็ว่าได้ ดังนั้นในบทความนี้เราจะมาแก้ไขข้อผิดพลาดพวกนั้นกัน โดยนำหลัก การออกแบบ เข้ามาช่วยเพื่อให้เราเข้าถึงแก่นแท้มันกันครัช
{% hint style="danger" %}
แนะนำให้อ่านก่อนอ่านบทนี้
สำหรับใครยังไม่ได้อ่านบทความก่อนหน้าให้รีบไปอ่านด่วนจะได้เข้าใจบทความนี้มากยิ่งขึ้น ซึ่งสามารถไปอ่านได้จากลิงค์นี้เบย 📝 ลองเขียน OOP ดูดิ๊
{% endhint %}
{% hint style="info" %}
ขอขอบคุณ
ตอนแรกว่าจะไม่ได้เขียนบทความนี้แล้ว แต่ได้รับแรงบันดาลใจจากเพื่อนๆชาว developer เพราะหลายๆท่านกลัวว่าเพื่อนๆที่อ่านบทความนั้นแล้วจะเอาไปใช้งานจริงๆเลยนั่นเอง และผมก็เขียนเพลินจนลืมนึกถึงคนที่พึ่งศึกษา OOP ใหม่ๆด้วย ดังนั้นบทความนี้เลยถือกำเหนิดขึ้นเป็นภาคจบที่สมบูรณ์(มั๊ง) ของคอร์ส Object-Oriented Programming นี้ครับ
🙏 ขอขอบคุณท่าน Bee Yodrak และเพื่อนๆใน Programmer Thai Blood ด้วยนะครับที่ช่วยชี้แนะในจุดที่ผมลืม หรือ มองข้ามไปมากครับ ❤️ {% endhint %}
จากรอบก่อนมันมีเรื่องที่ลืมทำให้ดูอยู่ 2 อย่าง ดังนั้นเราจะมาทำโจทย์เพิ่มกันนิดนุงนะ
เนื่องด้วยอะไรมาดลใจก็ไม่รู้ทำให้บริษัทคิดว่า ถ้าตัวละคร นั่งพัก จนครบ 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 ที่แท้จริงของมันอีกด้วย ตามรูปเบย
บ่อยครั้งที่ทีมพัฒนาเกมด้วยกันเองหลงไปสร้าง 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" นั่นเอง
{% hint style="success" %}
ความสัมพันธ์แบบ "IS A"
เป็นมุมมองในการมองความสัมพันธ์ของ Model ว่า มันเป็นหนึ่งในตระกูลนั้นหรือเปล่า และ มันจะต้องเป็น type นั้นตั้งแต่เกิดจนตาย นั่นเอง
{% endhint %}
ถ้าอ่านถึงตรงนี้ก็ยัง งงๆ อยู่ก็ไม่เป็นไร ลองดูตัวอย่างกันก่อนละกันว่ามันควรจะออกแบบยังไงกันดีกว่า
ในจุดนี้เราลองเอา Models ทั้งหมดมากางออกให้ชัดๆกันก่อน
ซึ่งเราจะเห็นว่าคลาส Novice มันสืบทอดมาจาก Character แต่ว่ามันไม่ได้มีอะไรต่างจาก Character เลย!! ดังนั้นนี่คือ การทำ Inheritance ที่ไม่เหมาะสม เพราะผมก็สามารถนำ Character ไปสร้าง Novice ได้เหมือนกันยังไงล่ะ!!
จำกันได้ไหมว่าตัวละครแต่ละตัวมันมีความสามารถที่ไม่เหมือนกันนั่นคือ
- นักดาป (Swordman) - มีท่าโจมตีพิเศษ
- พระ (Acolyte) - สามารถรักษาตัวเองและเพื่อนๆได้
แต่ลองคิดดูนะว่าถ้าเราเอา นักดาป หรือ พระ เปลี่ยนรูป (Polymorphism) ไปเป็น Character ตามโค้ดด้านล่างแล้วล่ะก็ เราก็จะไม่สามารถเรียกเมธอด SuperAttack หรือ Heal ได้อีกเลย นอกจากเราจะทำการ cast มันกลับมา
Character character1 = new Swordman();
character1.SuperAttack(); // error เพราะคลาสแม่ไม่รู้จัก
Character character2 = new Acolyte();
character2.Heal(); // error เพราะคลาสแม่ไม่รู้จัก
จากที่ออกแบบมา ถ้าเราอยาก เพิ่มความความสามารถอื่นๆเข้าไปล่ะ เช่น นักดาปมีท่าโจมตีพิเศษแบบที่ 2 ล่ะ? หรือ พระสามารถเพิ่มความพลังโจมตีให้เพื่อนๆได้ล่ะ? เราจะออกแบบยังไงดีไม่ให้เราต้องไปแก้คลาสเดิมที่เรามีอยู่ ?
ถ้าไปคิดดีๆก็จะมีคำถามปวดตับอีกเยอะ ดังนั้นเพื่อไม่ให้เป็นการเสียเวลาเราลองมาแก้ปัญหาทั้งหมดนั่นกันเลยดีกว่า โดยสิ่งแรกที่ผมมองก่อนก็คือเรื่อง ตัวละคร ซึ่งเราลองมาตั้งคำถามดูว่า เด็กฝึกหัด, นักดาป และ พระ มันต่างกันตรงไหน? . . . คำตอบคือมันต่างกันที่ โซนสีเหลือ เท่านั้นแหละ
ดังนั้นที่ผมจะทำก่อนก็คือสร้าง Model ที่ชื่อว่า Character สำหรับ โซนสีขาว เพราะมันเหมือนกัน เลยสามารถใช้ Model ร่วมกันได้หมดนั่นเอง เลยได้ผลลัพท์ออกมาเป็นแบบรูปด้านล่าง
ดังนั้นคำถามถัดไปคือ แล้วเจ้าเหลืองๆที่ไม่เหมือนเพื่อนพวกนั้นคืออะไร? ... ในมุมมองของผมมันคือ ความสามารถ หรือ Skill นั่นเอง ซึ่งผมมองว่าของพวกนี้มันไม่ได้มีแค่นี้หรอกมันจะ มาเพิ่มขึ้นเรื่อยๆ แน่นอน
และในบางที Skill เดียวกัน อาจจะเอาไปใช้กับหลายตัวละครก็ได้เหมือนกันนะ เช่น
- นักดาป กับ อัศวิน - ใช้ Bash และ Magnum Break ได้
- พระ กับ นักบวช - ใช้ Heal และ Divine Protection ได้
- Crusader - ใช้ได้หมดเลย (Bash, Magnum Break, Heal และ Divine Protection)
ดังนั้นเมื่อเรามองแล้วจริงๆความสามารถหรือเจ้า Skill มันเป็น ของคนละประเภทกัน กับตัวละคร เพราะมันจับคู่กับตัวละครได้เยอะมาก ซึ่งลักษณะความสัมพันธ์แบบนี้เราเรียกว่า "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 นั่นเอง
{% hint style="warning" %}
คำเตือน
😤 Skills ยังไม่ได้ทำ Encapsulation นะ ทำให้ดูหลายครั้งแล้วลองไปหัดทำต่อเอาละกัน
{% endhint %}
วิธีการใช้ 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 ครัช (จิ้มไปติดตามโลดจะได้ไม่พลาดอัพเดทใหม่ๆ)
{% hint style="success" %}
แนะนำให้อ่าน
หลักในการออกแบบที่จะช่วยให้เราทำงานได้ง่ายขึ้น ในตอนที่เราเจอปัญหาคล้ายๆกับเคสตัวอย่างอันนี้นั่นคือ
- ****Strategy Pattern****
- ****Bridge Pattern****
- ****Visitor Pattern****
ซึ่งถ้าเพื่อนๆสนใจอยากเรียนรู้หลักในการออกแบบปัญหาที่เรามักจะเจอกันบ่อยๆในวงการซอฟต์แวร์แล้วล่ะก็ สามารถศึกษาได้จากลิงค์ตัวนี้เบยครัช
{% endhint %}
{% hint style="success" %}
แนะนำให้อ่าน
หลักในการออกแบบขั้นพื้นฐานที่จะช่วยเป็นแนวทางในการออกแบบ Object-Oriented concept นั้นมีหลายตัว ซึ่งหนึ่งในนั้นมีชื่อว่า SOLID Design Principles ถ้าเพื่อนๆสนใจศึกษาเพิ่มเติมก็สามารถกดจากลิงค์ด้านล่างไปอ่านได้เบยครัช
{% endhint %}