เหตุใดจึงใช้การฉีดแบบพึ่งพา


536

ฉันพยายามที่จะเข้าใจการฉีดพึ่งพา (DI) และฉันล้มเหลวอีกครั้ง ดูเหมือนว่าโง่ รหัสของฉันไม่เป็นระเบียบ ฉันแทบจะไม่เขียนฟังก์ชั่นและอินเทอร์เฟซเสมือน (แม้ว่าฉันจะทำครั้งเดียวใน Blue Moon) และการกำหนดค่าทั้งหมดของฉันถูกจัดลำดับอย่างน่าอัศจรรย์ในชั้นเรียนโดยใช้ json.net (บางครั้งใช้ XML serializer)

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

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


3
นี่อาจเป็นข้อมูลที่เป็นประโยชน์: martinfowler.com/articles/inject.html
ta.speot.is


1
ดูเพิ่มเติม
BlueRaja - Danny Pflughoeft

41
ฉันกับผู้ชายคนนี้: jamesshore.com/Blog/Dependency-Injection-Demystified.html
Eric Lippert

5
คำอธิบายง่ายๆอีกอย่างของ DI: codearsenal.net/2015/03/…
ybonda

คำตอบ:


840

ก่อนอื่นฉันต้องการอธิบายข้อสันนิษฐานที่ฉันทำกับคำตอบนี้ ไม่เป็นความจริงเสมอไป แต่มักจะ:

การเชื่อมต่อเป็นคำคุณศัพท์; ชั้นเรียนเป็นคำนาม

(อันที่จริงมีอินเทอร์เฟซที่เป็นคำนามเช่นกัน แต่ฉันต้องการพูดคุยที่นี่)

ดังนั้นเช่นอินเตอร์เฟซที่อาจจะเป็นสิ่งเช่นIDisposable, หรือIEnumerable IPrintableชั้นคือการดำเนินการที่เกิดขึ้นจริงของหนึ่งหรือมากกว่าของการเชื่อมต่อเหล่านี้: Listหรือทั้งอาจจะมีการใช้งานของMapIEnumerable

ในการรับประเด็น: บ่อยครั้งที่คลาสของคุณต้องพึ่งพาซึ่งกันและกัน เช่นคุณอาจมีDatabaseคลาสที่เข้าถึงฐานข้อมูลของคุณ (hah, surprise! ;-)) แต่คุณต้องการให้คลาสนี้ทำการบันทึกเกี่ยวกับการเข้าถึงฐานข้อมูล สมมติว่าคุณมีชั้นอื่นLoggerแล้วมีการพึ่งพาไปDatabaseLogger

จนถึงตอนนี้ดีมาก

คุณสามารถสร้างแบบจำลองการอ้างอิงนี้ภายในDatabaseชั้นเรียนของคุณด้วยบรรทัดต่อไปนี้:

var logger = new Logger();

และทุกอย่างดี เป็นเรื่องที่ดีเมื่อคุณรู้ว่าคุณต้องมีตัวบันทึกจำนวนมาก: บางครั้งคุณต้องการเข้าสู่คอนโซลบางครั้งไปยังระบบไฟล์บางครั้งใช้ TCP / IP และเซิร์ฟเวอร์การบันทึกระยะไกลและอื่น ๆ ...

และแน่นอนคุณไม่ต้องการเปลี่ยนรหัสทั้งหมดของคุณ (ในขณะที่คุณมี gazillions อยู่) และแทนที่ทุกบรรทัด

var logger = new Logger();

โดย:

var logger = new TcpLogger();

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

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

ICanLog logger = new Logger();

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

ICanLog logger = LoggerFactory.Create();

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

แน่นอนว่าคุณสามารถทำให้โรงงานนี้เป็นมาตรฐานและทำให้ใช้ได้กับทุกประเภท:

ICanLog logger = TypeFactory.Create<ICanLog>();

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

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

และตอนนี้เมื่อคุณเปลี่ยนชื่อนิดหน่อยคุณก็จบลงด้วยการใช้งานตัวระบุบริการอย่างง่ายซึ่งเป็นหนึ่งในสองรูปแบบสำหรับInversion of Control (เนื่องจากคุณไม่สามารถควบคุมได้ว่าใครเป็นผู้ตัดสินใจว่าจะเรียนอินสแตนซ์อะไร)

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

ตอนนี้การฉีดการพึ่งพาเป็นขั้นตอนต่อไปในบรรทัดนี้: เพียงแค่กำจัดการพึ่งพาตัวเดียวไปยัง locator service: แทนคลาสต่างๆที่ขอให้บริการ locator สำหรับการนำไปใช้งานสำหรับ interface เฉพาะคุณ - อีกครั้ง .

ด้วยการฉีดพึ่งพาDatabaseชั้นเรียนของคุณตอนนี้มีนวกรรมิกที่ต้องใช้พารามิเตอร์ประเภทICanLog:

public Database(ICanLog logger) { ... }

ตอนนี้ฐานข้อมูลของคุณมีตัวบันทึกที่จะใช้เสมอ แต่ก็ไม่ทราบว่าตัวบันทึกนี้มาจากไหน

และนี่คือที่ที่เฟรมเวิร์ก DI เข้ามาเล่น: คุณกำหนดค่าการแม็พของคุณอีกครั้งจากนั้นขอให้เฟรมเวิร์ก DI ของคุณอินสแตนซ์แอปพลิเคชันของคุณให้คุณ เนื่องจากApplicationคลาสต้องการICanPersistDataการนำไปใช้งานอินสแตนซ์ของDatabaseจะถูกฉีด - แต่จะต้องสร้างอินสแตนซ์ของชนิดของตัวบันทึกที่ถูกกำหนดค่าICanLogก่อน และอื่น ๆ ...

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

ในทางปฏิบัติมีบางสิ่งที่คุณไม่สามารถทำได้โดยไม่มีตัวระบุตำแหน่งบริการ (เช่นหากคุณไม่ทราบล่วงหน้าว่ามีอินสแตนซ์ที่คุณต้องการอินเทอร์เฟซเฉพาะจำนวนเท่าไร: กรอบงาน DI จะแทรกหนึ่งอินสแตนซ์ต่อพารามิเตอร์หนึ่งตัวเท่านั้น เซอร์วิส locator ภายในลูป, แน่นอน), ดังนั้นส่วนใหญ่เฟรมเวิร์ก DI แต่ละตัวจึงจัดเตรียม service locator

แต่โดยทั่วไปนั่นแหละ

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


7
แน่นอนคุณสามารถทำได้ด้วยวิธีนี้เช่นกัน แต่จากนั้นคุณต้องใช้ตรรกะนี้ในทุก ๆ ชั้นซึ่งจะให้การสนับสนุนสำหรับการแลกเปลี่ยนการใช้งาน นี่หมายถึงรหัสที่ซ้ำซ้อนและซ้ำซ้อนจำนวนมากและนี่ก็หมายความว่าคุณต้องแตะคลาสที่มีอยู่แล้วเขียนใหม่บางส่วนเมื่อคุณตัดสินใจว่าคุณต้องการมันในตอนนี้ DI อนุญาตให้คุณใช้สิ่งนี้กับคลาสใด ๆ คุณไม่จำเป็นต้องเขียนมันในลักษณะพิเศษ (ยกเว้นการกำหนดการขึ้นต่อกันเป็นพารามิเตอร์ใน Constructor)
Golo Roden

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

17
@GoloRoden ทำไมคุณถึงเรียกส่วนต่อประสาน ICanLog แทน ILogger ฉันทำงานกับโปรแกรมเมอร์อีกคนที่ทำสิ่งนี้บ่อยครั้งและฉันไม่สามารถเข้าใจอนุสัญญานี้ได้? ให้ฉันมันเหมือนการโทร IEanumerable ICanEnumerate?
DermFrench

28
ฉันเรียกมันว่า ICanLog เพราะเราทำงานบ่อยเกินไปกับคำ (คำนาม) ที่ไม่มีความหมายอะไรเลย ตัวอย่างโบรคเกอร์คืออะไร ผู้จัดการ? แม้แต่ Repository ก็ไม่ได้ถูกกำหนดในลักษณะที่เป็นเอกลักษณ์ และการมีทุกสิ่งเหล่านี้เป็นคำนามเป็นโรคทั่วไปของภาษา OO (ดูsteve-yegge.blogspot.de/2006/03/… ) สิ่งที่ฉันต้องการแสดงคือฉันมีส่วนประกอบที่สามารถทำการบันทึกสำหรับฉัน - ดังนั้นทำไมไม่เรียกมันอย่างนั้น? แน่นอนว่าสิ่งนี้กำลังเล่นกับ I ในฐานะบุคคลแรกดังนั้น ICanLog (ForYou)
Golo Roden

18
การทดสอบ @David Unit ใช้งานได้ดี - หลังจากทั้งหมดหน่วยจะไม่ขึ้นกับสิ่งอื่นใด (มิฉะนั้นจะไม่ใช่หน่วย) สิ่งที่ใช้ไม่ได้หากไม่มีคอนเทนเนอร์ DI คือการทดสอบการเยาะเย้ย ยุติธรรมพอฉันไม่แน่ใจว่าประโยชน์ของการเยาะเย้ยเมื่อเทียบกับความซับซ้อนที่เพิ่มขึ้นของการเพิ่มตู้คอนเทนเนอร์ DI ในทุกกรณี ฉันทำการทดสอบหน่วยที่เข้มงวด ฉันไม่ค่อยล้อเลียน
Konrad Rudolph

499

ฉันคิดว่าหลายครั้งที่ผู้คนสับสนเกี่ยวกับความแตกต่างระหว่างการฉีดแบบพึ่งพาและกรอบการฉีดแบบพึ่งพา(หรือคอนเทนเนอร์ที่มักถูกเรียกว่า)

การฉีดพึ่งพาเป็นแนวคิดที่ง่ายมาก แทนรหัสนี้:

public class A {
  private B b;

  public A() {
    this.b = new B(); // A *depends on* B
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  A a = new A();
  a.DoSomeStuff();
}

คุณเขียนโค้ดแบบนี้:

public class A {
  private B b;

  public A(B b) { // A now takes its dependencies as arguments
    this.b = b; // look ma, no "new"!
  }

  public void DoSomeStuff() {
    // Do something with B here
  }
}

public static void Main(string[] args) {
  B b = new B(); // B is constructed here instead
  A a = new A(b);
  a.DoSomeStuff();
}

และนั่นคือมัน อย่างจริงจัง. สิ่งนี้ทำให้คุณมีข้อได้เปรียบมากมาย สองสิ่งที่สำคัญคือความสามารถในการควบคุมการทำงานจากศูนย์กลาง ( Main()ฟังก์ชั่น) แทนที่จะกระจายไปทั่วโปรแกรมของคุณและความสามารถในการทดสอบแต่ละชั้นแยกได้ง่ายขึ้น (เพราะคุณสามารถส่ง mocks หรือวัตถุปลอมอื่น ๆ เข้าไปในคอนสตรัคท์แทน) ของมูลค่าที่แท้จริง)

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


7
ทำไมฉันถึงต้องการรหัสที่สองมากกว่ารหัสแรก? คำแรกมีคำหลักใหม่เท่านั้นสิ่งนี้จะช่วยได้อย่างไร
user962206

17
@ user962206 คิดเกี่ยวกับวิธีที่คุณจะทดสอบ A แยกต่างหากจาก B
jk

66
@ user962206 เช่นกันคิดว่าจะเกิดอะไรขึ้นถ้า B ต้องการพารามิเตอร์บางอย่างใน Constructor ของมันเพื่อที่จะยกตัวอย่างมัน A จะต้องรู้เกี่ยวกับพารามิเตอร์เหล่านั้นสิ่งที่อาจไม่เกี่ยวข้องกับ A (มันแค่ต้องการพึ่ง B ไม่ใช่สิ่งที่ขึ้นอยู่กับ B) การส่งผ่านที่สร้างแล้ว B (หรือ subclass ใด ๆ หรือเยาะเย้ยของ B สำหรับเรื่องที่) เพื่อแก้ของสร้างที่และทำให้ขึ้นอยู่เฉพาะใน B :)
epidemian

17
@ acidzombie24: เช่นเดียวกับรูปแบบการออกแบบมากมาย DI ไม่มีประโยชน์จริง ๆ เว้นแต่ว่าโค้ดเบสของคุณใหญ่พอสำหรับวิธีการง่าย ๆ ที่จะเป็นปัญหา ความรู้สึกของฉันคือ DI จะไม่ได้รับการปรับปรุงจนกว่าใบสมัครของคุณจะมีโค้ดมากกว่า 20,000 บรรทัดและ / หรือมากกว่า 20 รายการขึ้นอยู่กับไลบรารีหรือกรอบงานอื่น ๆ หากแอปพลิเคชันของคุณมีขนาดเล็กกว่านั้นคุณอาจยังคงต้องการเขียนโปรแกรมในรูปแบบ DI แต่ความแตกต่างจะไม่ใกล้เคียงอย่างน่าทึ่ง
Daniel Pryden

2
@DanielPryden ฉันไม่คิดว่าขนาดของรหัสจะสำคัญเท่ากับรหัสของคุณแบบไดนามิก หากคุณเพิ่มโมดูลใหม่ที่เหมาะสมกับอินเทอร์เฟซเดียวกันเป็นประจำคุณไม่จำเป็นต้องเปลี่ยนรหัสที่ต้องพึ่งพาบ่อยๆ
FistOfFury

35

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

IoC เป็นหลักการโดยที่ DI คือรูปแบบ เหตุผลที่คุณอาจ "ต้องการคนตัดไม้มากกว่าหนึ่งคน" นั้นไม่เคยพบจริงเท่าที่ประสบการณ์ของฉันจะไป แต่เหตุผลที่แท้จริงคือคุณต้องการมันจริง ๆ ทุกครั้งที่คุณทดสอบอะไรบางอย่าง ตัวอย่าง:

คุณสมบัติของฉัน:

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

คุณอาจทดสอบสิ่งนี้:

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var formdata = { . . . }

    // System under Test
    var weasel = new OfferWeasel();

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(new DateTime(2013,01,13,13,01,0,0));
}

ดังนั้นที่ใดที่หนึ่งในOfferWeaselนั้นก็จะสร้างข้อเสนอวัตถุให้คุณเช่นนี้:

public class OfferWeasel
{
    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = DateTime.Now;
        return offer;
    }
}

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

public interface IGotTheTime
{
    DateTime Now {get;}
}

public class CannedTime : IGotTheTime
{
    public DateTime Now {get; set;}
}

public class ActualTime : IGotTheTime
{
    public DateTime Now {get { return DateTime.Now; }}
}

public class OfferWeasel
{
    private readonly IGotTheTime _time;

    public OfferWeasel(IGotTheTime time)
    {
        _time = time;
    }

    public Offer Create(Formdata formdata)
    {
        var offer = new Offer();
        offer.LastUpdated = _time.Now;
        return offer;
    }
}

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

[Test]
public void ShouldUpdateTimeStamp
{
    // Arrange
    var date = new DateTime(2013, 01, 13, 13, 01, 0, 0);
    var formdata = { . . . }

    var time = new CannedTime { Now = date };

    // System under test
    var weasel= new OfferWeasel(time);

    // Act
    var offer = weasel.Create(formdata)

    // Assert
    offer.LastUpdated.Should().Be(date);
}

เช่นนี้คุณใช้หลักการ "การกลับกันของการควบคุม" โดยการฉีดการพึ่งพา (รับเวลาปัจจุบัน) เหตุผลหลักในการทำเช่นนี้คือเพื่อการทดสอบหน่วยแยกที่ง่ายขึ้นมีวิธีอื่นในการทำ ตัวอย่างเช่นอินเทอร์เฟซและคลาสที่นี่ไม่จำเป็นเนื่องจากในฟังก์ชัน C # สามารถส่งผ่านเป็นตัวแปรได้ดังนั้นแทนที่จะใช้อินเตอร์เฟสที่คุณสามารถใช้ a Func<DateTime>เพื่อให้ได้ผลลัพธ์ที่เหมือนกัน หรือถ้าคุณใช้วิธีการแบบไดนามิกคุณเพียงผ่านวัตถุใด ๆ ที่มีวิธีการเทียบเท่า (การพิมพ์เป็ด ) และคุณไม่จำเป็นต้องมีส่วนต่อประสานเลย

คุณแทบจะไม่ต้องการตัวบันทึกมากกว่าหนึ่งตัว อย่างไรก็ตามการฉีดพึ่งพาเป็นสิ่งจำเป็นสำหรับรหัสที่พิมพ์แบบคงที่เช่น Java หรือ C #

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

ฉันหวังว่าจะช่วย


4
ที่จริงดูเหมือนว่าเป็นทางออกที่น่ากลัว แน่นอนฉันจะเขียนโค้ดเพิ่มเติมเช่นDaniel Pryden คำตอบแนะนำ แต่สำหรับการทดสอบหน่วยนั้นฉันจะทำ DateTime.Now ก่อนและหลังฟังก์ชั่นและตรวจสอบว่าเวลาอยู่ในระหว่าง? การเพิ่มส่วนต่อประสาน / จำนวนบรรทัดที่มากขึ้นดูเหมือนว่าเป็นความคิดที่ไม่ดีสำหรับฉัน

3
ฉันไม่ชอบตัวอย่าง A (B) ทั่วไปและฉันไม่เคยรู้สึกว่าคนตัดไม้จำเป็นต้องมีการใช้งาน 100 ครั้ง นี่คือตัวอย่างที่ฉันพบเมื่อเร็ว ๆ นี้และเป็นหนึ่งใน 5 วิธีในการแก้ปัญหาโดยที่หนึ่งรวมถึงการใช้ PostSharp มันแสดงให้เห็นถึงวิธีการฉีด ctor คลาสสิกตาม คุณช่วยยกตัวอย่างโลกแห่งความเป็นจริงที่ดีขึ้นว่าคุณใช้ประโยชน์จาก DI ได้ดีแค่ไหน?
ดูแล

2
ฉันไม่เคยเห็นประโยชน์ที่ดีสำหรับ DI นั่นเป็นเหตุผลที่ฉันเขียนคำถาม

2
ฉันไม่พบว่ามีประโยชน์ รหัสของฉันทดสอบได้ง่ายเสมอ ดูเหมือนว่าดีสำหรับฐานรหัสขนาดใหญ่ที่มีรหัสไม่ดี

1
โปรดทราบว่าแม้ในโปรแกรมที่มีฟังก์ชั่นขนาดเล็กเมื่อใดก็ตามที่คุณมี f (x), g (f) คุณกำลังใช้การฉีดพึ่งพาดังนั้นแต่ละการดำเนินการต่อใน JS จะนับเป็นการฉีดพึ่งพา ฉันเดาว่าคุณใช้มันอยู่แล้ว)
cessor

15

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

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

class PaymentProcessor{

    private String type;

    public PaymentProcessor(String type){
        this.type = type;
    }

    public void authorize(){
        if (type.equals(Consts.PAYPAL)){
            // Do this;
        }
        else if(type.equals(Consts.OTHER_PROCESSOR)){
            // Do that;
        }
    }
}

ทีนี้ลองนึกดูว่าตอนนี้คุณจะต้องรักษารหัสนี้ไว้ในคลาสเดียวเพราะมันไม่แยกกันอย่างถูกต้องคุณสามารถจินตนาการได้ว่าสำหรับโปรเซสเซอร์ใหม่ทุกตัวที่คุณสนับสนุนคุณจะต้องสร้างใหม่ถ้า // // ตัวเรือนสวิตช์สำหรับ ทุกวิธีนี้จะมีความซับซ้อนมากขึ้นอย่างไรก็ตามโดยใช้ Dependency Injection (หรือ Inversion of Control - ซึ่งบางครั้งเรียกว่าซึ่งหมายความว่าใครก็ตามที่ควบคุมการทำงานของโปรแกรมเป็นที่รู้จักเฉพาะในรันไทม์และไม่ซับซ้อน) คุณสามารถบรรลุบางสิ่งบางอย่าง เรียบร้อยและบำรุงรักษามาก

class PaypalProcessor implements PaymentProcessor{

    public void authorize(){
        // Do PayPal authorization
    }
}

class OtherProcessor implements PaymentProcessor{

    public void authorize(){
        // Do other processor authorization
    }
}

class PaymentFactory{

    public static PaymentProcessor create(String type){

        switch(type){
            case Consts.PAYPAL;
                return new PaypalProcessor();

            case Consts.OTHER_PROCESSOR;
                return new OtherProcessor();
        }
    }
}

interface PaymentProcessor{
    void authorize();
}

** รหัสจะไม่คอมไพล์ฉันรู้ :)


+1 เพราะดูเหมือนว่าคุณต้องการที่จะใช้วิธีการ / อินเตอร์เฟสเสมือน แต่นั่นก็ยังหายาก ฉันยังคงผ่านมันเป็นแบบnew ThatProcessor()ค่อนข้างใช้กรอบ

@ ItaiS คุณสามารถหลีกเลี่ยงสวิตช์จำนวนนับไม่ถ้วนด้วยรูปแบบการออกแบบโรงงานระดับ ใช้การสะท้อนระบบ System.Reflection.Asset.GetExecutingAssembly (). CreateInstance ()
domenicr

@domenicr ofcourse! แต่ฉันต้องการอธิบายในตัวอย่างง่าย ๆ
Itai Sagi

ฉันเห็นด้วยกับคำอธิบายข้างต้นยกเว้นความต้องการของระดับโรงงาน ทันทีที่เราใช้คลาสโรงงานเป็นการเลือกแบบหยาบ คำอธิบายที่ดีที่สุดของข้างต้นที่ฉันพบในบทของ Poymorphism และฟังก์ชันเสมือนโดย Bruce Erkel DI ที่แท้จริงควรเป็นอิสระจากการเลือกและประเภทของวัตถุควรได้รับการพิจารณา ณ เวลาทำการผ่านส่วนต่อประสานโดยอัตโนมัติ นั่นคือสิ่งที่พฤติกรรม polymorphic ที่แท้จริงคือ
Arvind Krmar

ตัวอย่างเช่น (ตาม c ++) เรามีอินเทอร์เฟซทั่วไปซึ่งใช้เพียงการอ้างอิงถึงคลาสฐานและใช้ลักษณะการทำงานของคลาสที่สืบทอดมาโดยไม่มีการเลือก การปรับเป็นโมฆะ (เครื่องมือ & i) {i.play (middleC); } int main () {ขลุ่ยลม; ปรับแต่ง (ขลุ่ย); } เครื่องดนตรีเป็นชั้นฐานลมมาจากมัน ตาม c ++ ฟังก์ชันเสมือนทำให้เป็นไปได้ในการใช้พฤติกรรมของคลาสที่ได้รับผ่านอินเตอร์เฟสทั่วไป
Arvind Krmar

6

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

นั่นเป็นเรื่องปกติใน OOP มาเป็นเวลานาน หลายครั้งที่สร้างรหัสชิ้นเช่น:

I_Dosomething x = new Impl_Dosomething();

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

I_Dosomething x = ServiceLocator.returnDoing(String pKey);

ตัวอย่าง DI:

I_Dosomething x = DIContainer.returnThat();

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

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

ฉันชอบการจัดการสัญญาการบริการร่วมกับ D (b) ฉันในการเขียนโปรแกรม พวกเขาควรไปด้วยกัน การใช้ D (b) ฉันเป็นวิธีการแก้ปัญหาทางเทคนิคโดยไม่ต้องมีการบริหารสัญญาการบริการองค์กรไม่เป็นประโยชน์มากในมุมมองของฉันเพราะ DI นั้นเป็นเพียงแค่การห่อหุ้มอีกชั้นหนึ่ง แต่เมื่อคุณสามารถใช้ร่วมกับการบริหารองค์กรคุณสามารถใช้หลักการจัดระเบียบ D (b) ที่ฉันเสนอ มันสามารถช่วยคุณในระยะยาวในการจัดโครงสร้างการสื่อสารกับลูกค้าและหน่วยงานด้านเทคนิคอื่น ๆ ในหัวข้อการทดสอบการกำหนดรุ่นและการพัฒนาทางเลือก เมื่อคุณมีอินเทอร์เฟซโดยนัยเช่นเดียวกับในคลาส hardcoded แล้วมันจะสื่อสารน้อยกว่ามากในช่วงเวลานั้นเมื่อคุณทำให้มันชัดเจนโดยใช้ D (b) I ทุกอย่างเดือดลงไปเพื่อการบำรุงรักษาซึ่งเป็นเมื่อเวลาผ่านไปและไม่ได้ในเวลา :-)


1
"ข้อเสียเปรียบคือคลาสการใช้งานยังคงฮาร์ดโค้ด" <- ส่วนใหญ่มีเพียงการใช้งานเพียงครั้งเดียวและอย่างที่ฉันบอกว่าฉันไม่สามารถนึกถึงโค้ด nongame ที่ต้องใช้อินเทอร์เฟซที่ยังไม่ได้สร้างขึ้นใน. NET )

@ acidzombie24 อาจเป็น ... แต่เปรียบเทียบความพยายามในการใช้งานโซลูชันโดยใช้ DI ตั้งแต่ต้นจนถึงความพยายามในการเปลี่ยนโซลูชันที่ไม่ใช่ DI ในภายหลังหากคุณต้องการอินเตอร์เฟส ฉันมักจะไปกับตัวเลือกแรก มันจะดีกว่าที่จะจ่ายตอนนี้ $ 100 แทนที่จะต้องจ่ายในวันพรุ่งนี้
Golo Roden

1
@GoloRoden แน่นอนการบำรุงรักษาเป็นประเด็นสำคัญโดยใช้เทคนิคเช่น D (b) I นั่นคือค่าใช้จ่าย 80% ของแอปพลิเคชัน การออกแบบที่พฤติกรรมที่จำเป็นต้องทำอย่างชัดเจนโดยใช้ส่วนต่อประสานจากจุดเริ่มต้นช่วยให้องค์กรประหยัดเวลาและเงินจำนวนมากในภายหลัง
Loek Bergman

ฉันจะไม่เข้าใจอย่างแท้จริงจนกว่าฉันจะต้องจ่ายเพราะจนถึงตอนนี้ฉันจ่าย $ 0 และจนถึงตอนนี้ฉันยังต้องจ่ายแค่ $ 0 เท่านั้น แต่ฉันจ่าย $ 0.05 เพื่อให้ทุกบรรทัดหรือฟังก์ชั่นสะอาด
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.