ฉีดพึ่งพา; แนวทางปฏิบัติที่ดีในการลดรหัสสำเร็จรูป


25

ฉันมีคำถามง่าย ๆ และฉันก็ไม่แน่ใจด้วยซ้ำว่ามีคำตอบลองทำดู ฉันกำลังเขียนโปรแกรมใน C ++ และใช้การฉีดพึ่งพาเพื่อหลีกเลี่ยงสถานะทั่วโลก มันใช้งานได้ค่อนข้างดีและฉันไม่ได้ทำงานในพฤติกรรมที่ไม่คาดคิด / ไม่ได้กำหนดบ่อยมาก

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

ไม่มีอะไรที่จะเป็นตัวอย่างที่ดีได้ดังนั้นลองทำกัน:

ฉันมีคลาสที่ชื่อว่า TimeFactory ซึ่งสร้าง Time object

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

ในแอพของฉันมีคลาสจำนวนมากที่ต้องสร้างวัตถุเวลา บางครั้งคลาสเหล่านั้นซ้อนกันอย่างล้ำลึก

สมมติว่าฉันมีคลาส A ซึ่งมีอินสแตนซ์ของคลาส B และอื่น ๆ จนถึงคลาส D คลาส D จำเป็นต้องสร้างวัตถุเวลา

ในการปฏิบัติที่ไร้เดียงสาของฉันฉันส่ง TimeFactory ไปยังตัวสร้างของคลาส A ซึ่งส่งผ่านไปยังตัวสร้างของคลาส B และต่อไปจนถึงคลาส D

ตอนนี้ลองนึกภาพว่าฉันมีคลาสสองชั้นเช่น TimeFactory และลำดับชั้นของชั้นสองอย่างที่กล่าวมาข้างต้น: ฉันสูญเสียความยืดหยุ่นและความสามารถในการอ่านได้ทั้งหมด

ฉันเริ่มสงสัยว่าไม่มีข้อบกพร่องด้านการออกแบบที่สำคัญในแอปของฉัน ... หรือนี่เป็นความชั่วร้ายที่จำเป็นในการใช้การฉีดแบบพึ่งพา

คุณคิดอย่างไร ?


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

2
การเขียนโปรแกรมที่มุ่งเน้นด้านยังเป็นสิ่งที่ดีสำหรับงานนี้ - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov

คลาส A เป็นโรงงานสำหรับคลาส B, C และ D หรือไม่ หากไม่ใช่เพราะเหตุใดจึงสร้างอินสแตนซ์ของพวกเขา หากคลาส D ต้องการวัตถุเวลาทำไมคุณจะเพิ่มคลาสอื่นด้วย TimeFactory คลาส D ต้องใช้ TimeFactory จริง ๆ หรืออาจเป็นเพียงตัวอย่างของเวลาที่ถูกแทรกเข้าไปหรือไม่
simoraman

D จำเป็นต้องสร้างวัตถุ Time โดยมีเฉพาะข้อมูล D เท่านั้นที่ควรมี ฉันต้องคิดถึงความคิดเห็นที่เหลือของคุณ อาจมีปัญหาใน "ผู้สร้างใคร" ในรหัสของฉัน
Dinaiz

"ใครกำลังสร้างใคร" ถูกแก้ไขโดยคอนเทนเนอร์ IoC ดูคำตอบของฉันสำหรับรายละเอียด .. "ใครกำลังสร้างใคร" เป็นหนึ่งในคำถามที่ดีที่สุดที่คุณสามารถถามเมื่อใช้การฉีดพึ่งพา
GameDeveloper

คำตอบ:


23

ในแอพของฉันมีคลาสจำนวนมากที่ต้องสร้างวัตถุเวลา

ดูเหมือนว่าTimeคลาสของคุณเป็นประเภทข้อมูลพื้นฐานที่ควรเป็นของ "โครงสร้างพื้นฐานทั่วไป" ของแอปพลิเคชันของคุณ DI ใช้งานไม่ได้กับคลาสดังกล่าว คิดว่ามันหมายถึงอะไรถ้าคลาสอย่างที่stringต้องถูกแทรกลงในส่วนต่าง ๆ ของรหัสที่ใช้สตริงและคุณจะต้องใช้stringFactoryเป็นโอกาสเดียวในการสร้างสายใหม่ - ความสามารถในการอ่านของโปรแกรมของคุณจะลดลงตามลำดับ ขนาด.

ดังนั้นคำแนะนำของฉัน: ไม่ได้ใช้ DI Timeสำหรับประเภทข้อมูลทั่วไปเช่น เขียนการทดสอบหน่วยสำหรับTimeชั้นเรียนและเมื่อเสร็จแล้วให้ใช้มันทุกที่ในโปรแกรมของคุณเช่นเดียวกับstringคลาสหรือvectorคลาสหรือคลาสอื่น ๆ ของ lib มาตรฐาน ใช้ DI สำหรับส่วนประกอบที่ควรแยกออกจากกัน


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

3
@Dinaiz: ความคิดที่จะยอมรับการมีเพศสัมพันธ์แน่นระหว่างTimeและส่วนอื่น ๆ ของโปรแกรมของคุณ TimeFactoryเพื่อให้คุณสามารถที่จะยอมรับยังยอมรับการมีเพศสัมพันธ์แน่น อย่างไรก็ตามสิ่งที่ฉันจะหลีกเลี่ยงคือการมีTimeFactoryออบเจ็กต์โกลบอลเดี่ยวที่มีสถานะ (ตัวอย่างเช่นข้อมูลโลแคลหรืออะไรทำนองนั้น) - ที่อาจทำให้เกิดผลข้างเคียงที่น่ารังเกียจให้กับโปรแกรมของคุณและทำการทดสอบทั่วไป ทั้งทำให้มันไร้สัญชาติหรือไม่ใช้มันเป็นซิงเกิล
Doc Brown

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

1
@Dinaiz: สุจริตขึ้นอยู่กับชนิดของรัฐกับชนิดและจำนวนชิ้นส่วนของโปรแกรมที่ใช้TimeและTimeFactoryระดับ "วิวัฒนาการ" ที่คุณต้องการสำหรับการขยายในอนาคตที่เกี่ยวข้องTimeFactoryและอื่น ๆ
Doc Brown

ตกลงฉันจะพยายามรักษาความสัมพันธ์แบบพึ่งพา แต่มีการออกแบบที่ชาญฉลาดกว่าซึ่งไม่ต้องใช้หม้อไอน้ำมากนัก! ขอบคุณมาก doc '
Dinaiz

4

คุณหมายถึงอะไรโดย "ฉันสูญเสียความยืดหยุ่นและความสามารถในการอ่านที่ฉันคาดว่าจะได้รับจากการใช้การพึ่งพา" - DI ไม่เกี่ยวกับความสามารถในการอ่าน มันเกี่ยวกับการแยกการพึ่งพาระหว่างวัตถุ

ดูเหมือนว่าคุณมีคลาส A ที่สร้างคลาส B, คลาส B ที่สร้างคลาส C และคลาส C ที่สร้างคลาส D

สิ่งที่คุณควรมีคือ Class B ฉีดเข้าไปใน Class A Class C ฉีดเข้าไปใน Class B Class D ฉีดเข้าไปใน Class C


DI อาจเกี่ยวกับความสามารถในการอ่าน หากคุณมีตัวแปรทั่วโลก "อย่างน่าอัศจรรย์" ปรากฏขึ้นตรงกลางของวิธีการมันไม่ช่วยให้เข้าใจรหัส (จากมุมมองการบำรุงรักษา) ตัวแปรนี้มาจากไหน? ใครเป็นเจ้าของมัน ใครกำหนดค่าเริ่มต้น และอื่น ๆ ... สำหรับจุดที่สองของคุณคุณอาจจะถูก แต่ฉันไม่คิดว่ามันจะใช้ในกรณีของฉัน ขอบคุณที่สละเวลาในการตอบ
Dinaiz

DI เพิ่มความสามารถในการอ่านเป็นส่วนใหญ่เพราะ "ใหม่ / ลบ" จะถูกดึงออกมาอย่างน่าอัศจรรย์จากรหัสของคุณ หากคุณไม่ต้องกังวลเกี่ยวกับรหัสการจัดสรรแบบไดนามิกอ่านง่ายกว่า
GameDeveloper

อย่าลืมที่จะบอกว่าสิ่งนี้ทำให้โค้ดมีความแข็งแกร่งขึ้นเล็กน้อยเนื่องจากไม่สามารถตั้งสมมติฐานเกี่ยวกับ "วิธี" การสร้างการพึ่งพาได้อีกต่อไปแล้วมัน "เพียงแค่ต้องการมัน" และนั่นทำให้ชั้นง่ายต่อการรักษา .. ดูคำตอบของฉัน
GameDeveloper

3

ฉันไม่แน่ใจว่าทำไมคุณไม่ต้องการทำให้เวลาของคุณเป็นแบบซิงเกิล หากมีเพียงอินสแตนซ์เดียวในแอปทั้งหมดของคุณมันเป็นความจริงเพียงอย่างเดียว

ที่ถูกกล่าวว่ามันเป็นอันตรายมากที่จะแบ่งปันวัตถุที่ไม่แน่นอนยกเว้นถ้ามันได้รับการปกป้องอย่างถูกต้องโดยประสานบล็อกในกรณีนี้ไม่มีเหตุผลที่จะไม่เป็นคนเดียว

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


Spring ใช้สำหรับ java และฉันใช้ C ++ แต่ทำไมคน 2 คนถึงลงคะแนนคุณ? มีจุดในการโพสต์ของคุณผมไม่เห็นด้วยกับ ... แต่ยังคงมี
Dinaiz

ฉันจะถือว่า downvote เกิดจากการไม่ตอบคำถามต่อ se อาจจะรวมถึงการพูดถึง Spring อย่างไรก็ตามข้อเสนอแนะของการใช้ซิงเกิลตันนั้นเป็นสิ่งที่ถูกต้องในใจของฉันและอาจไม่ตอบคำถาม - แต่แนะนำทางเลือกที่ถูกต้องและทำให้เกิดปัญหาการออกแบบที่น่าสนใจ ด้วยเหตุนี้ฉันจึง +1
เฟอร์กัสในลอนดอน

1

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

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

เดี่ยว ไม่เป็นไรขอบคุณ. หากคุณต้องการเพียง istance เพียงตัวเดียวให้แบ่งใช้กับบริบทแอปพลิเคชันของคุณ (การใช้คอนเทนเนอร์ IoC สำหรับ DI เช่น Infector ++ ต้องการเพียงแค่ผูก TimeFactory เป็น istance เดียว) นี่คือตัวอย่าง (C ++ 11 ตามวิธี แต่อาจเป็น C ++ ถึง C ++ 11 แล้วหรือยังคุณได้รับแอปพลิเคชันแบบไม่รั่วไหลฟรี):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

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

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

ตามที่คุณเห็นคุณฉีด TimeFactory เพียงครั้งเดียว วิธีใช้ "A" ง่ายมากทุกคลาสจะถูกฉีดสร้างขึ้นในโรงงานหลักหรือเป็นโรงงาน

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

ทุกครั้งที่คุณสร้างคลาส A มันจะถูกฉีดโดยอัตโนมัติ (สันหลังยาวขี้เกียจ) ที่มีการพึ่งพาทั้งหมดจนถึง D และ D จะถูกฉีดด้วย TimeFactory ดังนั้นโดยการเรียกใช้เพียง 1 วิธีเท่านั้นที่คุณมีลำดับชั้นที่สมบูรณ์ของคุณ (และแม้กระทั่งลำดับชั้นที่ซับซ้อน การลบรหัสแผ่นบอยเลอร์จำนวนมาก): คุณไม่ต้องเรียก "ใหม่ / ลบ" และนั่นสำคัญมากเพราะคุณสามารถแยกตรรกะการใช้งานออกจากรหัสกาวได้

D สามารถสร้างวัตถุเวลาด้วยข้อมูลที่มีเพียง D

นั่นเป็นเรื่องง่าย TimeFactory ของคุณมีวิธี "สร้าง" จากนั้นใช้ลายเซ็นที่แตกต่าง "สร้าง (params)" และทำเสร็จแล้ว พารามิเตอร์ที่ไม่มีการขึ้นต่อกันมักจะได้รับการแก้ไขด้วยวิธีนี้ นอกจากนี้ยังลบหน้าที่ในการฉีดสิ่งต่าง ๆ เช่น "สาย" หรือ "จำนวนเต็ม" เพราะเพียงแค่เพิ่มจานหม้อไอน้ำพิเศษ

ใครสร้างใคร คอนเทนเนอร์ IoC สร้าง istances และโรงงานโรงงานสร้างส่วนที่เหลือ (โรงงานสามารถสร้างวัตถุต่าง ๆ ด้วยพารามิเตอร์ตามอำเภอใจดังนั้นคุณไม่จำเป็นต้องมีสถานะสำหรับโรงงาน) คุณยังสามารถใช้โรงงานเป็นเครื่องห่อหุ้มสำหรับคอนเทนเนอร์ IoC: โดยทั่วไปแล้วการพูด Injectin คอนเทนเนอร์ IoC นั้นแย่มากและเหมือนกับการใช้ตัวระบุบริการ บางคนแก้ไขปัญหาด้วยการห่อ IoC Container กับโรงงาน (ไม่จำเป็นอย่างเคร่งครัด แต่มีข้อได้เปรียบที่ลำดับชั้นได้รับการแก้ไขโดย Container และโรงงานทั้งหมดของคุณจะง่ายต่อการบำรุงรักษา)

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

ยังไม่ละเมิดการฉีดพึ่งพาประเภทง่าย ๆ สามารถเป็นสมาชิกของชั้นเรียนหรือตัวแปรที่กำหนดขอบเขตท้องถิ่น นี่ดูเหมือนชัดเจน แต่ฉันเห็นคนฉีด "std :: vector" เพียงเพราะมีกรอบ DI ที่อนุญาต โปรดจำไว้เสมอกฎของ Demeter: "ฉีดเฉพาะสิ่งที่คุณต้องการฉีดเท่านั้น"


ว้าวฉันไม่เห็นคำตอบของคุณมาก่อนขอโทษ! ท่านที่ให้ข้อมูลมาก! upvoted
Dinaiz

0

คลาส A, B และ C ของคุณจำเป็นต้องสร้างอินสแตนซ์เวลาหรือคลาส D เท่านั้นหรือไม่ หากเป็นคลาส D เท่านั้น A และ B ไม่ควรรู้อะไรเลยเกี่ยวกับ TimeFactory สร้างอินสแตนซ์ของ TimeFactory ภายในคลาส C และส่งผ่านไปยังคลาส D โปรดทราบว่าด้วยการ "สร้างอินสแตนซ์" ฉันไม่จำเป็นต้องหมายความว่าคลาส C ต้องรับผิดชอบในการสร้างอินสแตนซ์ของ TimeFactory สามารถรับ DClassFactory จากคลาส B และ DClassFactory รู้วิธีสร้างอินสแตนซ์เวลา

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


-1

ฉันได้ติดตั้งเฟรมเวิร์กการฉีดพึ่งพา C ++ อีกตัวหนึ่งซึ่งเพิ่งเสนอให้เพิ่ม - https://github.com/krzysztof-jusiak/di - ห้องสมุดมีมาโครน้อยลง (ฟรี) ส่วนหัวเท่านั้น C ++ 03 / C ++ 11 / C ++ 14 ไลบรารี่ที่ให้ประเภทปลอดภัยเวลาในการคอมไพล์การสร้างคอนสตรัคแบบพึ่งพาแมโคร


3
การตอบคำถามเก่า ๆ ใน P.SE เป็นวิธีที่ไม่ดีในการโฆษณาโครงการของคุณ พิจารณาว่ามีโครงการหลายหมื่นโครงการที่อาจเกี่ยวข้องกับคำถาม หากผู้เขียนทั้งหมดโพสต์ที่นี่คุณภาพการตอบของเว็บไซต์จะลดลง
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.