Skip to content

Latest commit

 

History

History
284 lines (184 loc) · 28.3 KB

oop-n-design.md

File metadata and controls

284 lines (184 loc) · 28.3 KB
description
🧐 บทปิดท้ายแห่งการหักมุม! ที่จะเผยพลังที่แท้จริงของการออกแบบ

👑 OOP + Power of Design

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

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

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

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

🔥 เก็บตก

จากรอบก่อนมันมีเรื่องที่ลืมทำให้ดูอยู่ 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" นั่นเอง

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

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

😱 หลุมพรางข้อที่ 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 นั่นเอง

{% hint style="warning" %} คำเตือน
😤 Skills ยังไม่ได้ทำ Encapsulation นะ ทำให้ดูหลายครั้งแล้วลองไปหัดทำต่อเอาละกัน {% endhint %}

🤔 แล้วจะเรียกใช้ 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 ครัช (จิ้มไปติดตามโลดจะได้ไม่พลาดอัพเดทใหม่ๆ)

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

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

{% endhint %}

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

{% endhint %}