วิธีการใช้การฉีดพึ่งพาและหลีกเลี่ยงการมีเพศสัมพันธ์ชั่วคราว?


11

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

public interface IService
{
    void Initialize(Context context);
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3)
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
    }

    public void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

public class Context
{
    public int Value1;
    public string Value2;
    public string Value3;
}

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

นี่คือลักษณะลูกค้าตัวอย่าง:

public class Client
{
    private readonly IService service;

    public Client(IService service)
    {
        this.service = service ?? throw new ArgumentNullException(nameof(service));
    }

    public void OnStartup()
    {
        service.Initialize(new Context
        {
            Value1 = 123,
            Value2 = "my data",
            Value3 = "abcd"
        });
    }

    public void Execute()
    {
        service.DoSomething();
        service.DoOtherThing();
    }
}

อย่างที่คุณเห็น - มีการมีเพศสัมพันธ์ชั่วคราวและกำหนดค่าเริ่มต้นให้มีกลิ่นรหัสที่เกี่ยวข้องเพราะก่อนอื่นฉันต้องโทรservice.Initializeเพื่อให้สามารถโทรservice.DoSomethingและservice.DoOtherThingหลังจากนั้น

อะไรคือแนวทางอื่นที่ฉันสามารถขจัดปัญหาเหล่านี้ได้?

การชี้แจงเพิ่มเติมเกี่ยวกับพฤติกรรม:

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

คำตอบ:


18

มีหลายวิธีในการจัดการกับปัญหาการเริ่มต้น:

  • ตามที่ได้รับคำตอบในhttps://softwareengineering.stackexchange.com/a/334994/301401เมธอด init () เป็นกลิ่นรหัส การกำหนดค่าเริ่มต้นวัตถุเป็นความรับผิดชอบของตัวสร้าง - นั่นคือสาเหตุที่เรามีตัวสร้างหลังจากทั้งหมด
  • เพิ่มบริการที่กำหนดจะต้องเริ่มต้นความคิดเห็น doc ของตัวClientสร้างและปล่อยให้ตัวสร้างหากบริการไม่ได้เริ่มต้น สิ่งนี้เป็นการย้ายความรับผิดชอบไปยังผู้ที่ให้IServiceสิ่งของแก่คุณ

อย่างไรก็ตามในตัวอย่างของคุณเป็นเพียงคนเดียวที่รู้ค่าที่ส่งผ่านไปยังClient Initialize()หากคุณต้องการให้เป็นเช่นนั้นฉันจะแนะนำสิ่งต่อไปนี้:

  • เพิ่มIServiceFactoryและส่งไปยังตัวClientสร้าง จากนั้นคุณสามารถโทรหาได้serviceFactory.createService(new Context(...))ซึ่งจะช่วยให้คุณได้รับการเริ่มต้นIServiceที่ลูกค้าสามารถใช้งานได้

โรงงานสามารถทำได้ง่ายมากและยังช่วยให้คุณหลีกเลี่ยงวิธีการเริ่มต้น () และใช้ตัวสร้างแทน:

public interface IServiceFactory
{
    IService createService(Context context);
}

public class ServiceFactory : IServiceFactory
{
    public Service createService(Context context)
    {
        return new Service(context);
    }
}

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

เมื่อServiceมีการอ้างอิงที่ไม่ได้ให้ไว้Clientโดย DI จะได้รับจากServiceFactory:

public interface IServiceFactory
{
    IService createService(Context context);
}    

public class ServiceFactory : IServiceFactory
{        
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public ServiceFactory(object dependency1, object dependency2, object dependency3)
    {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
        this.dependency3 = dependency3;
    }

    public Service createService(Context context)
    {
        return new Service(context, dependency1, dependency2, dependency3);
    }
}

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

1
@Dusan ไม่ใช้ Service Locator หากServiceมีการอ้างอิงอื่นนอกเหนือจากContextที่จะไม่ให้โดยClientพวกเขาสามารถให้ผ่าน DI ไปยังที่ServiceFactoryจะถูกส่งผ่านไปServiceเมื่อcreateServiceถูกเรียก
Mr.Mindor

@Dusan หากคุณต้องการจัดหาการพึ่งพาที่แตกต่างกันไปยังบริการที่แตกต่างกัน (เช่น: สิ่งนี้ต้องการการพึ่งพา 1_1 แต่สิ่งต่อไปนี้จำเป็นต้องมีการพึ่งพา 1_2) แต่ถ้ารูปแบบนี้เหมาะกับคุณเป็นอย่างอื่น ตัวสร้างช่วยให้คุณสามารถตั้งค่าวัตถุทีละน้อยในช่วงเวลาถ้าจำเป็น จากนั้นคุณสามารถทำสิ่งนี้ ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);และปล่อยให้อยู่กับการตั้งค่าบริการบางส่วนของคุณจากนั้นทำในภายหลังService s = partial.context(context).build()
Aaron

1

InitializeวิธีการควรจะถูกลบออกจากIServiceอินเตอร์เฟซเช่นนี้เป็นรายละเอียดการดำเนินงาน ให้กำหนดคลาสอื่นที่ใช้อินสแตนซ์ที่เป็นรูปธรรมของบริการและเรียกวิธีการเตรียมใช้งานแทน คลาสใหม่นี้ใช้อินเตอร์เฟส IService:

public class ContextDependentService : IService
{
    public ContextDependentService(Context context, Service service)
    {
        this.service = service;

        service.Initialize(context);
    }

    // Methods in the IService interface
}

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


1

สำหรับฉันดูเหมือนว่าคุณมีสองตัวเลือกที่นี่

  1. ย้ายรหัสการเริ่มต้นไปที่บริบทและฉีดบริบทเริ่มต้น

เช่น.

public InitialisedContext Initialise()
  1. รับสายแรกเพื่อดำเนินการโทรเริ่มต้นหากยังไม่เสร็จสิ้น

เช่น.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. เพียงแค่โยนข้อยกเว้นถ้าบริบทไม่ได้เริ่มต้นเมื่อคุณเรียกใช้ดำเนินการ ชอบ SqlConnection

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

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


0

คุณไม่ควรใช้อินเทอร์เฟซของคุณกับบริบท db ใด ๆ และวิธีการเริ่มต้น คุณสามารถทำได้ในตัวสร้างคลาสที่เป็นรูปธรรม

public interface IService
{
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;
    private readonly object context;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3,
        object context )
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));

        // context is concrete class details not interfaces.
        this.context = context;

        // call init here constructor.
        this.Initialize(context);
    }

    protected void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

และคำตอบของคำถามหลักของคุณจะฉีดทรัพย์สิน

public class Service
    {
        public Service(Context context)
        {
            this.context = context;
        }

        private Dependency1 _dependency1;
        public Dependency1 Dependency1
        {
            get
            {
                if (_dependency1 == null)
                    _dependency1 = Container.Resolve<Dependency1>();

                return _dependency1;
            }
        }

        //...
    }

วิธีนี้คุณสามารถเรียกการอ้างอิงโดยการฉีดทรัพย์สิน แต่มันอาจเป็นจำนวนมาก ถ้าเป็นเช่นนั้นคุณสามารถใช้ Constructor Injection สำหรับพวกเขา แต่คุณสามารถตั้งค่าบริบทของคุณตามคุณสมบัติโดยตรวจสอบว่ามันเป็นโมฆะ


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

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

ฉันโน้มตัวไปยังโรงงานฉีดที่สามารถสร้างและเริ่มต้นบริการด้วยข้อมูลบริบทที่กำหนด (แทนที่จะฉีดบริการเอง) แต่ฉันไม่แน่ใจว่ามีวิธีแก้ปัญหาที่ดีกว่าหรือไม่
Dusan

@Ewan คุณพูดถูก ฉันจะพยายามหาทางออกให้กับมัน แต่ก่อนหน้านั้นฉันจะลบออกตอนนี้
Engineert

0

Misko Hevery มีบล็อกโพสต์ที่เป็นประโยชน์มากเกี่ยวกับกรณีที่คุณเผชิญ คุณทั้งสองต้องการnewableและinjectableสำหรับServiceชั้นเรียนของคุณและโพสต์บล็อกนี้อาจช่วยคุณได้

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