/ OOD

[365 วันแห่งโปรแกรม #day44] SOLID (ตอนที่ 1 Single responsibility)

วันที่สี่สิบสี่ของ ‪#‎365วันแห่งโปรแกรม วันนี้เราจะคุยกันเรื่อง SOLID ที่ผมเกริ่นไว้เมื่อวาน วันนี้เราจะเริ่มจากเรื่อง Single responsibility ซึ่งเป็นเรื่องแรกของ SOLID


Single responsibility principle

Single responsibility principle หรือ SRP เป็น principle ที่มีหลักการง่ายๆ ครับ แต่เข้าใจและนำไปใช้ยากมาก principle นี้มีข้อกำหนดเดียวคือ ไม่ควรมีเหตุผลที่จะเปลี่ยนแปลงคลาสมากกว่า 1 เหตุผล หรือกล่าวคือ 1 คลาสจะมีหน้าที่ (responsibility) เดียวเท่านั้น ถ้าคลาสเรามีหน้าที่เดียวแล้ว เหตุผลที่เราจะแก้คลาสนั้นก็มีแค่มีการเปลี่ยนแปลง requirement ของหน้าที่นั้นๆ

ทำไมต้องทำตาม SRP?

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

จะเอา SRP ไปใช้ต้องทำอย่างไร?

ง่ายๆ ครับ ก็แค่กำหนดหน้าที่ของคลาสหรืออินเทอร์เฟสให้ชัดเจนไปเลย ว่าสำหรับทำอย่างนั้น อย่างนี้ ซึ่งถ้าเราจกันได้ขั้นตอนนี้คือ OOD ครับ ที่ผมแนะนำให้ใช้ CRC Card นั่นแหละครับ

ตัวอย่างของ responsibility ที่ควรแยก

- Persistence
- Validation
- Notification
- Error Handling
- Logging
- Class Selection / Instantiation
- Formatting
- Parsing
- Mapping

จากรายชื่อ responsibility ที่ควรแยก จะพบว่าประเด็นที่ทำผิดกันมาตลอดเลยก็คือเรื่อง Persistence เพราะ object ไม่ควรจะเซฟตัวเองได้ แต่เราก็ยังทำกันเป็นเรื่องปกติเนื่องจากมันง่าย = ="

ลองลงมือทำ

โจทย์ของเราคือให้สร้างคลาสสำหรับระบบสร้างและจัดการเอกสาร จากโจทย์นี้ ผมคิดว่าจะลองสร้างคลาส Document ดู เราก็ต้องลองเขียนดูว่ามี responsibility อะไรบ้าง

  • เขียน

  • อ่าน/พิมพ์

  • บันทึก

ถ้าเราลองทำแบบง่ายๆ แบบที่คุ้นเคย ก็จะได้เป็น

class Document{
    private String [] content;
    private int currentPage;
    public void goToPage(int page);
    public void nextPage();
    public void previousPage();
    public void write(int page, String data);
    public String read();
    public void print();
    public void save();
}

จะเห็นว่าในโค้ดของเรานั้นมีอะไรเยอะแยะไปหมดเลย แต่ก็ดูคุ้นเคยดี >< ต่อไปเราจะมาลองแยกคลาสออกไปกัน

ส่วนแรกที่ต้องการคือคลาส Document จริงๆ ที่ใช้เป็นตัวแทน Document นั้นๆ

class Document{
    private String [] content;
    public String getPage(int page){...}
    public void setPage(int page, String data){...}
    public int pageCount(){...}
}

เราก็ได้คลาสที่เก็บเนื้อหาของ Document กับคำสั่งพื้นฐานมาแล้วครับ ต่อไปเราจะทำคลาสสำหรับอ่านและพิมพ์กัน

interface DocumentReader{
    public void read(Document document, int page);
}

class DocumentViewer implements DocumentReader{
    public void read(Document document, int page){...}
}

class DocumentViewer implements DocumentReader{
    public void read(Document document, int page){...}
}

จริงๆ แล้วตัว Viewer กับ Printer ก็ใช้ Interface เดียวกับครับ เพราะว่าเป็นการอ่านข้อมูลขึ้นมาแสดงผลเหมือนกันนั่นเอง ข้อต่อไปเราจะทำส่วนเซฟ Document กัน

interface DocumentPersistence{
    public void save(Document document, String name);
}

class DocumentToFileSerializer implements DocumentPersistence{
    public void save(Document document, String name){...}
}

class DocumentUploader implements DocumentPersistence{
    public void save(Document document, String name){...}
}

ส่วนนี้ก็เหมือนกับส่วนที่แล้วครับ เราทำเป็น Interface เพราะเรามีโอกาสที่จะเซฟเป็นอย่างอื่นนอกจากไฟล์ เช่น อัพโหลดขึ้น server เซฟลง database ที่เหลือก็มีแค่ writer ละครับ

class DocumentWriter{
    public void write(Document document, int page, String data){...}
}

จะเห็นว่าการแยกออกมานั้นทำให้เสียเวลามาก เอ้ยไม่ใช่ละ ต้องบอกเกี่ยวกับประโยชน์ดิ >< การแยกออกมานั้นทำให้โค้ดของเราดูเป็นระเบียบเรียบร้อย และการแก้ไขก็ทำได้ง่ายขึ้น ทดสอบได้ง่ายขึ้น เวลาเอาไปใช้งาน อยากใช้แค่ไหนก็ import แค่นั้นเลย ไม่ต้องเอาไปทุกฟังก์ชัน อีกเรื่องคือมันจะทำให้เราเห็นโอกาสที่เราพลาดไป เช่นในส่วน viewer เราจะเห็นเลยว่าจริงๆ มันก็คือสิ่งเดียวกับการ print หรือส่วนบันทึก เราก็จะเห็นโอกาสในการบันทึกในแหล่งอื่นๆ นอกเหนือจากไฟล์

สรุป

SRP นั้น ทำให้โค้ดเราดูดีขึ้นครับ แก้ไขง่ายขึ้น ไม่ต้องเทสใหม่ทั้งระบบ แต่ก็ต้องแลกมากับความยุ่งยากและเวลา อย่างไรก็ตามแค่ SRP อย่างเดียว ยังไม่สามารถทำให้เราเขียนโค้ดในระดับเทพได้ ยังต้องอาศัยหลักการอื่นๆ และฝีมือของผู้เขียนด้วย ดังนั้นโปรดติดตามตอนต่อไปครับ

#‎day44 #365วันแห่งโปรแกรม ‪#‎โครงการ365วันแห่ง‬...