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"