9.1 การผกผันการพึ่งพา

โปรดจำไว้ว่า เราเคยกล่าวไว้ว่าในแอปพลิเคชันเซิร์ฟเวอร์ คุณไม่สามารถสร้างสตรีมผ่านnew Thread().start()? เฉพาะคอนเทนเนอร์เท่านั้นที่ควรสร้างเธรด ตอนนี้เราจะพัฒนาแนวคิดนี้ให้ดียิ่งขึ้นไปอีก

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

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

โมดูลไม่ควรมีการอ้างอิงถึงการใช้งานเฉพาะ และการพึ่งพาและการโต้ตอบระหว่างกันทั้งหมดควรสร้างขึ้นบนพื้นฐานของนามธรรมเท่านั้น (นั่นคือ อินเทอร์เฟซ) สาระสำคัญของกฎนี้สามารถเขียนได้ในวลีเดียว: การพึ่งพาทั้งหมดจะต้องอยู่ในรูปแบบของอินเทอร์เฟ

แม้จะมีลักษณะพื้นฐานและความเรียบง่ายที่ชัดเจน แต่กฎนี้มักถูกละเมิดมากที่สุด กล่าวคือ ทุกครั้งที่เราใช้ตัวดำเนินการใหม่ในโค้ดของโปรแกรม/โมดูล และสร้างวัตถุใหม่ประเภทเฉพาะ ดังนั้น แทนที่จะขึ้นอยู่กับอินเทอร์เฟซ การพึ่งพาการใช้งานจึงเกิดขึ้น

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

วิธีแก้ปัญหาที่ดีมากคือความคิดบ้าๆ บอๆ ของการมุ่งสร้างวัตถุใหม่ภายในวัตถุและโมดูลเฉพาะ - โรงงาน, ตัวระบุตำแหน่งบริการ, คอนเทนเนอร์ IoC

ในแง่หนึ่ง การตัดสินใจดังกล่าวเป็นไปตามหลักการทางเลือกเดียว ซึ่งกล่าวว่า: "เมื่อใดก็ตามที่ระบบซอฟต์แวร์ต้องรองรับทางเลือกมากมาย รายการทั้งหมดควรเป็นที่รู้จักในโมดูลเดียวของระบบเท่านั้น "

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

ตัวอย่างที่ 1

new ArrayList แทนที่จะ เขียนบางอย่างเช่น JDK จะให้List.new()การใช้งาน leaf ที่ถูกต้องแก่คุณ: ArrayList, LinkedList หรือแม้แต่ ConcurrentList

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

ตัวอย่างที่ 2

สิ่งนี้ได้เกิดขึ้นแล้วเช่น ครั้งสุดท้ายที่คุณเขียนอัลกอริทึมการเรียงลำดับเพื่อจัดเรียงคอลเลกชันคือเมื่อใด ตอนนี้ทุกคนใช้วิธีนี้แทนCollections.sort()และองค์ประกอบของคอลเลกชั่นต้องรองรับอินเทอร์เฟซที่เปรียบเทียบได้ (เปรียบเทียบได้)

หากsort()คุณส่งคอลเลกชันที่มีองค์ประกอบน้อยกว่า 10 รายการไปยังเมธอด ค่อนข้างเป็นไปได้ที่จะจัดเรียงด้วยการจัดเรียงแบบฟอง (Bubble sort) ไม่ใช่ Quicksort

ตัวอย่างที่ 3

คอมไพเลอร์กำลังเฝ้าดูวิธีการเชื่อมสตริงของคุณ และจะแทนที่โค้ดของคุณด้วยStringBuilder.append().

9.2 การผกผันการพึ่งพาในทางปฏิบัติ

ตอนนี้สิ่งที่น่าสนใจที่สุด: ลองคิดดูว่าเราจะรวมทฤษฎีและการปฏิบัติได้อย่างไร โมดูลสามารถสร้างและรับ "การพึ่งพา" ได้อย่างถูกต้องและไม่ละเมิดการพึ่งพาการผกผันได้อย่างไร

ในการทำเช่นนี้ เมื่อออกแบบโมดูล คุณต้องตัดสินใจด้วยตัวเอง:

  • โมดูลทำอะไร ทำหน้าที่อะไร
  • จากนั้นโมดูลต้องการจากสภาพแวดล้อมนั่นคือวัตถุ / โมดูลใดที่จะต้องจัดการ
  • แล้วเขาจะรับได้อย่างไร?

เพื่อให้เป็นไปตามหลักการของ Dependency Inversion คุณต้องตัดสินใจอย่างแน่ชัดว่าโมดูลของคุณใช้อ็อบเจ็กต์ภายนอกใด และจะได้รับการอ้างอิงถึงอ็อบเจ็กต์นั้นอย่างไร

และนี่คือตัวเลือกต่อไปนี้:

  • โมดูลสร้างวัตถุเอง
  • โมดูลรับวัตถุจากคอนเทนเนอร์
  • โมดูลไม่รู้ว่าวัตถุมาจากไหน

ปัญหาคือในการสร้างวัตถุคุณต้องเรียกตัวสร้างประเภทเฉพาะและด้วยเหตุนี้โมดูลจะไม่ขึ้นอยู่กับอินเทอร์เฟซ แต่ขึ้นอยู่กับการใช้งานเฉพาะ แต่ถ้าเราไม่ต้องการให้สร้างอ็อบเจกต์อย่างชัดเจนในโค้ดโมดูล เราสามารถใช้รูปแบบ Factory Method ได้

"สิ่งที่สำคัญที่สุดคือ แทนที่จะสร้างอินสแตนซ์อ็อบเจกต์โดยตรงผ่าน new เราจัดเตรียมอินเทอร์เฟซสำหรับคลาสไคลเอนต์เพื่อสร้างออบเจกต์ เนื่องจากอินเทอร์เฟซดังกล่าวสามารถถูกแทนที่ด้วยการออกแบบที่เหมาะสมได้เสมอ เราจึงมีความยืดหยุ่นเมื่อใช้โมดูลระดับต่ำ ในโมดูลระดับสูง" .

ในกรณีที่จำเป็นต้องสร้าง กลุ่มหรือตระกูลของวัตถุที่เกี่ยวข้อง โรงงานแบบนามธรรมจะถูกใช้แทนวิธีการ แบบโรงงาน

9.3 การใช้ตัวระบุตำแหน่งบริการ

โมดูลนำวัตถุที่จำเป็นจากวัตถุที่มีอยู่แล้ว สันนิษฐานว่าระบบมีที่เก็บอ็อบเจ็กต์บางส่วน ซึ่งโมดูลสามารถ "วาง" อ็อบเจ็กต์ของตนและ "รับ" อ็อบเจ็กต์จากที่เก็บได้

แนวทางนี้ดำเนินการโดยรูปแบบ Service Locator แนวคิดหลักคือโปรแกรมมีวัตถุที่รู้วิธีรับการอ้างอิง (บริการ) ทั้งหมดที่อาจจำเป็น

ข้อแตกต่างหลักจากโรงงานคือ Service Locator ไม่ได้สร้างออบเจกต์ แต่จริงๆ แล้วมีออบเจกต์ที่สร้างอินสแตนซ์อยู่แล้ว (หรือรู้ว่าจะรับได้ที่ไหน / อย่างไร และหากสร้าง ก็จะมีเพียงครั้งเดียวในการเรียกครั้งแรก) โรงงานในการโทรแต่ละครั้งจะสร้างวัตถุใหม่ที่คุณเป็นเจ้าของโดยสมบูรณ์ และคุณสามารถทำอะไรก็ได้ที่คุณต้องการกับมัน

สำคัญ ! ตัวระบุตำแหน่งบริการสร้างการอ้างอิงไปยังวัตถุเดียวกันที่มีอยู่แล้ว ดังนั้น คุณต้องระวังให้มากกับวัตถุที่ออกโดย Service Locator เนื่องจากอาจมีคนอื่นใช้วัตถุเหล่านั้นในเวลาเดียวกันกับคุณ

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

Service Locator บางครั้งเรียกว่า anti-pattern และเราไม่สนับสนุน (เพราะสร้างการเชื่อมต่อโดยนัยและให้รูปลักษณ์ของการออกแบบที่ดีเท่านั้น) คุณสามารถอ่านเพิ่มเติมจาก Mark Seaman:

9.4 การฉีดพึ่งพา

โมดูลไม่สนใจเกี่ยวกับการพึ่งพา "การขุด" เลย มันจะกำหนดเฉพาะสิ่งที่ต้องการทำงานเท่านั้น และการพึ่งพาที่จำเป็นทั้งหมดจะจัดหา (แนะนำ) จากภายนอกโดยบุคคลอื่น

นี่คือสิ่งที่เรียกว่า - การฉีดการพึ่งพา โดยทั่วไป การขึ้นต่อกันที่จำเป็นจะถูกส่งผ่านเป็นพารามิเตอร์คอนสตรัคเตอร์ (Constructor Injection) หรือผ่านเมธอดคลาส (Setter injection)

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

การเปลี่ยนแปลงทิศทางนี้เรียกว่าInversion of Controlหรือหลักการของฮอลลีวูด - "อย่าโทรหาเรา เราจะโทรหาคุณ"

นี่เป็น โซลูชันที่ ยืดหยุ่นที่สุด ทำให้โมดูลมีความเป็นอิสระมากที่สุด เราสามารถพูดได้ว่ามีเพียงการดำเนินการตาม "หลักการความรับผิดชอบเดี่ยว" อย่างครบถ้วน - โมดูลควรมุ่งเน้นไปที่การทำงานให้ดีและไม่ต้องกังวลกับสิ่งอื่นใด

การจัดหาทุกสิ่งที่จำเป็นสำหรับการทำงานให้กับโมดูลเป็นงานแยกต่างหาก ซึ่งควรจัดการโดย "ผู้เชี่ยวชาญ" ที่เหมาะสม (โดยปกติจะเป็นคอนเทนเนอร์หนึ่งๆ คอนเทนเนอร์ IoC มีหน้าที่จัดการการพึ่งพาและการนำไปใช้งาน)

ในความเป็นจริง ทุกสิ่งที่นี่เป็นเหมือนชีวิต: ในบริษัทที่มีการจัดการที่ดี โปรแกรมของโปรแกรมเมอร์ โต๊ะทำงาน คอมพิวเตอร์ และทุกสิ่งที่จำเป็นสำหรับการทำงานถูกซื้อและจัดหาโดยผู้จัดการสำนักงาน หรือถ้าคุณใช้คำเปรียบเปรยของโปรแกรมเป็นตัวสร้าง โมดูลไม่ควรคิดเกี่ยวกับสาย คนอื่นมีส่วนร่วมในการประกอบตัวสร้าง ไม่ใช่ชิ้นส่วน

คงไม่ใช่เรื่องเกินจริงที่จะกล่าวว่าการใช้อินเทอร์เฟซเพื่ออธิบายการพึ่งพาระหว่างโมดูล (Dependency Inversion) + การสร้างที่ถูกต้องและการฉีดการพึ่งพาเหล่านี้ (การพึ่งพาอาศัยกันเป็นหลัก) เป็นเทคนิคสำคัญสำหรับการแยกส่วน

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

หลักการของการผกผันของการควบคุม (ร่วมกับการพึ่งพาการฉีดและตัวระบุตำแหน่งบริการ) ได้รับการกล่าวถึงโดยละเอียดโดย Martin Fowler มีการแปลบทความทั้งสองของเขา: "Inversion of Control Containers and the Dependency Injection pattern"และ"Inversion of Control"