วิธีการหลีกเลี่ยงการพึ่งพาการสร้างบ้าฉีด?


300

ฉันพบว่าตัวสร้างของฉันเริ่มมีลักษณะเช่นนี้:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

ด้วยรายการพารามิเตอร์ที่เพิ่มขึ้น เนื่องจาก "คอนเทนเนอร์" เป็นคอนเทนเนอร์ฉีดที่ขึ้นต่อกันของฉันทำไมฉันจึงทำสิ่งนี้ไม่ได้:

public MyClass(Container con)

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


64
ทำไมคุณผ่านภาชนะบรรจุ ฉันคิดว่าคุณอาจเข้าใจผิด IOC
Paul Creasey

33
หากคอนสตรัคเตอร์ของคุณต้องการพารามิเตอร์มากขึ้นหรือมากขึ้นคุณอาจทำมากเกินไปในคลาสเหล่านั้น
Austin Salonen

38
นั่นไม่ใช่วิธีที่คุณใช้ในการสร้างคอนสตรัคชัน วัตถุไม่รู้เกี่ยวกับคอนเทนเนอร์ IoC เลยและไม่ควรใช้
duffymo

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

คำตอบ:


409

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

หนึ่งในผลประโยชน์ที่ยอดเยี่ยมของ Constructor Injection คือการละเมิดหลักการความรับผิดชอบแบบเดี่ยวอย่างเห็นได้ชัด

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


8
+1 สำหรับการนับจำนวนความพยายามในการปรับโครงสร้างให้เป็นแนวคิดเดียว น่ากลัว :)
Ryan Emerle

46
จริงเหรอ คุณเพิ่งสร้างทางอ้อมเพื่อย้ายพารามิเตอร์เหล่านั้นไปยังคลาสอื่น แต่ยังคงมีอยู่! ซับซ้อนมากขึ้นเพื่อจัดการกับพวกเขา
ปฏิเสธไม่ได้

23
@ โต้แย้งได้: ในกรณีที่ความเสื่อมโทรมที่เราย้ายการพึ่งพาทั้งหมดไปสู่การรวมบริการฉันยอมรับว่ามันเป็นอีกระดับหนึ่งของการอ้อมที่ไม่มีประโยชน์ดังนั้นการเลือกคำพูดของฉันจึงเล็กน้อย อย่างไรก็ตามประเด็นก็คือเราย้ายการอ้างอิงบางส่วนที่ละเอียดอ่อนไปยัง Aggregate Service สิ่งนี้ จำกัด จำนวนการเปลี่ยนลำดับการพึ่งพาทั้งในบริการรวมใหม่และสำหรับการพึ่งพาที่เหลืออยู่ ทำให้ทั้งคู่จัดการได้ง่ายขึ้น
Mark Seemann

92
คำพูดที่ดีที่สุด: "หนึ่งในผลประโยชน์ที่ยอดเยี่ยมของ Constructor Injection คือการละเมิดหลักการความรับผิดชอบแบบเดี่ยวอย่างเห็นได้ชัด"
Igor Popov

2
@ DonBox ในกรณีนั้นคุณสามารถเขียนการประยุกต์ใช้วัตถุเป็นโมฆะเพื่อหยุดการเรียกซ้ำ ไม่ใช่สิ่งที่คุณต้องการ แต่ประเด็นคือ Constructor Injection ไม่ได้ป้องกันวงจร - มันทำให้ชัดเจนว่าพวกมันอยู่ที่นั่น
Mark Seemann

66

ฉันไม่คิดว่าผู้สร้างคลาสของคุณควรมีการอ้างอิงถึงช่วงเวลาคอนเทนเนอร์ IOC ของคุณ สิ่งนี้แสดงถึงการพึ่งพาที่ไม่จำเป็นระหว่างคลาสของคุณและคอนเทนเนอร์ (ชนิดของ IOC อ้างอิงที่พยายามหลีกเลี่ยง!)


+1 ขึ้นอยู่กับคอนเทนเนอร์ IoC ทำให้มันยากที่จะเปลี่ยนคอนเทนเนอร์นั้นในภายหลังโดยไม่ต้องเปลี่ยนพวงของรหัสในคลาสอื่น ๆ ทั้งหมด
Tseng

1
คุณจะนำ IOC ไปใช้อย่างไรโดยไม่ต้องมีพารามิเตอร์ของอินเตอร์เฟสบนตัวสร้าง ฉันอ่านโพสต์ของคุณผิดหรือเปล่า?
J Hunt

@J Hunt ฉันไม่เข้าใจความคิดเห็นของคุณ สำหรับฉันพารามิเตอร์ของอินเทอร์เฟซหมายถึงพารามิเตอร์ที่เป็นอินเทอร์เฟซสำหรับการอ้างอิงเช่นถ้าคอนเทนเนอร์การฉีดของคุณเริ่มต้นMyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(พารามิเตอร์อินเตอร์เฟส) สิ่งนี้ไม่เกี่ยวข้องกับการโพสต์ของ @ derivation ซึ่งฉันตีความว่าการพูดว่าถังบรรจุที่ต้องพึ่งพาไม่ควรฉีดตัวเองเข้าไปในวัตถุเช่นMyClass myClass = new MyClass(this)
John Doe

25

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

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


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

4

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

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

รหัสรายละเอียดพร้อมคำอธิบายอยู่ที่นี่

แนวทางปฏิบัติที่ดีที่สุดสำหรับ IoC ในชั้นบริการที่ซับซ้อน


3

ฉันอ่านกระทู้ทั้งหมดนี้สองครั้งและฉันคิดว่าผู้คนตอบสนองโดยสิ่งที่พวกเขารู้ไม่ใช่สิ่งที่ถูกถาม

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

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

Container.GetSevice<MyClass>(someObject1, someObject2)

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

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

มีกรอบที่อาจช่วยแม้ว่าพวกเขาจะดูน่าเกลียดเล็กน้อย ตัวอย่างเช่น Ninject:

การสร้างอินสแตนซ์โดยใช้ Ninject พร้อมพารามิเตอร์เพิ่มเติมในตัวสร้าง

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


3

การฉีดคอนเทนเนอร์เป็นทางลัดที่คุณจะต้องเสียใจในที่สุด

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

นี่คือรายการที่ไม่สมบูรณ์ของสิ่งที่ต้องระวัง

การออกแบบโดเมนไม่ดี (รวมรากของ…. ฯลฯ )

การแยกข้อกังวลไม่ดี (องค์ประกอบบริการคำสั่งคิวรี) ดู CQRS และการจัดหากิจกรรม

หรือผู้ทำแผนที่ (ระวังสิ่งเหล่านี้สามารถนำคุณไปสู่ปัญหา)

ดูแบบจำลองและ DTO อื่น ๆ (อย่านำมาใช้ซ้ำและพยายามทำให้น้อยที่สุด !!!!)


2

ปัญหา:

1) คอนสตรัคเตอร์ที่มีรายการพารามิเตอร์เพิ่มขึ้นเรื่อย ๆ

2) ถ้าคลาสนั้นได้รับการสืบทอด (Ex:) RepositoryBaseดังนั้นการเปลี่ยนลายเซ็นคอนสตรัคเตอร์ทำให้เกิดการเปลี่ยนแปลงในคลาสที่ได้รับ

โซลูชันที่ 1

ผ่านIoC Containerไปยังตัวสร้าง

ทำไม

  • ไม่มีการเพิ่มรายการพารามิเตอร์อีกต่อไป
  • ลายเซ็นของตัวสร้างกลายเป็นเรื่องง่าย

ทำไมจะไม่ล่ะ

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

โซลูชันที่ 2

สร้างคลาสที่จัดกลุ่มบริการทั้งหมดและส่งต่อไปยังตัวสร้าง

 public abstract class EFRepositoryBase 
 {
    public class Dependency
    {
        public DbContext DbContext { get; }
        public IAuditFactory AuditFactory { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
        }
    }

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    {
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    }
  }

คลาสที่ได้รับมา

  public class ApplicationEfRepository : EFRepositoryBase      
  {
     public new class Dependency : EFRepositoryBase.Dependency
     {
         public IConcreteDependency ConcreteDependency { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        }
     }

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
      { 
        _concreteDependency = dependency.ConcreteDependency;
      }
   }

ทำไม

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

ทำไมจะไม่ล่ะ

  • จำเป็นต้องสร้างคลาสเพิ่มเติม
  • การลงทะเบียนบริการมีความซับซ้อน(คุณต้องลงทะเบียนX.Dependencyแยกต่างหากทุกครั้ง)
  • แนวคิดเหมือนกับผ่าน IoC Container
  • ..

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


1

นี่คือวิธีที่ฉันใช้

public class Hero
{

    [Inject]
    private IInventory Inventory { get; set; }

    [Inject]
    private IArmour Armour { get; set; }

    [Inject]
    protected IWeapon Weapon { get; set; }

    [Inject]
    private IAction Jump { get; set; }

    [Inject]
    private IInstanceProvider InstanceProvider { get; set; }


}

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

public class InjectAttribute : Attribute
{

}


public class TestClass
{
    [Inject]
    private SomeDependency sd { get; set; }

    public TestClass()
    {
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    }
}

public class SomeDependency
{

}


class Program
{
    static void Main(string[] args)
    {
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    }
}

ฉันกำลังทำงานเกี่ยวกับโครงการงานอดิเรกซึ่งทำงานเช่นนี้ https://github.com/Jokine/ToolProject/tree/Core


-8

คุณใช้กรอบการฉีดแบบพึ่งพาอะไร คุณเคยลองใช้การฉีด setter แทนหรือไม่?

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

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

ทั้งสองวิธีทำงานมันเป็นเรื่องของการตั้งค่า


21
เหตุผลในการฉีดคอนสตรัคเตอร์คือการทำให้การอ้างอิงชัดเจนขึ้นไม่ใช่เพราะมันดูเป็นธรรมชาติมากขึ้นสำหรับนักพัฒนาจาวา
L-Four

8
ความคิดเห็นปลาย แต่คำตอบนี้ทำให้ฉันหัวเราะ :)
เฟรดเดอ Prijck

1
+1 สำหรับการฉีดตาม setter ถ้าฉันมีบริการและที่เก็บข้อมูลที่กำหนดไว้ในชั้นเรียนของฉันมันค่อนข้างชัดเจนว่าพวกเขาเป็นผู้อ้างอิง .. ฉันไม่จำเป็นต้องเขียนคอนสตรัคเตอร์ VB6 ขนาดใหญ่ที่มองหาและทำการกำหนดรหัสโง่ ๆ ในคอนสตรัคเตอร์ มันค่อนข้างชัดเจนว่าการพึ่งพาอาศัยกันบนฟิลด์ที่ต้องการคืออะไร
Piotr Kula

ตาม 2018 Spring แนะนำอย่างเป็นทางการว่าไม่ควรใช้ setter injection ยกเว้นการพึ่งพาที่มีค่าเริ่มต้นที่สมเหตุสมผล เช่นเดียวกับในกรณีที่การขึ้นต่อกันนั้นจำเป็นต้องมีในชั้นเรียนแนะนำให้ทำการสร้างการฉีด ดูการสนทนาเกี่ยวกับ setter vs ctor DI
John Doe
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.