Class Diagram

🤔 ออกแบบคลาสต่างๆร่วมกันกับคนอื่นโดยไม่เขียนโค้ดเขาทำกันยังไง?

😢 ปัญหา

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

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

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

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

🤔 Class Diagram ใช้ยังไง?

สมมุติว่าเราต้องเขียนโปรแกรม Login ละกัน แล้วเราจะต้องออกแบบยังไงดีนะ? ดังนั้นเราจะลองให้ ดช.แมวน้ำ 🧔 เป็นคนไล่โครงสร้างแบบเร็วๆดูละกันนะ

ข้อแนะนำ จริงๆในการออกแบบเราไม่ควรจะตั้งต้นจาก Class Diagram นะครับ เราควรจะเอา Scenarios ขึ้นมาตั้งก่อน แล้วใช้หลักการของ TDD เข้ามาช่วยในการออกแบบ ถ้าเพื่อนคนไหนสนใจสามารถไปอ่านตัวอย่าง TDD แบบเร็วๆได้จากลิงค์นี้ 👦 Test-First Design หรือถ้าอยากรู้จัก TDD แบบเต็มตัวสามารถดูได้จากคอร์สนี้ครับ 👦 Test-Driven Development

😄 ลองเขียน Class Diagram กัน

โดยปรกติเวลาที่เราใช้ UML เราจะไม่เขียนโค้ดแต่เราจะวาดรูปเล่นกันครับ ดังนั้นมาลองไล่ตามสิ่งที่ ดช.แมวน้ำ จะวาดรูปให้ดูทีละขั้นตอน เพื่อที่จะมีโครงสร้างของระบบ Login ดูละกันนะ

🔥 Class & Field

🧔 สิ่งแรกที่ตัวระบบ Login จะต้องมีเลยก็คือคลาส LoginRequest เอาไว้เก็บข้อมูลที่ผู้ใช้จะป้อนเข้ามา เช่น Username กับ Password ดังนั้นเราก็จะวาดรูปแรกกัน

ด้านบนถ้าเอาไปเขียนโค้ดก็จะเป็นแบบนี้

public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}

Class & Fields เวลาที่เราเขียนคลาสเราจะวาดรูปกล่องสี่เหลี่ยมและเขียนชื่อคลาสนั้นๆไว้ด้านบนสุด ถัดมาถ้าภายในคลาสมี data members ต่างๆก็จะเขียนเอาไว้ในส่วนถัดไปโดยที่แต่ละ member จะระบุ data type ต่อท้ายไว้ด้วย

🔥 Method & Parameter

🧔 ถัดไปเราก็จะมีคลาสที่เอาไว้ตรวจสอบว่าผลการ Login นั้นผ่านหรือเปล่าซึ่งจะตั้งชื่อคลาสนั้นว่า LoginHandler ละกัน และมันจะมี method 1 ตัวชื่อว่า CheckLogin ที่จะตรวจว่า ข้อมูลที่ผู้ใช้ส่งมาสามารถ login ได้หรือเปล่า ตามรูปด้านล่างเลย

ด้านบนถ้าเอาไปเขียนโค้ดก็จะเป็นแบบนี้

public class LoginHandler
{
public bool CheckLogin(LoginRequest req)
{
// โค้ดตรวจสอบผลการเข้าสู่ระบบ
}
}

Methods ในส่วนของ methods ต่างๆเราจะเขียนเอาไว้ใต้พื้นที่ของ data members

🔥 Visibility

🧔 อ๋อเกือบลืมไป มันจะเก็บจำนวนครั้งที่มีคน Login แล้วไม่สำเร็จเอาไว้ด้วยนะ ซึ่งข้อมูลตัวนี้คนอื่นจะดูได้อย่างเดียว ดังนั้นวาดรูปเพิ่มอีกนิสสส

Visibility ความสามารถในการเข้าถึงต่างๆ เราจะใช้ใช้เครื่องหมายพิเศษเขียนนำหน้าไว้ตามด้านล่างนี้เลย

  • private ใช้เครื่องหมาย -

  • protected ใช้เครื่องหมาย #

  • package ใช้เครื่องหมาย ~

  • public ใช้เครื่องหมาย +

🔥 Composition (ความสัมพันธ์)

🧔 คราวนี้ในการ Login แต่ละครั้งเราจะต้องมีการเก็บบันทึกการเข้าใช้งานผ่านคลาส LoginLogger ด้วยนะ วาดๆๆ

ด้านบนถ้าเอาไปเขียนโค้ดก็จะเป็นแบบนี้ (ขอเขียนแค่ตัวที่ถูกเพิ่มเข้าไปนะ)

public class LoginHandler
{
protected LoginLogger log;
...
}

Composition รูปแบบความสัมพันธ์ตัวนี้จะมีหัวเป็น Diamon ทึบ ชี้ไปหาคลาสที่เป็นเจ้าของ object ซึ่งในรูป LoginHandler จะต้องเก็บ object ของคลาส LoginLogger เอาไว้นั่นเอง

ลักษระความสัมพันแบบ Composition คือ ถ้า object ที่เป็นเจ้าของถูกทำลายลงไป พวก object ที่เป็นส่วนประกอบทั้งหมดของมันก็จะต้องถูกทำลายตามไปด้วยเช่นกัน (ตายยกรัง)

🔥 Aggregation (ความสัมพันธ์)

🧔 อย่าลืมนะว่าในการเขียน Log นั้นมันจะต้องบันทึกลงฐานข้อมูลด้วย ดังนั้นเราก็จะมีคลาสที่ชื่อว่า SqlDatabase เอาไว้เชื่อมต่อจัดการกับ Sql database นั่นเอง วาดต่อๆ

Aggregation รูปแบบความสัมพันธ์ตัวนี้จะมีหัวเป็น Diamon ธรรมดา ชี้ไปหาคลาสที่เป็นเจ้าของ object ซึ่งในรูป LoginLogger จะต้องเก็บ object ของคลาส SqlDatabase เอาไว้นั่นเอง

ลักษระความสัมพันแบบ Aggregation คือ แม้ว่า object ที่เป็นเจ้าของถูกทำลายลงไป แต่ object ที่เป็น aggregate นั้นจะยังคงอยู่ต่อ (ตายเดี่ยว)

Composition vs Aggregation ความสัมพันธ์ 2 แบบนี้ถ้าดูแล้วจะคล้ายกันมัน มันแค่ใช้อธิบายถึงความละเอียดอ่อนใน life cycle ของ object พวกนั้นกับตัวคลาสที่เป็นเจ้าของมันหรือที่เราเรียกว่า container ครับ

🔥 Generalization (ความสัมพันธ์)

🧔 อ๋อเกือบลืมไป เรามีการ Login แบบใช้ความปลอดภัยขั้นสูงด้วย โดยนอกจามันจะตรวจการ login แบบปรกติละมันจะยังตรวจเรื่องอื่นๆด้วย ดังนั้นเจ้าตัวนี้มันจะสืบทอดมาจาก LoginHandler เพื่อตรวจสอบตามปรกติและทำการตรวจสอบขั้นสูงต่อ ดังนั้นเราก็จะสร้างคลาส AdvancedLoginHandler เพิ่มเข้ามาตามมรูปด้านล่างเลย

ด้านบนถ้าเอาไปเขียนโค้ดก็จะเป็นแบบนี้

โค้ดนี้เป็นตัวอย่างที่ไม่ดีนะ เพราะไม่ได้ทำ virtual & override กับให้กับ CheckLogin เอาไว้ เพราะมันไม่ใช่ส่วนที่เราจะสอน

public class AdvancedLoginHandler : LoginHandler
{
...
private bool checkFraud(string username)
{
// โค้ดตรวจสอบขั้นสูง
}
}

Generalization เป็นการบอกความสัมพันธ์ระหว่าง base class กับ sub class โดยมันจะชี้หัวลูกศรไปหาตัวที่เป็น base class

🔥 Interface class

🧔 ในคราวตัวโปรแกรมของเหลามันอาจจะมีการต่อ database หลายตัวนะ ดังนั้นเราจะต้องเขียน interface class ที่ชื่อว่า IDatabase เอาไว้บ้าง ตามรูป

🧔 ซึ่งเจ้า IDatabase ตัวนี้เราก็จะให้มันสามารถเขียน log ได้ละกันเลยขอเพิ่ม method เข้าไปให้มันนิดหน่อย

🔥 Realization (ความสัมพันธ์)

🧔 ถัดไปเราก็จะให้เจ้าคลาส SqlDatabase มาทำการ implement เจ้า IDatabase ไปซะ ซึ่งสิ่งนี้เราเรียกมันว่า Realization นั่นเอง ตามรูปด้านล่างเลย

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

Realization จะมีลักษณะคล้ายกับ Generalization ต่างกันคือเป็นเส้นประ มีไว้บอกว่าคลาสนั้นๆ implement interface อะไร โดยใช้หัวลูกศรชี้ไปยัง interface class ที่มันจะทำการ implement

🔥 Abstract class & Method

🧔 ไหนเราลองเปลี่ยนเจ้า AdvancedLoginHandler ให้กลายเป็น abstract คลาสหน่อยละกันเพื่อรองรับการ login หลายๆแบบในอนาคต เราก็จะได้ภาพออกมาเป็นประมาณนี้

จากรูปถ้าแปลงเป็นโค้ดจะได้ประมาณนี้

public abstract class AdvancedLoginHandler : LoginHandler
{
public abstract bool ValidateSpecification(string username);
}

Abstract class & method ของที่เป็น abstract เราจะใส่ <<abstract>> ระบุเอาไว้ก็ได้ และเราจะเขียนให้มันเป็นตัวเอียง

🤔 มีอย่างอื่นอีกไหม ?

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

🔥 Static member

🧔 ถ้าภายในคลาส์ของเรามีอะไรที่เป็น static member ไม่ว่าจะเป็น field หรือ method ก็ตามก็สามารถใช้แผนภาพอธิบายมันได้เหมือนกันนะ โดยการขีดเส้นใต้มันลงไปนั่นเอง

จากภาพเมื่อแปลงเป็นโค้ดจะเขียนว่า

public class Awesome
{
public static int GlobalMember;
}

🔥 Association (ความสัมพันธ์)

🧔 ในบางทีเราก็ขี้เกียจอธิบายความสัมพันธ์ว่าเป็นแบบ Aggregation หรือ Composition เราก็สามารถลากเส้นความสัมพันธ์ระหว่างคลาสลงไปได้ดื้อๆเลย เช่น อาจารย์ จะต้องไปสอนที่ ห้องเรียน เราก็สามารถเขียนความสัมพันธ์ของคลาสทั้งสองได้ว่า

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

Teacher สามารถ navigate ไป ClassRoom ได้ ส่วน ClassRoom อาจจะทำได้ก็ได้แค่เราไม่ระบุลงไป ตามรูปด้านล่าง

ClassRoom ไม่สามารถ navigate ไป Teacher ได้ ส่วน Teacher อาจจะทำได้ก็ได้แค่เราไม่ระบุลงไป ตามรูปด้านล่าง

Teacher สามารถ navigate ไป ClassRoom ได้ และ ClassRoom ไม่สามารถ navigate ไป Teacher ได้ ตามรูปด้านล่าง

ทั้ง Teacher และ ClassRoom สามารถ navigate หากันได้

ทั้ง Teacher และ ClassRoom ไม่สามารถ navigate หากันได้

Multiplicity

🧔 บนเส้นความสัมพันธ์เรายังสามารถระบุจำนวนของความสัมพันธ์ลงไปได้ด้วย เช่น อาจารย์จะสอนหรือไม่สอนก็ได้แต่ถ้าสอนต้องห้ามสอนเกิน 4 วิชา ส่วนห้องเรียนต้องมีอาจารย์สอนอย่างน้อย 1 คนเสมอ ก็จะเขียนเป็นแผนภาพได้เป็น

🔥 Dependency (ความสัมพันธ์)

🧔 ในบางทีคลาสแต่ละคลาสมันอาจเกิดความสัมธ์แบบชั่วคราวเกิดขึ้นก็ได้ เช่น ในตัวอย่างเราจะเห็นว่าคลาส LoginRequest มันถูกส่งเข้ามาเป็น parameter ให้กับ method ของคลาส LoginHandler เพื่อใช้ตรวจสอบว่า login ได้หรือไม่เท่านั้นเองแล้วก็จบไปไม่ได้ยุ่งเกี่ยวกับอีก ดังนั้นเราจะเขียนเป็นแผนภาพได้ว่า

Dependency ความสัมพันธ์แบบชั่วคราวจะคล้ายๆกับ Association เพียงแค่มันเป็นเส้นประ เพื่อบอกว่าความสัมพันธ์ที่เกิดขึ้นนี้ไม่ได้ยั่งยืนเดี๋ยวก็แยกทางกัน

😄 แผนภาพและโค้ดทั้งหมด

หากไม่ชัดให้กดที่รูปเพื่อดูเต็มๆได้นะ
LoginRequest
LoginHandler
AdvancedLoginHandler
LoginLogger
IDatabase
SqlDatabase
public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class LoginHandler
{
protected Logger log;
private int loggedInFailureCount;
public virtual bool CheckLogin(LoginRequest req)
{
// โค้ดตรวจสอบผลการเข้าสู่ระบบ
}
public int LoggedInFailureCount()
{
return loggedInFailureCount;
}
}
public abstract class AdvancedLoginHandler : LoginHandler
{
public override bool CheckLogin(LoginRequest req)
{
return base.CheckLogin(req) && ValidateSpecification(req.Username);
}
public abstract bool ValidateSpecification(string username);
}
public class LoginLogger
{
private SqlDatabase sqlDb;
public void LoginSuccess(string username, DateTime timeStamp)
{
sqlDb.InsertLoginLog(username, timeStamp);
}
}
public interface IDatabase
{
void InsertLoginLog(string username, DateTime timeStamp);
}
public class SqlDatabase : IDatabase
{
public void InsertLoginLog(string username, DateTime timeStamp)
{
// Insert เข้าฐานข้อมูล
}
}

🎯 บทสรุป

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

ข้อแนะนำ การใช้แผนภาพ Class Diagram เราไม่จำเป็นที่จะต้องเขียนลงรายละเอียดถี่ยิบว่ามันเป็นความสัมพันธ์กันแบบไหนเช่นเป็น aggregation หรือ composition บลาๆ เพราะมันจะเปลืองแรงการทำงานเกินไป ควรรีบคุยให้เข้าใจตรงกันแล้วแยกยายไปทำงานต่อก็พอ ยกเว้นเราจะเขียน diagram เพื่อเอาไปทำเป็น Document ส่งงานที่ต้องเขียนเป็นทางการ

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

คำเตือน 2 การออกแบบที่ดีไม่ควรจะเริ่มต้นจาก Diagram แต่ควรเริ่มต้นจาก scenario แล้วตามด้วยการเขียนเทส เพื่อ drive design ต่างหาก หากเริ่มต้นเขียนจาก diagram มันจะทำให้ดูเหมือนเท่ห์แต่สุดท้ายมันจะกลายเป็น สปาเก็ตตี้โค้ดที่ทำให้โครงสร้างของโปรแกรมมันซับซ้อนโดยใช่เหตุ ดังนั้นเพื่อป้องกันปัญหาที่ว่ามา เพื่อนสามารศึกษาเพิ่มเติมได้จากลิงค์นี้ 👦 Test-First Design หรือถ้าอยากรู้จัก TDD แบบเต็มตัวสามารถดูได้จากคอร์สนี้ครับ 👦 Test-Driven Development