Dart: Extension Methods - มาเพิ่มความสามารถให้กับ Third-party Library กันเถอะ
Published on
Authored by Pete. Pittawat Taveekitworachai.
ปฏิเสธไม่ได้เลยว่าการพัฒนาโปรแกรมในปัจจุบันนั้น การใช้งาน Third-party Library นั้นมีความสำคัญเป็นอย่างมาก โดยเฉพาะในแง่ของการลดความซับซ้อนของการพัฒนาโปรแกรมลง ซึ่งช่วยให้เราสามารถพัฒนาระบบต่าง ๆ ได้รวดเร็วมากยิ่งขึ้น
อย่างไรก็ตามหลายครั้งที่เรามักมีความรู้สึกว่า “ถ้ามี xxx มาด้วยก็ดีสินะ” หรือ “อยากให้ xxx ทำแบบนี้มากกว่าแบบที่ให้มาจังเลย” ปัญหาเหล่านี้ หากเป็น Library ที่เราพัฒนาขึ้นมาเอง หรือเป็นส่วนหนึ่งของโปรแกรมของเราก็คงไม่ใช่ปัญหาอะไรที่จะไปแก้ไขโค้ดในส่วนนั้น ๆ ให้มีความสามารถมากยิ่งขึ้น
ถึงกระนั้นการแก้ไข Third-party Library นั้น แทบจะเป็นไปไม่ได้เลย หรืออาจทำให้เกิดความวุ่นวายในการแก้ไขเกินความจำเป็น วิธีปกติที่เราใช้กันเพื่อเพิ่มความสามารถ ก็อาจจะเป็นการ Extends class นั้น ๆ ออกมาเป็น Class ของเราเองและเพิ่มความสามารถเข้าไปให้มีความสามารถตามที่เราตั้งใจไว้
แต่จุดนี้เองที่หลายครั้งมักทำให้โปรแกรมของเรามีการความซับซ้อน เข้าใจได้ยากมากยิ่งขึ้น รวมไปถึงโอกาสในการเกิดข้อผิดพลดาต่าง ๆ รวมถึงเพิ่มระดับความยากในการ Maintain โค้ดชุดนั้น ๆ อีกด้วย
Dart เล็งเห็นปัญหานี้ ดังนั้นตั้งแต่เวอร์ชัน 2.7 เป็นต้นมา Dart ได้มาพร้อมกับฟีเจอร์ที่เรียกว่า Extension Methods ซึ่งช่วยให้ชีวิตนักพัฒนาอย่างเรา ๆ ดีขึ้นไปอีก โดยเฉพาะหากใครที่เคยเขียน Swift ในฝั่ง iOS มาน่าจะคุ้นเคยกันเป็นอย่างดีกับ Pattern นี้
Extension Methods ส่วนขยายเพิ่มความสามารถ
ฟีเจอร์ Extension Methods นั้นเป็นฟีเจอร์ที่เข้ามาเพื่อช่วยให้เราสามารถเพิ่มความสามารถของ Class นั้น ๆ ได้ โดยไม่ต้องอาศัยกับ Extends ออกไปเป็น Subclass นั่นหมายความว่าความสามารถใด ๆ ก็ตามที่เราเพิ่มเข้าไปนั้น จะสามารถเรียกใช้งานผ่าน Class เดิมได้เลยนั่นเอง
โดย Syntax สำหรับการใช้งาน Extension Methods นั้นก็เป็นดังนี้
extension <extension name> on <type> {
(<member definition>)*
}
โดย Extension name นั้นเราสามารถละไว้ได้ แต่ที่ Dart แนะนำให้เราตั้งชื่อด้วยนั้น เนื่องจากเราไม่รู้ว่านักพัฒนาคนอื่น ๆ ในทีมจะบังเอิญตั้งชื่อ Method ใน Extension เหมือนกับเราหรือไม่ ซึ่งอาจทำให้เกิด Conflict ได้
ดังนั้นก็ตั้งชื่อเสมอย่อมเป็นเรื่องที่ดี เนื่องจากเวลาเรา Import เราสามารถใช้ show
, hide
, as
เข้ามาช่วยในการจัดการ Conflict เหล่านี้ได้ หรือหากเราต้องการให้ใช้งานทั้ง 2 Method ก็อาจจะใช้การทำ Wrapper เข้ามาเพื่อระบุได้ เช่น
<extension name>(<literal value>).<extended method>()
// For example
StringExtension('123').parseInt();
StringExtension2('456').parseInt();
ตัวอย่าง: ลูกอม
สมมติว่าเรามี Class ที่เราใช้งานจาก Third-party Library ใช้ในการสร้าง ลูกอม (Candy) ขึ้นมา สำหรับโรงงานของเรา ดังนี้
class Candy {
final List<String> _flavors = [];
final int _sweetness;
Candy(this._sweetness);
String get detail =>
'This candy has degree of sweetness at $_sweetness and has the following flavor(s): $_flavors';
void addFlavor(String flavor) {
_flavors.add(flavor);
}
}
แต่แล้ววันดีคืนดี โรงงานของเราตัดสินใจว่าลูกอมจะต้องมีลูกเล่นใหม่ ๆ เพื่อให้แข่งขันกับคู่แข่งได้ โดยการที่รสชาติของลูกอมนั้นสามารถหายไปได้ แต่เนื่องจากโรงงานของเราใช้งานเครื่องจักรผลิตลูกอมที่สั่งมาจากต่างประเทศ (ใช้งาน Third-party Library) เราจึงไม่สามารถแก้ไขลูกอม (Class) นั้นได้โดยตรง
แต่เราก็นึกขึ้นได้ว่าเราลูกอมของเราเขียนขึ้นในภาษา Dart เราสามารถ Extension ออกมาได้ โดยที่ไม่ต้องทำให้ลูกอมนั้นเป็นประเภทพิเศษ (Extends ให้กลายเป็น Subclass) เราจึงเขียนโค้ดเพิ่มขึ้นมาดังนี้
extension on Candy {
void removeFlavor(String flavor) {
_flavors.remove(flavor);
}
}
เพียงเท่านี้เราก็สามารถเรียกใช้งาน candyInstance.removeFlavor("Mint");
ได้แล้ว อย่างไรก็ตาม ข้อควรระวังหนึ่งก็คือ เราไม่สามารถเพิ่ม Instance Variables ให้กับ Extension Methods ได้ตามที่ชื่อนั้นบอกใบ้เราไว้เลย
หากเราต้องการเพิ่ม Instance Variables แล้วละก็ เราอาจพิจารณาใช้การ Extends ไปเป็น Subclass แทน ซึ่งเป็นวิธีที่มีความเหมาะสมมากกว่า
ใช้ Extension Methods กับ Built-in Class
จริง ๆ แล้ว Extension Methods นั้นไม่ได้ใช้งานเฉพาะกับ Class ที่มากับ Third-party Library เท่านั้น แต่สามารถใช้ได้กับทุก Class รวมไปถึง Class ที่เป็น Built-in Class ใน Dart ด้วยเช่นกัน เช่น หากเราต้องการให้ String มีความสามารถในการ Return Reversed Uppercase String เราก็สามารถใช้งาน Extension Methods ในการเพิ่มความสามารถนั้นได้เช่นกัน เช่น
extension ExtendedString on String {
String get uppercaseReversed => this.toUpperCase().split('').reversed.join();
}
ซึ่งจะทำให้เราสามารถเรียกใช้งานได้แบบนี้
print('Hello, Dart!'.uppercaseReversed);
// OUTPUT: !TRAD ,OLLEH
Final Thought
สำหรับ Extension Methods นั้น ไม่ได้มีความจำเป็นสำหรับการเรียนรู้ที่ต้องเข้าใจสำหรับผู้ที่พึ่งเริ่มศึกษา Dart อย่างไร (แต่ไหน ๆ ก็อาจมาถึงตรงนี้แล้ว ก็อาจจะไม่ทันแล้ว -.-) และไม่ใช่ฟีเจอร์ที่ Dart บังคับให้ทุกคนใช้ เป็นเพียงทางเลือกหนึ่งเท่านั้น ที่นำไปใช้ หรือไม่ใช้ก็ได้ตามที่เห็นสมควร
นอกจากนี้ยังมีอีกหลากหลายวิธีสำหรับการประยุกต์ใช้เช่นกัน เช่น วิธีหนึ่งที่มักจะเห็นได้ใน iOS คือ การใช้งาน Extension เพื่อแยกส่วนของโค้ดออกเป็นส่วน ๆ ให้มีความเป็นระเบียบ และในแต่ละ Extension มีความจำเพาะต่อส่วนของ Logic
โดยสรุปก็ถือว่าเป็นหนึ่งในฟีเจอร์ที่น่าสนใจและเพิ่มความสะดวกสบาย รวมถึงทำให้โค้ดของเราอ่านได้ง่ายยิ่งขึ้นด้วยเช่นกัน