Bisakah saya menggunakan Injeksi Ketergantungan tanpa melanggar Enkapsulasi?


15

Inilah Solusi dan proyek saya:

  • BookStore (solusi)
    • BookStore.Coupler (proyek)
      • Bootstrapper.cs
    • BookStore.Domain (proyek)
      • BuatBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (proyek)
      • BuatBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (proyek)
      • Global.asax
    • BookStore.BatchProcesses (proyek)
      • 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<>));
    }
}

BuatBookCommandValidator.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);
}

BuatBookCommandHandler.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

Maaf untuk pengaturan panjang untuk masalah ini, saya tidak memiliki cara yang lebih baik untuk menjelaskan hal ini selain merinci masalah saya yang sebenarnya.

Saya tidak ingin membuat CreateBookCommandValidator saya public. Saya lebih suka internaltetapi jika saya membuatnya internalmaka saya tidak akan bisa mendaftarkannya dengan Container DI saya. Alasan saya ingin ini bersifat internal adalah karena satu-satunya proyek yang seharusnya memiliki gagasan tentang implementasi <> IValidate saya adalah dalam proyek BookStore.Domain. Setiap proyek lain hanya perlu mengkonsumsi IValidator <> dan CompositeValidator harus diselesaikan yang akan memenuhi semua validasi.

Bagaimana saya bisa menggunakan Injeksi Ketergantungan tanpa melanggar enkapsulasi? Atau apakah saya salah tentang semua ini?


Just a nitpick: Apa yang Anda gunakan bukan pola perintah yang benar, jadi menyebutnya perintah mungkin informasi yang salah. Juga, CreateBookCommandHandler sepertinya melanggar LSP: apa yang akan terjadi, jika Anda melewatkan objek, yang berasal dari CreateBookCommand? Dan saya pikir apa yang Anda lakukan di sini adalah antipattern Anemic Domain Model. Hal-hal seperti tabungan harus ada di dalam domain dan validasi harus menjadi bagian dari entitas.
Euforia

1
@ Euphoric: Itu benar. Ini bukan pola perintah . Faktanya, OP mengikuti pola yang berbeda: pola perintah / penangan .
Steven

Ada begitu banyak jawaban yang baik saya berharap saya bisa menandai lebih banyak sebagai jawabannya. Terima kasih semuanya atas bantuan Anda.
Evan Larsen

@ Euphoric, setelah memikirkan kembali tata letak proyek saya pikir CommandHandlers harus berada di Domain. Tidak yakin mengapa saya menempatkan mereka di proyek Infrastruktur. Terima kasih.
Evan Larsen

Jawaban:


11

Membuat CreateBookCommandValidatorpublik tidak melanggar enkapsulasi, sejak

Enkapsulasi digunakan untuk menyembunyikan nilai-nilai atau keadaan objek data terstruktur di dalam kelas, mencegah akses langsung ke pihak yang tidak berwenang ke mereka ( wikipedia )

Anda CreateBookCommandValidatortidak mengizinkan akses ke anggota datanya (saat ini sepertinya tidak ada) sehingga tidak melanggar enkapsulasi.

Membuat kelas ini menjadi publik tidak melanggar prinsip lain apa pun (seperti prinsip SOLID ), karena:

  • Kelas itu memiliki tanggung jawab tunggal yang jelas dan karenanya mengikuti Prinsip Tanggung Jawab Tunggal.
  • Menambahkan validator baru ke sistem dapat dilakukan tanpa mengubah satu baris kode dan karena itu Anda mengikuti Prinsip Terbuka / Tertutup.
  • Antarmuka IValidator <T> yang diterapkan kelas ini sempit (hanya memiliki satu anggota) dan mengikuti Prinsip Segregasi Antarmuka.
  • Konsumen Anda hanya bergantung pada antarmuka IValidator <T> tersebut dan karenanya mengikuti Prinsip Ketergantungan Pembalikan.

Anda hanya dapat membuat CreateBookCommandValidator internal jika kelas tidak dikonsumsi langsung dari luar perpustakaan, tetapi ini jarang terjadi, karena unit test Anda adalah konsumen penting kelas ini (dan hampir setiap kelas di sistem Anda).

Meskipun Anda dapat membuat kelas internal dan menggunakan [InternalsVisibleTo] untuk memungkinkan proyek uji unit mengakses internal proyek Anda, mengapa repot-repot?

Alasan paling penting untuk membuat kelas internal adalah untuk mencegah pihak eksternal (bahwa Anda tidak memiliki kendali atas) untuk mengambil ketergantungan pada kelas seperti itu, karena itu akan mencegah Anda membuat perubahan di masa depan ke kelas itu tanpa merusak apa pun. Dengan kata lain, ini hanya berlaku ketika Anda membuat perpustakaan yang dapat digunakan kembali (seperti perpustakaan injeksi ketergantungan). Faktanya, Simple Injector berisi hal-hal internal dan proyek uji unitnya menguji internal ini.

Namun, jika Anda tidak membuat proyek yang dapat digunakan kembali, masalah ini tidak ada. Tidak ada, karena Anda dapat mengubah proyek yang bergantung padanya, dan pengembang lain di tim Anda harus mengikuti panduan Anda. Dan satu pedoman sederhana akan dilakukan: Program ke abstraksi; bukan implementasi (Prinsip Pembalikan Ketergantungan).

Singkat cerita, jangan buat kelas ini internal kecuali Anda menulis perpustakaan yang dapat digunakan kembali.

Tetapi jika Anda masih ingin menjadikan kelas ini internal, Anda masih dapat mendaftarkannya dengan Simple Injector tanpa masalah seperti ini:

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

Satu-satunya hal yang perlu dipastikan adalah bahwa semua validator Anda memiliki konstruktor publik, meskipun mereka internal. Jika Anda benar-benar ingin tipe Anda memiliki konstruktor internal (tidak benar-benar tahu mengapa Anda menginginkannya), Anda dapat mengesampingkan Perilaku Resolusi Pembuat .

MEMPERBARUI

Sejak Simple Injector v2.6 , perilaku default RegisterManyForOpenGenericadalah untuk mendaftarkan tipe publik dan internal. Jadi penyediaan AccessibilityOption.AllTypessekarang berlebihan dan pernyataan berikut akan mendaftarkan tipe publik dan internal:

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

8

Bukan masalah besar bahwa CreateBookCommandValidatorkelas itu publik.

Jika Anda perlu membuat instance di luar perpustakaan yang mendefinisikannya, itu adalah pendekatan yang cukup alami untuk mengekspos kelas publik, dan mengandalkan klien yang hanya menggunakan kelas itu sebagai implementasi dari IValidate<CreateBookCommand> . (Mengekspos suatu tipe tidak berarti enkapsulasi rusak, itu hanya membuat klien lebih mudah untuk memecah enkapsulasi).

Kalau tidak, jika Anda benar-benar ingin memaksa klien untuk tidak tahu tentang kelas, Anda juga dapat menggunakan metode pabrik statis publik alih-alih mengekspos kelas, misalnya:

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

Adapun mendaftar di wadah DI Anda, semua wadah DI yang saya tahu memungkinkan konstruksi dengan menggunakan metode pabrik statis.


Ya, terima kasih .. Awalnya saya memikirkan hal yang sama sebelum saya membuat posting ini. Saya berpikir untuk membuat kelas Factory yang akan mengembalikan kembali implementasi <validasi IV> yang sesuai, tetapi jika salah satu implementasi << IValidate <> memiliki dependensi, itu mungkin akan menjadi sangat berbulu dengan cepat.
Evan Larsen

@ EvanLarsen Kenapa? Jika IValidate<>implementasi memiliki dependensi, maka taruh dependensi ini sebagai parameter ke metode pabrik.
jhominal



1

Pilihan lain adalah mempublikasikannya tetapi menempatkannya di majelis lain.

Jadi pada dasarnya, Anda memiliki unit antarmuka layanan, unit implementasi layanan (yang mereferensikan antarmuka layanan), unit layanan konsumen (yang mereferensikan antarmuka layanan), dan unit pendaftar IOC (yang mereferensikan antarmuka layanan dan implementasi layanan untuk menghubungkan keduanya. ).

Saya harus menekankan, ini tidak selalu merupakan solusi yang paling tepat, tetapi ini patut dipertimbangkan.


Apakah itu menghilangkan risiko keamanan sligth membuat internal terlihat?
Johannes

1
@ Johnannes: Risiko keamanan? Jika Anda mengandalkan pengubah akses untuk memberi Anda keamanan, Anda perlu khawatir. Anda bisa mendapatkan akses ke metode apa pun melalui refleksi. Tapi itu menghilangkan akses mudah / didorong ke internal dengan menempatkan implementasi di majelis lain yang tidak dirujuk.
pdr
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.