Dart: Extension Methods - มาเพิ่มความสามารถให้กับ Third-party Library กันเถอะ

Dart Aug 4, 2020

ปฏิเสธไม่ได้เลยว่าการพัฒนาโปรแกรมในปัจจุบันนั้น การใช้งาน 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

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


📚 Hope you enjoy reading! 📚


Tags

Pittawat Taveekitworachai

A CS student who passionate about web and mobile technology with the belief that technology can enhance people's life.