มีรูปแบบสำหรับการเริ่มต้นวัตถุที่สร้างผ่านคอนเทนเนอร์ DI หรือไม่


147

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

ในขณะนี้วิธีเดียวที่ฉันสามารถคิดวิธีทำคือมีวิธี Init บนอินเทอร์เฟซ

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

จากนั้นให้ใช้ (ใน Unity) ฉันจะทำสิ่งนี้:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

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

สิ่งนี้สร้างปัญหาจำนวนหนึ่ง ได้แก่ Initializeวิธีนี้มีอยู่ในส่วนต่อประสานและสามารถเรียกได้หลายครั้ง การตั้งธงในการดำเนินการและการขว้างปาข้อยกเว้นในการเรียกซ้ำInitializeดูเหมือนว่าวิธีที่อุ้ยอ้าย

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

แก้ไข: อธิบายอินเตอร์เฟสอีกเล็กน้อย


9
คุณไม่มีจุดประสงค์ในการใช้คอนเทนเนอร์ DI ควรจะมีการอ้างอิงการพึ่งพาสำหรับคุณ
Pierreten

คุณรับพารามิเตอร์ที่ต้องการจากที่ไหน (ไฟล์กำหนดค่า, db, ??)
Jaime

runTimeParamเป็นการอ้างอิงที่พิจารณา ณ รันไทม์ตามอินพุตของผู้ใช้ ทางเลือกสำหรับสิ่งนี้ควรแยกออกเป็นสองอินเตอร์เฟส - หนึ่งสำหรับการเริ่มต้นและอื่น ๆ สำหรับการจัดเก็บค่า?
Igor Zevaka

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

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

คำตอบ:


276

ที่ใดก็ตามที่คุณต้องการค่าเวลาทำงานเพื่อสร้างการพึ่งพาเฉพาะAbstract Factoryคือคำตอบ

มีวิธีการเริ่มต้นในการเชื่อมต่อกลิ่นของรั่วที่เป็นนามธรรม

ในกรณีของคุณฉันจะบอกว่าคุณควรทำโมเดลIMyIntfอินเทอร์เฟซเกี่ยวกับวิธีการใช้งาน - ไม่ใช่วิธีที่คุณต้องการสร้างการใช้งาน นั่นคือรายละเอียดการใช้งาน

ดังนั้นอินเตอร์เฟสควรเป็น:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

ตอนนี้กำหนด Abstract Factory:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

ตอนนี้คุณสามารถสร้างการใช้งานที่เป็นรูปธรรมของ IMyIntfFactoryซึ่งสร้างอินสแตนซ์ที่เป็นรูปธรรมของIMyIntfสิ่งนี้:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

สังเกตว่ามันช่วยให้เรา ปกป้องค่าคงที่ของคลาสโดยใช้readonlyคำหลักได้อย่างไร ไม่จำเป็นต้องใช้วิธีการส่งกลิ่น

IMyIntfFactoryติดตั้งอาจทำได้ง่ายเช่นนี้:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

ในผู้บริโภคของคุณทุกที่ที่คุณต้องการIMyIntfเช่นคุณก็จะพึ่งพาIMyIntfFactoryโดยการร้องขอผ่านทางสร้างฉีด

ภาชนะ DI ใด ๆ ที่มีมูลค่าจะสามารถทำการIMyIntfFactoryอินสแตนซ์ให้คุณโดยอัตโนมัติหากคุณลงทะเบียนอย่างถูกต้อง


13
ปัญหาคือวิธีการ (เช่นเริ่มต้น) เป็นส่วนหนึ่งของ API ของคุณในขณะที่ตัวสร้างไม่ได้ blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann

13
นอกจากนี้วิธีการเตรียมใช้งานบ่งชี้ว่าการมีเพศสัมพันธ์
Mark Seemann

2
@Darlene คุณอาจจะสามารถที่จะใช้เริ่มต้นได้อย่างเฉื่อยชามัณฑนากรตามที่อธิบายไว้ในส่วน 8.3.6 ของหนังสือของฉัน ฉันยังให้ตัวอย่างของสิ่งที่คล้ายกันในงานนำเสนอกราฟวัตถุขนาดใหญ่ของฉันล่วงหน้า
Mark Seemann

2
@ Mark หากการสร้างMyIntfการนำไปปฏิบัติใช้ในโรงงานต้องการมากกว่าrunTimeParam(อ่าน: บริการอื่น ๆ ที่ต้องการแก้ไขโดย IoC) แสดงว่าคุณยังต้องเผชิญกับการแก้ไขการพึ่งพาเหล่านั้นในโรงงานของคุณ ฉันชอบ @PhilSandler คำตอบของการส่งต่อการพึ่งพาเหล่านั้นลงในคอนสตรัคเตอร์ของโรงงานเพื่อแก้ปัญหานี้ - นั่นคือสิ่งที่คุณใช้กับมันหรือไม่?
เจฟฟ์

2
เป็นสิ่งที่ยอดเยี่ยมเช่นกัน แต่คำตอบของคุณสำหรับคำถามอื่นนี้มาถึงจุดของฉันจริงๆ
Jeff

15

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

หากคุณต้องการพารามิเตอร์เฉพาะบริบทที่ส่งผ่านในตัวสร้างหนึ่งตัวเลือกคือการสร้างโรงงานที่แก้ไขการพึ่งพาบริการของคุณผ่านตัวสร้างและใช้พารามิเตอร์เวลาทำงานของคุณเป็นพารามิเตอร์ของวิธีการสร้าง () หรือสร้าง ), Build () หรืออะไรก็ตามที่คุณตั้งชื่อโรงงานของคุณ)

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


5

ฉันยังเจอสถานการณ์นี้สองสามครั้งในสภาพแวดล้อมที่ฉันสร้างวัตถุ ViewModel แบบไดนามิกโดยใช้วัตถุ Model (สรุปได้ดีมากโดยโพสต์ Stackoverflowอื่น ๆ นี้ )

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

Bind<IMyFactory>().ToFactory();

ฉันไม่พบฟังก์ชันการทำงานที่คล้ายกันโดยตรงในUnity ; ดังนั้นฉันจึงเขียนส่วนขยายของตัวเองไปยังIUnityContainerซึ่งช่วยให้คุณสามารถลงทะเบียนโรงงานที่จะสร้างวัตถุใหม่โดยใช้ข้อมูลจากวัตถุที่มีอยู่ในปัจจุบันการแมปจากลำดับชั้นชนิดหนึ่งไปยังลำดับชั้นชนิดอื่น: UnityMappingFactory @ GitHub

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

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

จากนั้นคุณเพิ่งประกาศอินเตอร์เฟสการแมปโรงงานในตัวสร้างสำหรับ CI และใช้วิธีการสร้าง () ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

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

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


1

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

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

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

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


ขอบคุณสำหรับสิ่งนั้น จริง ๆ แล้วฉันกำลังประเมินกรอบ DI และ NInject จะเป็นกรอบต่อไปของฉัน
Igor Zevaka

@johann: ผู้ให้บริการ? github.com/ninject/ninject/wiki/…
anthony

1

ฉันคิดว่าฉันแก้ไขมันและรู้สึกค่อนข้างดีดังนั้นมันต้องเป็นครึ่งเดียว :))

ฉันแบ่งIMyIntfเป็นอินเทอร์เฟซ "ทะเยอทะยาน" และ "เซทเทอร์" ดังนั้น:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

จากนั้นการดำเนินการ:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()ยังคงสามารถเรียกหลายครั้ง แต่ใช้บิตของกระบวนทัศน์บริการสเราสามารถตัดมันขึ้นอย่างมากเพื่อที่เกือบจะเป็นอินเตอร์เฟซภายในที่แตกต่างจากIMyIntfSetterIMyIntf


13
นี่ไม่ใช่วิธีการแก้ปัญหาที่ดีโดยเฉพาะอย่างยิ่งเนื่องจากต้องอาศัยวิธีการเตรียมใช้งานซึ่งเป็นสิ่งที่ทำให้เข้าใจผิด Btw ดูเหมือนว่าตัวค้นหาบริการ แต่จะเหมือนกับ Interface Injection ไม่ว่าในกรณีใดดูคำตอบของฉันสำหรับวิธีแก้ปัญหาที่ดีกว่า
Mark Seemann
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.