Literature Review: Dependency Injection + Inversion of Control

ช่วงนี้ผมรู้สึกว่าการใช้ Inversion of Control หรือที่เรียกกันสั้นๆ ว่า “IoC” และ Dependency Injection หรือ “DI” กำลังเป็นที่พูดถึงอย่างกว้างขวางมาก และกำลังได้รับความนิยมใน Framework ต่างๆ ของหลากหลายภาษาครับ ผมเคยรู้จัก IoC ครั้งแรกเมื่อประมาณ 3-4 ปีที่แล้ว ตอนที่กำลังพัฒนาระบบด้วย NopCommerce เวอร์ชั่น 3 ซึ่งกำลังถูก Rewrite ใหม่ให้ใช้ ASP.NET MVC จากเดิมที่เป็น ASP.NET Web Form ธรรมดา และจากนั้นผมก็พบกับมันมากขึ้นเรื่อยๆ จนตอนนี้แม้แต่ Microsoft Azure Mobile Service หรือกระทั่ง Nancy ที่ผมชอบมาก ก็ใช้หลักการแบบ IoC ในการพัฒนาแทบทั้งสิ้น

ในเมื่อใครๆ เขาก็ใช้กัน ต่อไปถ้าเราจะพัฒนาอะไรขึ้นมา เราก็ควรจะต้องใช้ Pattern นี้บ้างสินะ? ถ้าอย่างนั้น เราควรจะต้องมาทำความรู้จักกับ IoC และ DI ให้มากขึ้นกันดีกว่า

ที่มา-ที่ไปของ DI+IoC

เพื่อความเข้าใจที่กระจ่างมากขึ้น เราลองมาดูตั้งแต่พื้นฐานของแนวคิด IoC ไปตามลำดับเลยนะครับ โดย IoC นั้น มีพื้นฐานความคิดมาจาก Hollywood Principle

Hollywood Principle [wikipedia]

“Don’t Call us, we’ll call you”

ความต้องการของ Hollywood Principle ก็คือตามประโยคด้านบนนั่นละครับ คือ “คุณไม่ต้องเรียกเรา(ระบบ) เดี๋ยวเรา(ระบบ)เรียกคุณเอง” ฟังดูพิลึก แต่ว่านั่นคือสิ่งที่ IoC ต้องการช่วยเราในแง่ของการวางโครงสร้างครับ และโดยที่เราไม่รู้ตัว โดยเฉพาะถ้าเกิดว่าเราพัฒนาด้วย Platform ของ Microsoft ไม่ว่าจะเป็น WinRT, WPF, Silverlight, ASP.NET แม้แต่ Windows Form

ยกตัวอย่างเช่น ถ้าเป็นการเขียนโปรแกรมบน Windows Form นะครับ การที่เราจะเขียนโค๊ดเพื่อให้ผู้ใช้กดปุ่ม แล้วมีคำว่า “Hello World” ปรากฏขึ้นมา ก็คงต้องใช้โค๊ดแบบด้านล่างนี้ เราลองมาพิจารณาโค๊ดชุดนี้กันดูนะครับ

  1. “ใคร” คือผู้ที่สั่งให้หน้าจอ Windows Form แสดงผลขึ้นมา (เราเป็นคนสั่ง)
  2. “ใคร” คือคนที่วาดหน้าจอ Winows Form (ไม่เห็นมี)
  3. “ใคร” คือคนที่ทำการวัดพิกัดของเมาส์ และเช็คว่าเมาส์อยู่ด้านบนปุ่มหรือไม่ และทำให้ปุ่มเรืองแสง ถ้าเมาส์ชี้อยู่เหนือปุ่ม (ไม่มีเหมือนกัน)
  4. “ใคร” คือคนที่จัดพฤติกรรมของผู้ใช้ว่า ผู้ใช้ต้องการจะกดปุ่ม (ไม่มีอีกนั่นแหละ)
  5. “ใคร” คือคนที่ตัดสินใจว่า ถ้าปุ่มถูกกด จะแสดง Message Box ขึ้นมา (เราเอง)
  6. “ใคร” คือคนที่กำหนดว่า Message Box จะต้องเป็นหน้าต่างเล็กๆ ตรงกลางหน้าจอ มีปุ่ม OK ปุ่มหนึ่ง? (ไม่มีอยู่ดี)

สมมุติว่า ถ้าเราต้องการที่จะสร้าง Windows Form ขึ้นมาเองบ้างละ? แต่ว่าใช้ DirectX หรือ OpenGL ในการแสดงผลหน้าจอแทน ผู้ที่รับผิดชอบหน้าที่ทั้ง 6 อย่างนั้นจะเปลี่ยนไปเลย เพราะว่าคุณจะต้องทำหน้าที่ในการเขียนโค๊ดทั้งหมด ตั้งแต่การวาดหน้าจอ การเช็ดพิกัดของเมาส์ การทำให้ปุ่มเรืองแสงได้เวลาเมาส์ชี้ เข้าใจว่าถ้าผู้ใช้คลิกปุ่มซ้าย ขณะที่เมาส์ชี้ที่ปุ่มหมายถึงผู้ใช้ต้องการจะคลิกปุ่ม โดยใช้ API ที่ Windows/DirectX มีให้ ในการติดต่อกับ GPU และเมาส์

ในขณะที่ถ้าเราใช้ Windows Form เราก็แค่ทำการเรียกคำสั่งของ Windows Form ให้หน้าจอแสดงออกมา แล้วรอฟังว่าเมื่อไหร่ปุ่มจะโดนคลิ๊ก “ไม่ต้องมาเรียกเรา เดี๋ยวเราเรียกคุณเอง”

image
รูปที่ 1: ใครเรียกใคร? เราคิดว่าเราเป็นคนสั่งให้ Windows Form ทำงาน
แต่จริงๆ Windows Form มาเรียกเราให้ทำงานต่างหาก!

Inversion of Control [wikipedia]

“In software engineering, inversion of control (IoC) describes a design in which custom-written portions of a computer program receive the flow of control from a generic, reusable library. A software architecture with this design inverts control as compared to traditional procedural programming: in traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the reusable code that calls into the custom, or task-specific, code.”

สรุปใจความของ IoC ก็คือ แทนที่โปรแกรมเมอร์ ซึ่งเป็น”ผู้ใช้” ของ Windows Form (ซึ่งเป็น Library ให้เราใช้งาน) จะเป็นคนเขียนโค๊ด เพื่อกำหนดว่า Windows Form จะต้องทำงานอย่างไร ตามบทบาทของการที่ Windows Form นั้นเป็น Library แต่กลับกลายเป็นว่า โค๊ดที่โปรแกรมเมอร์เขียน กลับกลายเป็นผู้ที่ถูก Windows Form ใช้งานแทน นั่นก็คือ Windows Form ก็จะทำงานไปตามใจชอบ แต่ถ้าถึงตรงที่โปรแกรมเมอร์กำหนดไว้ Windows Form ก็จะมาเรียกให้โค๊ดของโปรแกรมเมอร์ทำงาน โดยที่เราไปเปลี่ยนแปลงการทำงานของ Windows Form นอกเหนือจากจุดที่กำหนดไว้ไม่ได้

ดังนั้น Windows Form จึงไม่ใช่แค่ “API” หรือ Library ที่ใช้สำหรับการแสดงผลหน้าจอครับ แต่ว่า Windows Form จริงๆ แล้วก็อาจนับได้ว่าเป็นระบบแบบ IoC ชนิดหนึ่ง ขออ้างคำพูดของคุณ Martin Fowler ไว้เลยแล้วกันครับ:

“A framework embodies some abstract design, with more behavior built in. In order to use it you need to insert your behavior into various places in the framework either by subclassing or by plugging in your own classes. The framework's code then calls your code at these points.”

“Framework (แตกต่างจาก Library ตรงที่) มีการออกแบบโครงสร้าง และขั้นตอนการทำงานอยู่ข้างใน ในการใช้งาน Framework คุณจะต้องแทรกการทำงานของคุณเข้าไปยังจุดต่างๆ ภายใน Framework ด้วยวิธีการต่างๆ ไม่ว่าจะเป็นการสร้างคลาสลูก (Subclass) หรือโยนคลาสของคุณไปให้ตัว Framework ซึ่งโค๊ดภายในตัว Framework จะมาเรียกใช้งานโค๊ดของคุณ ตามจุดต่างๆ เหล่านี้”

จากที่ผมได้ลองดู ระบบที่พัฒนาโดยใช้แนวคิดของ IoC โดยทั่วไปก็จะมีลักษณะคล้ายคลึงกัน ดังนี้ครับ

  • มีการวางผังโครงสร้างการทำงานเอาไว้ทั้งหมด คล้ายๆ เป็น Workflow ที่ร้อยการทำงานของส่วนต่างๆ เข้าด้วยกัน แต่ด้านในไม่มี Implementation อยู่ มีแค่แผนผังการทำงาน ว่าจะต้องทำงาน A ก่อน แล้วจึงค่อยทำงาน B แต่ว่าไม่ได้บอกว่า งาน A ทำอย่างไร และงาน B ทำอย่างไร และคุณ ซึ่งเป็นผู้ใช้ ไม่สามารถเขียนการทำงาน A หรือ B ที่ไปเปลี่ยนโครงสร้างหรือแผนผังการทำงานนี้ได้
  • การทำงานส่วนต่างๆ จะถูกแยกชิ้นออกจากโครงสร้างหลัก โดยผู้ที่ออกแบบระบบจะเป็นผู้กำหนดว่า จุดใดบ้างที่สามารถเปลี่ยนวิธีการทำงานได้ และกำหนดให้เรานำเอา Implementation วางลงไป
  • มีกระบวนการที่ทำการประกอบร่างระบบขึ้นมา ตามที่ผังการทำงานระบุไว้ โดยเอาการทำงานที่เป็นชิ้นๆ ประกอบเข้าไปในผัง เมื่อทุกชิ้นส่วนประกอบครบถ้วน ระบบจึงจะสามารถทำงานได้
image 
รูปที่ 2 ระบบแบบ IoC สามารถถอดประกอบแยกชิ้นได้ เหมือนรถยนต์สามารถวางเครื่องใหม่ที่แรงขึ้น
หรือเปลี่ยนล้อแม็กซ์ได้ แล้วก็ยังวิ่งได้เหมือนเดิม (แต่จะดีเหมือนเดิมมั๊ยนี่อีกเรื่องนึง)

ประโยชน์ของ IoC

ผมมีความเห็นว่าการออกแบบตามแนวคิดของ IoC มีประโยชน์หลักอยู่สี่ข้อคือ

  • เพื่อให้สามารถแยกชิ้นส่วนของการทำงานได้อย่างอิสระจากกัน (De-Coupling) ยิ่งเรา De-coupling ได้มากชิ้นงานที่ได้ก็จะไม่สปาเก็ตตี้ ไม่พันกันยุ่งมาก  และการเอาชิ้นงานที่พัฒนาเสร็จแล้ว ไปใช้ในระบบอื่นๆ ก็สามารถทำได้ไม่ยากเย็นนัก เพราะว่าชิ้นงานส่วนใหญ่ จะอยู่ได้ด้วยตัวเอง ไม่ต้องพึ่งกัน โดยที่ในภาพรวมแล้ว ระบบยังทำงานตามโครงสร้างและขั้นตอนเดิมที่กำหนดไว้
  • ทำให้สามารถ “Config” ระบบได้ เนื่องจากเราไม่ได้ผูก (Coupling) Concrete Class (ก็คือคลาสธรรมดา) เข้าด้วยกัน แต่เราผูกกันด้วยสิ่งที่เป็นมโนภาพ (Abstract) เช่น Interface คือมีแต่หัวฟังก์ชั่นกำหนดไว้ ยังไม่ได้ระบุตัวฟังก์ชั่นว่าจะต้องทำงานอย่างไร เป็นแค่มโนเอาไว้ว่าจะต้องทำแบบนี้ได้ หรือ Abstract Class คือคลาสที่ยังไม่สมบูรณ์ บางฟังก์ชั่นอาจจะมีการทำงานระบุไว้ บางฟังก์ชั่นยังไม่มี ทำให้เราสามารถใช้ความสามารถ Polymorphism ของ OOP ในการเปลี่ยนแปลงการทำงานของระบบได้ โดยนำเอาคลาสที่ Implement Interface หรือเป็น Subclass ของ Abstract Class นั้น มาสวมลงไปแทนที่
  • สามารถทำ Unit Test ได้ง่าย เพราะว่าเรามีการแยกชิ้นชัดเจน มันก็เลยเป็น Unit อย่างดี และเนื่องจากว่าแต่ละชิ้นมีการระบุ Dependency ระหว่างกันชัดเจน เราก็สามารถสร้างชิ้นที่เป็น Dependency หลอกๆ ขึ้นมาได้ (Mock/Fake) โดยให้ชิ้นที่เป็น Depdencency พวกนี้ เทสยังไงก็ไม่พัง รันยังไงก็ผ่าน เพื่อจำกัดวงการทดสอบให้อยู่เฉพาะใน Unit ที่เรากำลังเทสจริงๆ ถ้าไม่มีการทำ Mock ไว้ บางที โค๊ดของ Unit นี้ใช้งานได้ แต่ไปพังที่ตัวที่เอามาใช้ ก็จะทำให้ผลการทดสอบผิดพลาด เกิดการจับแพะไปแทน ผู้ร้ายตัวจริงลอยนวลไป
  • สามารถออกแบบระบบที่รองรับการเปลี่ยน Environment ของระบบได้ เช่น การเปลี่ยนยี่ห้อฐานข้อมูล หรือ Application ที่รันได้ทั้งบน Windows, Windows Phone และ Xamarin Forms เป็นต้น โดยการทำส่วนที่เป็น Platform-Specific/Environment เป็น Interface เอาไว้

การสร้างระบบที่ใช้แนวคิดแบบ IoC ขึ้นมา

จะเห็นว่าการสร้างระบบด้วยแนวคิดแบบ IoC นั้นน่าสนใจมาก จะว่าไปเหมือนเรากำลังทำการ Abstract สิ่งที่เราต้องทำซ้ำซ้อนกันบ่อยๆ ให้ตกผลึกมาเป็นสิ่งที่เอามาใช้ซ้ำกับงานอื่นๆ ต่อไปได้ (Framework) เพียงแค่เปลี่ยนแปลงจุดสำคัญบางจุดเท่านั้น การทำให้ระบบ หรือ Framework เรา สามารถรองรับการเปลี่ยนแปลงจุดสำคัญที่เราต้องการจะให้เปลี่ยนได้นี่ละ คือความท้าทายครับ เนื่องจาก IoC เป็นเพียงแค่แนวคิด แต่การที่จะทำออกมาให้ใช้งานได้นั้น จะต้องอาศัย Implementation ซึ่งก็มีคนตีเป็น Pattern เอาไว้ให้เราแล้วหลายแบบด้วยกัน ซึ่งทุก Pattern ก็เพียงแค่ต้องการแก้ปัญหาเดียวกันคือการ De-couple การทำงานที่สามารถเปลี่ยนแปลงได้ ออกจากตัว Framework ครับ ซึ่ง Pattern ที่ได้รับความนิยมมากคือ Dependency Injection หรือ DI ครับ

Dependency Injection [wikipedia]

เนื่องจากว่าการทำงานแต่ละชิ้นนั้น ถูกแยกอิสระจากกัน ไม่มีใครที่เป็นคนพัฒนาระบบโดยภาพรวมคนเดียวแบบที่เราเขียนทุกอย่างใน Function Main หรือ “ปุ่มวิเศษ” ที่มีโค๊ดการทำงานทุกอย่างอยู่ใน Button_Click จึงจะเกิดปัญหาว่า ถ้าจะใช้การทำงานชิ้นนี้ จะต้องหาจากไหน (ผมเรียกว่า “ชิ้นงาน” ซึ่งการทำงานหนึ่งอย่าง อาจจะมีหลายคลาสก็ได้ หรืออาจจะไม่จำเป็นต้องทำเป็นคลาสก็ได้นะ) ตัวอย่างเช่น

  • ถ้าชิ้นงาน A ต้องการจะแสดง MessageBox จะต้องใช้งานคลาสไหน และแน่ใจได้อย่างไรว่าจะไม่แสดง MessageBox ซ้อนกับคนอื่น?
  • ถ้าชิ้นงาน B ต้องการจะบันทึกข้อมูล เพื่อใช้งานร่วมกัน เช่น ชื่อผู้ใช้ที่กำลังใช้งาน จะต้องบันทึกที่ไหน?

การที่ชิ้นงาน A และ B มีความต้องการดังนี้ ก็คือ ชิ้นงานทั้งสอง มี Dependency กับชิ้นงานอื่น ซึ่งทางเดียวที่จะช่วยให้ชิ้นงาน A รู้ว่าจะต้องใช้คลาสไหน ก็คือการ “บอก” ไปยังชิ้นงาน A นั่นเองครับ

 image
รูปที่ 3 เลือกเอาว่า จะส่ง Instance ของคลาสอะไรให้ A เอาไช้งาน ตราบใดที่คลาสนั้น Implement IShow ก็ใช้ได้หมด

วิธีการ “บอก” จริงๆ แล้วก็คือการส่งค่าของสิ่งที่ A ต้องการใช้งานไปให้เท่านั้นเอง เช่น กรณีนี้ ชิ้นงาน A ต้องการใช้ MessageBox เราก็ทำการส่ง Instance ของ MessageBox (ก็คือตัวแปรชนิด MessageBox นั่นแหละ) ไปให้ชิ้นงาน A และเมื่อ A ได้รับค่ามาผ่านตัวแปร ก็เพียงเรียกใช้สิ่งที่ตนเองต้องการ จากตัวแปร ที่ได้รับมา ซึ่งวิธี “บอก” ก็มีได้หลายวิธีครับ

  • บอกผ่าน Constructor ก็คือ ถ้าจะสร้าง (Instantiate) A ขึ้นมาได้ จะต้องมี Instance ของคลาส MessageBox พร้อมอยู่ก่อนแล้วเท่านั้น โดยส่วนตัวแล้วผมมองว่ามันสามารถสื่อสารเรื่อง Dependency ระหว่าง A กับ MessageBox ได้ชัดเจนกว่าวิธีอื่นมาก แต่ว่าข้อเสียคือ ระหว่างที่รันอยู่ จะเปลี่ยนไปใช้ Service ตัวอื่นได้ลำบาก วิธีนี้คือวิธีที่ NancyFX ใช้ครับ
  • บอกผ่าน Parameter เวลาที่จะเรียกให้ A ทำงาน เช่น A.Calculate( MessageBox m ) กรณีนี้ก็คือ หลังจากที่ A ทำงานเสร็จแล้ว ต้องการแสดงผล ควรจะแสดงผลโดยใช้ฟังก์ชั่นจากตัวแปร m ที่ระบบ “บอก” มาให้ ข้อเสียคือ จะต้องส่ง Instance ให้กับ A ทุกครั้งที่เรียกใช้การทำงานของ A แต่ข้อดีคือ A สามารถสร้างขึ้นมาได้ โดยไม่ต้องมี MessageBox อยู่ก่อน
  • บอกผ่าน Attribute ของคลาส A เช่น A.MessageBox = m; A.Calculate() วิธีนี้ผมมองว่าเป็นวิธีที่แย่ที่สุด เพราะว่าก่อนที่ Calculate จะทำงานได้ จะต้องมีการตั้งค่า Attribute MessageBox ให้ถูกต้องเสียก่อน ซึ่งเป็นสิ่งที่ควรหลีกเลี่ยงอย่างยิ่ง [Sequencial Coupling] เพราะถ้าจะเรียกใช้ Calculate จะต้องทราบด้วยว่า จะต้อง Set ค่าของ MessageBox ก่อน แน่นอนว่า คนที่ทราบก็มีเพียงแค่โปรแกรมเมอร์ที่สร้างคลาส A เท่านั้น และจะต้องจดใส่ Documentation เอาไว้ด้วย ซึงก็อาจจะไม่มีคนตั้งใจอ่านอยู่ดี

แต่ว่าในการรับตัวแปรเข้ามา (Inject) เรามักจะไม่ใช้ Concrete Class (คลาสทั่วๆ ไป) ในการ Inject ครับ แต่เราจะใช้ Interface แทน ลองดูโปรแกรม “Hello World” ของเราต่อ ตอนนี้เราสามารถเปลี่ยนวิธีได้ด้วย ว่าจะให้แสดงข้อความ “Hello World” อย่างไร อยากจะแสดง MessageBox เหมือนเดิม หรืออยากจะให้เขียนลง Text File

จะเห็นว่า ถ้าเราต้องการ “เปลี่ยนวิธีการ” ในการแสดงคำว่า “Hello World” ก็สามารถทำได้ ตอนที่เรา Instantiate ตัว IoCForm ขึ้นมานั่นเอง (บรรทัดที่ 61) แล้วก็เลือกว่า จะใช้ Implementation ไหนของ IShow ในการทำงาน

แต่ก็ยังมีปัญหาอยู่คือ แล้วจะไปเอา Instance ของ MessageBox ที่ A ต้องการมาจากไหนส่งให้ A!?!?!? ทางหนึ่งที่ทำได้ และเรามักจะคิดได้ก่อนคือ เราก็สร้างโรงงานสร้าง A ขึ้นมาสิ

Factory Method Pattern [wikipedia]

อีกทางที่ทำได้ในการสร้าง Instance ของชิ้นงานที่มี Dependency คือสร้างฟังก์ชั่นที่ทำการประกอบชิ้นงานนั้นขึ้นมา ซึ่งก็คือ Factory Method ครับ

สำหรับ Factory Method นั้นก็มีด้วยกันสองแบบคือแบบ Static กับแบบ Abstract ต่างกันตรงที่ แบบ Abstract คือ เราจะมี Factory ของ Factory อีกทีหนึ่ง เพื่อที่จะได้เปลี่ยน Factory ได้ครับ ขอยกตัวอย่างเป็น Static Factory เพียงอย่างเดียวแล้วกัน อย่างเช่นกรณีของ ‘'งาน A” ที่ต้องการใช้ MessageBox เราสามารถทำได้ โดยการสร้าง Factory Method ที่สร้าง A ขึ้นมา ซึ่งโค๊ดก็ควรจะมีอยู่บรรทัดเดียว ก็คือ return new A();

แต่ทีนี้ เนื่องจาก A ต้องการใช้ MessageBox ตัว Factory Method ของ A ก็ต้องทราบด้วยว่า A นั้น ก่อนจะสามารถใช้งานได้ จะต้องได้รับการ “บอก” Implementation ของ MessageBox ก่อน เพื่อที่จะได้ทำการกำหนดค่าของ A ให้ครบถ้วน ดังนั้น Factory Method ก็ต้องจัดการสร้าง Instance ของ MessageBox ขึ้นมาด้วย เพื่อมอบให้ A ก่อนจะคืนค่าออกไป

ถ้าอย่างนั้น เราก็จะต้องสร้าง Factory Method สำหรับทุกคลาสที่มี Dependency อย่างนั้นหรือ?

ปัญหาของ DI, Factory Method และบทบาทของ IoC Container (เช่น TinyIoC)

ถ้ามองดูให้ดีจะพบว่า DI และ FM นั้น ย้ายภาระในการหา Instance ของสิ่งที่คลาสต้องการใช้งาน ออกมาจากตัวคลาสเท่านั้นเอง แต่ว่า มันไม่ได้แก้ปัญหาให้คนที่ออกแบบระบบเลย

ในการใช้งานจริงของ DI, FM จึงจำเป็นจะต้องมี Library เฉพาะกิจ ที่ทำขึ้นมาเพื่อช่วยเราในเรื่องนี้ ซึ่ง Library ที่ได้รับความนิยมมากตัวหนึ่ง ก็คือ TinyIoC ครับ (อีกตัวก็คงจะเป็น AutoFac) ผมแนะนำให้ลองเข้าไปดู Source Code ของ TinyIoC หรือ SimpleIoC (ซึ่งมัน Tiny กว่า TinyIoC) ดูประกอบด้วยนะครับ

สิ่งที่ TinyIoC ทำก็คือ ทำหน้าที่เป็น Service Locator+Factory ให้เราครับ โดย IoC Container พวกนี้ ก็จะมีฟังก์ชั่นหลักๆ อยู่สองอย่าง

  • Register ซึ่งจะรับ Interface กับ Implementation เช่น Register( typeof( IShow ), typeof( MessageBoxShow ) ) คือ บอกว่า ถ้ามีคนจะใช้ IShow ให้ใช้ Implementation (คลาส) ชื่อ MessageBoxShow นะ
  • Resolve ซึ่งจะรับ Type ของ Interface ที่เราต้องการ เช่น Resolve( typeof( IShow) ) จากที่เมื่อสักครู่เรา Register ไปว่า IShow ให้ใช้ MessageBoxShow กรณีนี้ เราก็จะได้ Instance ของ MessageBoxShow ออกมา

    และถ้าเราใช้คำสั่ง Resolve( typeof( A) ) บ้าง ในฟังก์ชั่น Resolve ก็จะทำการหาให้ด้วยว่า A มี Dependency อะไรอีกบ้าง สมมุติว่า A ใน Constructor รับ IShow ตัวฟังก์ชั่น Resolve ก็จะทำการค้นหามาให้อีกว่า IShow จะต้องใช้ Implementation ไหน แล้วทำการสร้าง Instance ของ Implementation นั้น ส่งให้ Constructor ของ A ก่อนที่จะคืนค่า Instance ของ A ที่ได้รับการประกอบร่างเรียบร้อยแล้วออกมา

จะว่าไป TinyIoC นี่ควรเรียกชื่อว่า TinyFactory น่าจะถูกต้องกว่านะ ส่วน AutoFac(tory) นี่ ตั้งชื่อได้ถูกต้องแล้ว

ทางเลือกอื่นๆ ในการทำ IoC

นอกจาก DI และ FM แล้ว เรายังมีทางเลือกอีกหลายทางในการทำระบบแบบ IoC ครับ ขอเริ่มเรียงลำดับจากพื้นฐานที่สุดไปตามลำดับ

State and Strategy Pattern [wikipedia]

จริงๆ แล้ว DI นั้น ก็ก้ำกึ่งกับ State & Strategy มากเลยทีเดียว คือเราจะต้องออกแบบคลาส ด้วยวิธีแบบ State and Strategy จึงจะสามารถใช้ DI ในการ Inject Dependency เข้ามาได้ แต่ถ้าตามแบบของ State and Strategy ก็คือ ออกแบบให้ส่วนที่จะ Customize ได้นั้น เป็น Attribute ของคลาส แล้วเราก็ “เปลี่ยน” Implementation โดยการเอา Instance ของคลาสใหม่ วางลงไปแทน แต่ว่า State and Strategy ไม่ได้ระบุเอาไว้ถึงวิธีที่จะได้มาซึ่ง Implementation เหล่านั้น DI จึงเป็น Pattern ที่มาเติมให้ครบถ้วนขึ้นครับ

Template Method Pattern [wikipedia]

อีกวิธีที่เราจะทำตัวเป็น IoC ก็คือ เราทำการสร้างคลาสที่เป็นแกนของระบบเอาไว้โดยทำให้บาง Method ที่เป็นจุดที่ Customize ได้ ให้เป็น protected ไว้ เพื่อให้ Subclass มา Override ได้ ก็จะคล้ายกับ DI และ State and Strategy อยู่เหมือนกัน แต่ข้อจำกัดคือ เราไม่สามารถ Config ระบบตอน Runtime ได้ แต่ได้ข้อดีมาคือ เรารู้ตั้งแต่ตอน Compile เลยว่า เราประกอบระบบครบหรือไม่

อันที่จริงแล้ส นี่ก็น่าจะเป็นวิธีของ Windows Form, WPF, Silverlight, ASP.NET, WinRT เหมือนกัน เช่น หน้าจอสามารถให้เรา Override เอาว่า OnActivated จะทำอะไร แต่ถ้าเราไม่ทำอะไร มันก็จะ Activated เฉยๆ)

หรือถ้าจะเอาแบบ Extreme (เพ้อเจ้อ) ไปเลย อยากจะเปลี่ยนระบบตอน Runtime ด้วย เราก็สามารถประกอบร่างระบบด้วยการ Generate Code แล้ว Compile มันเลยด้วย Managed Compiler จากนั้นค่อยสั่งให้ระบบทำงาน ก็อาจจะเป็นไปได้เหมือนกัน

Observer Pattern [wikipedia]

แทนที่เราจะต้องทำให้เกิด Coupling กันกับคลาสที่เราสร้าง กับ Implementation ที่เราต้องการใช้ เราสามารถทำให้ Coupling ทั้งหมดหายไปเลยก็ยังได้!

ยกตัวอย่างเช่น ถ้าคลาสต้องการจะเขียนลง Log ซึ่งถ้าเป็น DI คลาสนี้ก็จะต้องรับ ILog เข้ามาทาง Constructor

ถ้าหากว่าใช้ Observer Pattern ก็คือ ผู้ออกแบบคลาสนี้จะสร้าง Event (ใน C#) ว่า public Action<object, string> LogWritten ขึ้นมา และเมื่อต้องการเขียน Log โค๊ดภายในคลาสนี้ก็แค่ทำการสั่งให้ Event นี้ทำงาน ถ้าตอนนั้น มีคนมา Handle Event นี้อยู่ Log ก็จะโดนเขียนลงไป มองอีกมุมหนึ่ง ก็คือ Observer Pattern แปรสภาพของ Dependency จากรูปของคลาส ให้อยู่ในรูปของ Function แทน (สำหรับภาษาซึ่งมี Function Pointer หรือ Delegate ใน C#) จะว่าไป Windows Form, WPF, Silverlight, ASP.NET, WinRT ก็ใช้วิธีนี้ในการทำ IoC ตามตัวอย่าง Hello World ในตอนต้นครับ

Service Locator [wikipedia]

นอกจากการส่ง Instance ของ Implementation ที่เราต้องการกันไปมาแล้ว อีกวิธีหนึ่งที่ทำได้ก็คือเราก็ใช้วิธี “ประกาศ” เอาไว้ให้เลยว่า MessageBox จะต้องใช้ตัวนี้นะ ซึ่งภาษาที่ไม่มี Static จะไม่สามารถทำแบบนี้ได้ หรือทำได้ยาก เพราะว่า เราก็จะต้องหาตัวคนประกาศให้เจอก่อนอยู่ดี ซึ่งก็วนมาที่ปัญหาเดิมคือ จะเอาคนประกาศมาจากไหน กลายเป็นต้องใช้ DI “บอก” ซ้ำมาอีกรอบว่า ให้ใช้คนประกาศคนนี้นะ

ตัวอย่างเช่น สำหรับใน Platform ที่เป็น Windows-Based เราก็ไม่จำเป็นจะต้อง Inject MessageBox เข้ามาให้คลาส A ก็ได้ เพราะว่า ทั้งบน Windows Form, WPF, Silverlight, Windows Phone ก็จะมี คลาสนี้อยู่ ซึ่งเป็น Static แต่ว่าอยู่คนละ Namespace กันเท่านั้น (ใช้ Compiler Directive ในการ Map Namespace ช่วยก็ได้เช่นกัน) ถ้ามองในอีกมุมหนึ่ง ตัว MessageBox เอง ก็เหมือนเป็น Service ในตัวอยู่แล้วครับ

แน่นอนว่า การใช้ Service Locator มีปัญหาคือ เราไม่สามารถรู้ล่วงหน้าได้เลยว่า คลาส A นั้น ต้องการใช้ MessageBox เพราะว่า โค๊ดที่คลาส A ใช้งาน MessageBox นั้น จะถูกซุกซ่อนอยู่ใน Method ต่างๆ ทางเดียวที่จะรู้ได้คือจะต้องสั่งให้คลาส A ทำงานก่อนเท่านั้น และก็รอดูตอน Runtime ว่า คลาส A จะเรียกใช้อะไรบ้าง

จะเห็นว่า มีอีกหลากหลายทางเลือกเลยทีเดียว ในการที่จะทำให้ระบบเราเป็น IoC ได้ ไม่จำเป็นว่า จะต้องใช้ IoC คู่กับ DI เสมอไป แต่ว่าการใช้ DI ได้รับความนิยมมากกว่าวิธีอื่นๆ ในการสร้างระบบแบบ IoC โดยเฉพาะ DI แบบ Constructor Injection เพราะว่าสามารถสร้าง Tool ตรวจสอบก่อนที่โปรแกรมจะทำงาน (Static Analysis) เพื่อเช็คได้ว่า มี Dependency ระหว่าง A กับอะไรบ้าง และถ้าจะสร้าง Instance ของ A ขึ้นมาได้ จะต้อง Inject อะไรให้ A บ้าง และที่สำคัญคือ ในระบบตอนนั้น มี Dependency ครบแล้วหรือยัง และมี Circular Dependency หรือไม่ ผมเดาว่า AutoFac ก็น่าจะใช้ประโยชน์จากจุดนี้ในการทำงานครับ

รู้จัก Pattern แล้ว ต้องรู้จัก Anti-Pattern ด้วย [wikipedia]

ทั้งการที่สามารถปรับเปลี่ยนระบบอย่างไรก็ได้ การที่สามารพัฒนาระบบแยกกันได้ แถมยัง Unit Test ได้อย่างดี งั้นทุกคน เราจงทำให้ทุกอย่างเป็น IoC+DI ให้หมดเลย!!! อย่าเพิ่งใจร้อนครับ เราลองมาดูกันบ้างว่า การใช้ IoC นั้น อาจจะไปตกหลุม Anti-Pattern อะไรบ้าง

Anti-Pattern เป็นด้านตรงข้ามของ Pattern ครับ IoC นั้น ถือเป็น Pattern ในการพัฒนาระบบอย่างหนึ่ง ซึ่งเมื่อมี System Architect ที่พัฒนาระบบเรื่อยๆ พบว่าตัวเองทำแบบนี้แล้วดี เขาก็จะแต่งหนังสือขึ้นมาเล่มหนึ่ง อธิบายความดีงามของ Pattern นั้นให้เราฟัง (ผมชอบมากเลยนะครับ วิธีคิดแบบนี้ ทำให้เราได้รับรู้มุมมองกว้างดีครับ) แต่ว่าในการนำเอาไปประยุกต์ใช้ เราก็จะต้องพิจารณาองค์ประกอบหลายๆ อย่างประกอบกันด้วย ซึ่งก็จะมี System Architect อีกกลุ่มหนึ่ง ที่เขียนหนังสือประเภท Anti-Pattern เอาไว้ คือ Pattern ของการที่ทำแล้วมันแย่นั่นเอง

จากที่ผมลองอ่านๆ โค๊ดของชาวบ้านเขามา ผมเชื่อว่า IoC อาจจะพาคุณไปตก 2 หลุม Anti-Pattern นี้ได้ครับ

  • Inner-platform effect [wikipedia] ด้วยความที่ IoC นั้นยั่วยวนให้เราทำให้ทุกอย่างแยกชิ้นได้ เพื่อจะได้เปลี่ยนได้ (Customizable) ผมเชื่อว่าถึงจุดหนึ่ง มันจะ Customize ได้มากจนเกินไป ผมเคยพบระบบแบบนี้อยู่ระบบหนึ่งครับตอนที่ทำงานใหม่ๆ เป็นระบบที่สามารถถอดประกอบทุกอย่างแยกชิ้นได้ เพราะว่าตามความต้องการแล้ว ลูกค้าแต่ละเจ้าจะได้ระบบหน้าตาไม่เหมือนกัน หรือว่ามีความสามารถไม่เท่ากัน แล้วก็อาจจะสามารถอัพเดทความสามารถบางอย่างแยกชิ้นได้ด้วย แน่นอนว่า ถ้าไม่ใช้ IoC+DI นี่เลิกคิดไปได้เลยครับ แต่พอพัฒนาต่อไปเรื่อยๆ สุดท้ายระบบนี้ก็กลายร่างเป็น Visual Studio แบบลวกๆ สามารถเขียน Script ได้ สามารถเปิดดู Property ของแต่ละชิ้นที่ถูกโหลดขึ้นมาได้ เพราะว่าชิ้นส่วนแต่ละชิ้นถูกเรียกขึ้นมาแบบ Dynamic มากเกินไปจน Debug ไม่ได้ และกลายเป็นว่า งานส่วนหนึ่งคือต้อง Maintain ตัวโครงนี้ ให้เป็น Visual Studio ด้วย
  • Action at a distance [wikipedia] ผมพบว่าระบบที่เป็น IoC ส่วนใหญ่ มักจะชอบทำคลาสเป็น Data Class (มีแค่ Attribute) แล้วใช้ Service หรือก็คือคลาสอื่น ในการทำงานกับ Data Class พวกนี้ โดยความเห็นส่วนตัวแล้วมันดูขัดแย้งกับหลักการ OOP ตรงข้อ “Encapsulation” คือ การทำงานที่เกี่ยวข้องกับข้อมูล และตัวข้อมูล ควรจะอยู่ด้วยกัน ดังนั้น เวลาที่วางโครงสร้างระบบ ผมเชื่อว่าควรจะต้องให้แต่ละชิ้นงาน มีการคุยข้ามกันให้น้อยที่สุด ทำงานด้วยตัวเองได้ครับ

    ผมเคยได้รับโค๊ดที่รุ่นน้องคนหนึ่งส่งมาให้รีวิวว่าเขาเขียนดีไหม (แต่ว่าไม่ได้เป็นระบบของผมครับ) เป็นคลาสอ่านข้อมูลจากฐานข้อมูล โดยมีการ Inject แค่ Connection String มาให้ และน้องเขาต้องเป็นคนเขียนเปิด Connection และเขียน SQL Query และโค๊ดในการติดต่อฐานข้อมูลทั้งหมดเพื่ออ่านข้อมูลขึ้นมาเอง (ไม่ใช้ EF ด้วยนะ) และทำการ Map Field จากฐานข้อมูล เข้าไปที่ Data Class (Promotion) เองด้วย สรุปว่า น้องต้องรู้ด้วยว่า Field อะไร Map กับ Attribute อะไรในคลาสนั้น

    image
    ซึ่งมีหลายประเด็นที่ผมคิดว่าจะเป็นปัญหา ดังนี้ครับ
    • Attribute ของ Promotion ทุกอย่างต้องเปิดโล่งเป็น Public Get/Set หมด เพราะว่าจะต้องให้ PromotionDatabase เซ็ทค่าได้ [Object orgy] ถ้าในมุมมองของผมแล้ว ควรจะเปลี่ยนให้ Promotion นั้น มี Dependency กลับมาที่ PromotionDatabase และให้ชิ้นงานในการอ่านข้อมูลส่ง IDataReader ไปให้ Data Class นั้นแทน
    • โค๊ดในการเขียนติดต่อกับ Database เกิดความซ้ำซ้อน เพราะว่า Interface ที่กำหนดมานั้น กำหนดแค่ว่า ต้องการอ่านข้อมูลจากฐานข้อมูล และ Inject ให้มาแค่ Connection String ทางที่ดี ควรจะเปลี่ยน Pattern นี้ใหม่ ให้มีคลาสกลาง ที่ทำหน้าที่เปิด Connection และอ่านข้อมูลที่เดียว เพื่อจะได้ใช้ Pattern เดียวกันในการทำ แล้วให้ PromotionDatabase เป็น Dependency ของคลาสนั้นแทน ว่าเวลาจะอ่านข้อมูลโปรโมชั่นจะต้องอ่านอย่างไร ผมคิดว่าไม่น่าเป็นไปได้ที่น้องเขาจะเป็นคนแรกที่เขียนติดต่อฐานข้อมูลในระบบนี้นะ

      อีกทางที่ทำได้ และผมมองว่าดูง่ายกว่า คือให้คลาส PromotionDatabase สืบทอดจาก Base Class ที่ Generalize การติดต่อฐานข้อมูลไว้แล้ว ผมให้ Class Diagram เอาไว้นะครับ ลองนึกดูเล่นๆ ว่า จะ Implement คลาส BaseMsSqlQuery อย่างไร (เขียนแบบนี้เป็นมุขประจำของผมก่อนจะมี EF ครับ) 
       image

       

สรุปว่าอย่างไร?

เช่นเดียวกับทุก Design Pattern ที่เราเคยพบกันมา แต่ละอย่างเราก็ต้องเลือกใช้ให้เหมาะสม และประยุกต์ใช้ให้เหมาะกับงานครับ และที่สำคัญคือ เหมาะสมกับภาษาที่เราใช้ในการเขียนด้วย แต่ว่าเรื่องของ IoC และ DI ต้องมองโดยต้องแยกออกเป็น 2 ประเด็นคือ

  • IoC คือ สิ่งที่เราต้องการให้เกิดขึ้น ไม่ใช่ Design Pattern (Mark Fowler เรียกมันว่า “Phenomenon” )
  • DI เป็น Designer Pattern หรือ เครื่องมือ ที่สามารถทำให้เกิด IoC ได้

จุดอ่อนของ การทำ IoC ด้วย DI

  • จากประสบการณ์การใช้งาน NancyFX ผมพบว่า สิ่งที่เป็นปัญหาที่สุดก็คือ ผมไม่ทราบว่า ในระบบของ NancyFX นั้น มี Service อะไรให้เรียกใช้บ้าง โดยผมจะต้องแจ้งให้ Framework NancyFx ทราบ ผ่าน Constructor ของคลาสที่ผมสร้างขึ้น ซึ่งจะแตกต่างจาก Framework ของ Microsoft ซึ่งเป็น IoC แต่ว่าไม่ได้ใช้ DI ตรงที่ Framework ของ Microsoft มักจะมีคลาสยานแม่ ที่รวมทุก Service ที่เราต้องการเอาไว้ให้เป็นแบบ Static ด้วย เช่น File, Console หรือ HttpUtility เป็นต้น หรือว่า ใช้การ Override เอา ซึ่งเราก็จะรู้อยู่แล้วว่า อะไร Override ได้บ้างโดยการดูใน Visual Studio แต่ใน Nancy จะไม่มีคลาสแบบนี้อยู่ ต้องรู้เอาเองว่าอะไรบ้างที่ Inject เข้ามาได้บ้าง
  • IoC + DI ต้องอาศัยโปรแกรมเมอร์ที่มีความชำนาญพอสมควรในการทำความเข้าใจ และต้องมองออกว่า ระบบนี้ใช้การทำงานแบบ DI อยู่ ยกตัวอย่าง NancyFX อีกครั้งนะครับ ว่า จู่ๆ เราสร้าง Constructor ที่รับ Parameter เอาไว้ แล้วก็มีคนโยน Instance ของ IRootPathProvider มาให้ มาจากไหนก็ไม่รู้ ใครเป็นคนเรียกก็ไม่รู้อีกเหมือนกัน ถ้าเกิดว่าไม่ได้ศึกษาเรื่อง IoC+DI มาก่อน น่าจะงงงวยพอสมควรเลยทีเดียว และอาจจะไม่เข้าใจถึงการทำงานที่แท้จริงเบื้องหลังเลยก็ได้

แล้วถ้าเป็นผม จะเลือกแบบไหน?

ผมมีข้อสงสัยเพียงข้อเดียวคือ จำเป็นหรือไม่ ที่ภาษาที่มีทางเลือกอื่น ที่เข้าใจง่ายกว่าสำหรับโปรแกรมเมอร์ อย่างเช่น สามารถใช้ Static ได้ สามารถสืบทอดคลาสได้ จะต้องใช้ DI? ซึ่งทำให้เกิดความซับซ้อนในการทำความเข้าใจการทำงานของระบบ ผมรู้สึกว่า การพยายามทำ DI นั้น เป็นการไล่หนีข้อจำกัดของบางภาษา อย่างเช่น JavaScript ถ้าไม่มีเครื่องมือช่วยทำ DI เลย คงจะเขียนลำบากมาก เพราะว่าปัญหาเรื่อง Scope ซึ่งใน JavaScript ผมก็ใช้ความสามารถในการทำ DI ของ AngularJS ช่วยครับ

โดยสรุปก็คือ ผมเองมองว่า DI ไม่ใช่ทางเลือกที่ดีสำหรับภาษา C# เพราะว่ามีวิธีที่ง่ายกว่า และโปรแกรมเมอร์เข้าใจง่ายกว่าในการทำให้เกิด IoC ด้วยภาษานี้ เช่น การมีคลาสยานแม่แบบ Static หรือ Observer Pattern ครับ และเราก็ไม่ควรปิดกั้นความคิดว่า IoC จะต้องเป็น IoC+DI เท่านั้น ในภาษาที่คุณชื่นชอบ อาจจะทำให้เกิด IoC ได้ ด้วยความสามารถเฉพาะของภาษาที่คุณใช้ก็ได้

โพสยาวมาก จนตอนนี้ผมว่าถ้าเรียนโท วิชา Design Pattern น่าจะเป็น Literature Review ส่งอาจารย์ได้เลยทีเดียว เป็นการ Refresh ความเข้าใจ ทบทวนความรู้ รวมถึงปัดฝุ่นสกิลในการแต่งเอกสารของผมได้เป็นอย่างดีเลยทีเดียวครับ ขอบคุณทุกท่านที่อ่านมาจนถึงตรงนี้ ถ้ามีคำชี้แนะ หรือผิดพลาดตรงไหน ก็ขอเชิญใช้ Disqus ด้านล่างได้เลยครับ

References: