ฉันสามารถใช้ Dependency Injection โดยไม่ทำลาย Encapsulation ได้ไหม


15

นี่คือโซลูชันและโครงการของฉัน:

  • ร้านหนังสือ (โซลูชัน)
    • BookStore.Coupler (โครงการ)
      • Bootstrapper.cs
    • BookStore.Domain (โครงการ)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (โครงการ)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (โครงการ)
      • Global.asax
    • BookStore.BatchProcesses (โครงการ)
      • Program.cs

Bootstrapper.cs :

public static class Bootstrapper.cs 
{
    // I'm using SimpleInjector as my DI Container
    public static void Initialize(Container container) 
    {
        container.RegisterManyForOpenGeneric(typeof(ICommandHandler<>), typeof(CreateBookCommandHandler).Assembly);
        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ValidationCommandHandlerDecorator<>));
        container.RegisterManyForOpenGeneric(typeof(IValidate<>),
            AccessibilityOption.PublicTypesOnly,
            (serviceType, implTypes) => container.RegisterAll(serviceType, implTypes),
            typeof(IValidate<>).Assembly);
        container.RegisterSingleOpenGeneric(typeof(IValidator<>), typeof(CompositeValidator<>));
    }
}

CreateBookCommandValidator.cs

public class CreateBookCommandValidator : IValidate<CreateBookCommand>
{
    public IEnumerable<IValidationResult> Validate(CreateBookCommand book)
    {
        if (book.Author == "Evan")
        {
            yield return new ValidationResult<CreateBookCommand>("Evan cannot be the Author!", p => p.Author);
        }
        if (book.Price < 0)
        {
            yield return new ValidationResult<CreateBookCommand>("The price can not be less than zero", p => p.Price);
        }
    }
}

CompositeValidator.cs

public class CompositeValidator<T> : IValidator<T>
{
    private readonly IEnumerable<IValidate<T>> validators;

    public CompositeValidator(IEnumerable<IValidate<T>> validators)
    {
        this.validators = validators;
    }

    public IEnumerable<IValidationResult> Validate(T instance)
    {
        var allResults = new List<IValidationResult>();

        foreach (var validator in this.validators)
        {
            var results = validator.Validate(instance);
            allResults.AddRange(results);
        }
        return allResults;
    }
}

IValidate.cs

public interface IValidate<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

IValidator.cs

public interface IValidator<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

ICommandHandler.cs

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

CreateBookCommandHandler.cs

public class CreateBookCommandHandler : ICommandHandler<CreateBookCommand>
{
    private readonly IBookStore _bookStore;

    public CreateBookCommandHandler(IBookStore bookStore)
    {
        _bookStore = bookStore;
    }

    public void Handle(CreateBookCommand command)
    {
        var book = new Book { Author = command.Author, Name = command.Name, Price = command.Price };
        _bookStore.SaveBook(book);
    }
}

ValidationCommandHandlerDecorator.cs

public class ValidationCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;
    private readonly IValidator<TCommand> validator;

    public ValidationCommandHandlerDecorator(ICommandHandler<TCommand> decorated, IValidator<TCommand> validator)
    {
        this.decorated = decorated;
        this.validator = validator;
    }

    public void Handle(TCommand command)
    {
        var results = validator.Validate(command);

        if (!results.IsValid())
        {
            throw new ValidationException(results);
        }

        decorated.Handle(command);
    }
}

Global.asax

// inside App_Start()
var container = new Container();
Bootstrapper.Initialize(container);
// more MVC specific bootstrapping to the container. Like wiring up controllers, filters, etc..

Program.cs

// Pretty much the same as the Global.asax

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

ฉันไม่ต้องการที่จะทำให้ publicCreateBookCommandValidator ฉันอยากจะเป็นinternalแต่ถ้าฉันทำinternalฉันจะไม่สามารถลงทะเบียนกับ DI Container ของฉันได้ เหตุผลที่ฉันต้องการให้เป็นแบบภายในเพราะโครงการเดียวที่ควรมีแนวคิดเกี่ยวกับการทำให้เกิดผล IValidate <> ของฉันอยู่ในโครงการ BookStore.Domain โครงการอื่น ๆ เพียงแค่ต้องใช้ IValidator <> และ CompositeValidator ควรแก้ไขซึ่งจะตอบสนองการตรวจสอบทั้งหมด

ฉันจะใช้ Dependency Injection โดยไม่ทำลาย encapsulation ได้อย่างไร หรือฉันจะทำผิดทั้งหมดหรือเปล่า?


แค่ nitpick: สิ่งที่คุณใช้ไม่ใช่รูปแบบคำสั่งที่ถูกต้องดังนั้นการเรียกใช้คำสั่งอาจเป็นข้อมูลที่ผิด นอกจากนี้ CreateBookCommandHandler ดูเหมือนว่ามันกำลังทำลาย LSP: จะเกิดอะไรขึ้นถ้าคุณผ่านวัตถุนั่นมาจาก CreateBookCommand และฉันคิดว่าสิ่งที่คุณกำลังทำอยู่ที่นี่คืออันที่จริงรูปแบบ Anemic Domain Model สิ่งต่าง ๆ เช่นการบันทึกควรอยู่ภายในโดเมนและการตรวจสอบควรเป็นส่วนหนึ่งของเอนทิตี
ร่าเริง

1
@Eurhoric: ถูกต้อง นี้ไม่ได้เป็นรูปแบบคำสั่ง เป็นเรื่องของความเป็นจริง OP ดังต่อไปนี้เป็นรูปแบบที่แตกต่างกันคือคำสั่ง / รูปแบบการจัดการ
Steven

มีคำตอบที่ดีมากมายที่ฉันต้องการฉันสามารถทำเครื่องหมายได้มากกว่านี้เป็นคำตอบ ขอบคุณทุกคนสำหรับความช่วยเหลือของคุณ
Evan Larsen

@Eurhoric หลังจากคิดโครงการเค้าโครงฉันคิดว่า CommandHandlers ควรอยู่ในโดเมน ไม่แน่ใจว่าทำไมฉันถึงใส่พวกเขาในโครงการโครงสร้างพื้นฐาน ขอบคุณ
Evan Larsen

ที่เกี่ยวข้อง: stackoverflow.com/q/31121611/264697
Steven

คำตอบ:


11

ทำให้CreateBookCommandValidatorประชาชนไม่ได้ละเมิด encapsulation ตั้งแต่

การห่อหุ้มถูกใช้เพื่อซ่อนค่าหรือสถานะของวัตถุข้อมูลที่มีโครงสร้างภายในชั้นเรียนเพื่อป้องกันไม่ให้บุคคลที่ไม่ได้รับอนุญาตเข้าถึงพวกเขาโดยตรง ( วิกิพีเดีย )

คุณCreateBookCommandValidatorไม่อนุญาตให้เข้าถึงข้อมูลสมาชิก (ในขณะนี้ดูเหมือนจะไม่มี) ดังนั้นจึงไม่เป็นการละเมิด encapsulation

การทำให้คลาสนี้เป็นแบบสาธารณะไม่ได้ละเมิดหลักการอื่น ๆ (เช่นหลักการSOLID ) เนื่องจาก:

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

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

แม้ว่าคุณจะสามารถทำให้คลาสเป็นแบบภายในและใช้ [InternalsVisibleTo] เพื่ออนุญาตให้โครงการทดสอบหน่วยการเข้าถึงภายในของโครงการของคุณทำไมต้องกังวล?

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

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

เรื่องสั้นยาวมากอย่าทำให้คลาสนี้เป็นแบบภายในถ้าคุณไม่เขียนไลบรารีที่สามารถใช้ซ้ำได้

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

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    AccessibilityOption.AllTypes,
    container.RegisterAll,
    typeof(IValidate<>).Assembly);

สิ่งเดียวที่จะทำให้แน่ใจได้คือผู้ตรวจสอบความถูกต้องทุกคนของคุณมีคอนสตรัคสาธารณะแม้ว่าจะอยู่ภายใน ถ้าคุณอยากประเภทของคุณจะมีคอนสตรัคภายใน (ไม่ทราบจริงๆว่าทำไมคุณต้องการที่) คุณสามารถแทนที่พฤติกรรมสร้างความละเอียด

UPDATE

ตั้งแต่Simple Injector v2.6พฤติกรรมเริ่มต้นของRegisterManyForOpenGenericคือการลงทะเบียนทั้งประเภทสาธารณะและภายใน ดังนั้นการจัดหาAccessibilityOption.AllTypesจึงซ้ำซ้อนและคำสั่งต่อไปนี้จะลงทะเบียนทั้งแบบสาธารณะและภายใน:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);

8

ไม่ใช่เรื่องใหญ่ที่CreateBookCommandValidatorชั้นเรียนเป็นแบบสาธารณะ

หากคุณต้องการสร้างอินสแตนซ์ของมันนอกห้องสมุดที่กำหนดมันเป็นวิธีที่ค่อนข้างเป็นธรรมชาติในการเปิดเผยคลาสสาธารณะและนับลูกค้าที่ใช้คลาสนั้นเป็นการนำไปใช้IValidate<CreateBookCommand>เท่านั้น (เพียงเปิดเผยประเภทไม่ได้หมายความว่า encapsulation เสียมันเพียงทำให้ง่ายขึ้นเล็กน้อยสำหรับลูกค้าที่จะทำลาย encapsulation)

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

public static class Validators
{
    public static IValidate<CreateBookCommand> NewCreateBookCommandValidator()
    {
        return new CreateBookCommnadValidator();
    }
}

สำหรับการลงทะเบียนในภาชนะ DI ของคุณภาชนะ DI ทั้งหมดที่ฉันรู้จักอนุญาตให้มีการก่อสร้างโดยใช้วิธีการแบบคงที่จากโรงงาน


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

@EvanLarsen ทำไม หากการIValidate<>ใช้งานมีการขึ้นต่อกันให้ใส่การขึ้นต่อกันเหล่านี้เป็นพารามิเตอร์ไปยังวิธีการของโรงงาน
jhominal

5

คุณสามารถประกาศCreateBookCommandValidatorเป็นinternalใช้InternalsVisibleToAttributeเพื่อที่จะทำให้มันมองเห็นได้ด้วยBookStore.Couplerการชุมนุม สิ่งนี้มักจะช่วยได้เมื่อทำการทดสอบหน่วย


ฉันไม่รู้เลยว่าการดำรงอยู่ของคุณสมบัตินั้น ขอบคุณ
Evan Larsen

4

คุณสามารถทำให้เป็นภายในและใช้InternalVisibleToAttribute msdn.linkเพื่อให้กรอบการทดสอบ / โครงการของคุณสามารถเข้าถึงได้

ผมมีปัญหาที่เกี่ยวข้องกัน -> การเชื่อมโยง

นี่คือลิงก์ไปยังคำถาม Stackoverflow อื่นเกี่ยวกับปัญหา:

และในที่สุดก็มีบทความบนเว็บ


ฉันไม่รู้เลยว่าการดำรงอยู่ของคุณสมบัตินั้น ขอบคุณ
Evan Larsen

1

ตัวเลือกอื่นคือการทำให้เป็นสาธารณะ แต่ใส่ไว้ในการชุมนุมอื่น

ดังนั้นโดยพื้นฐานแล้วคุณมีแอสเซมบลีของเซอร์วิสอินเตอร์เฟสแอสเซมบลีของบริการ implementations (ซึ่งอ้างอิงถึงอินเทอร์เฟซบริการ), แอสเซมบลีของผู้บริโภคบริการ (ซึ่งอ้างอิงถึงอินเทอร์เฟซบริการ) )

ฉันควรเครียดนี่ไม่ใช่วิธีที่เหมาะสมที่สุดเสมอไป แต่มันก็คุ้มค่าที่จะพิจารณา


นั่นจะเป็นการลบความเสี่ยงด้านความปลอดภัยของ sligth ที่จะทำให้มองเห็นภายในหรือไม่?
โยฮันเนส

1
@ โยฮัน: ความเสี่ยงด้านความปลอดภัย? หากคุณพึ่งพาตัวดัดแปลงการเข้าถึงเพื่อให้ความปลอดภัยแก่คุณคุณต้องกังวล คุณสามารถเข้าถึงวิธีใดก็ได้ผ่านการไตร่ตรอง แต่มันจะลบการเข้าถึงที่ง่าย / สนับสนุนให้กับ internals โดยการทำให้การใช้งานในการชุมนุมอื่นที่ไม่ได้อ้างอิง
pdr
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.