Menggunakan struct untuk memberlakukan validasi tipe bawaan


9

Umumnya objek domain memiliki properti yang dapat diwakili oleh tipe bawaan tetapi nilai validnya adalah subset dari nilai yang mungkin diwakili oleh tipe itu.

Dalam kasus ini, nilai dapat disimpan menggunakan tipe bawaan tetapi perlu untuk memastikan bahwa nilai selalu divalidasi pada titik masuk, jika tidak kita akhirnya akan bekerja dengan nilai yang tidak valid.

Salah satu cara untuk mengatasi ini adalah dengan menyimpan nilai sebagai kebiasaan structyang memiliki private readonlybidang dukungan tunggal dari tipe bawaan dan yang konstruktornya memvalidasi nilai yang diberikan. Kami kemudian dapat selalu yakin hanya menggunakan nilai yang divalidasi dengan menggunakan structtipe ini .

Kami juga dapat menyediakan operator cor dari dan ke tipe bawaan yang mendasarinya sehingga nilai-nilai dapat masuk dan keluar dengan mulus sebagai jenis yang mendasarinya.

Ambil contoh situasi di mana kita perlu mewakili nama objek domain, dan nilai yang valid adalah string apa pun yang panjangnya antara 1 dan 255 karakter. Kami dapat mewakili ini menggunakan struct berikut:

public struct ValidatedName : IEquatable<ValidatedName>
{
    private readonly string _value;

    private ValidatedName(string name)
    {
        _value = name;
    }

    public static bool IsValid(string name)
    {
        return !String.IsNullOrEmpty(name) && name.Length <= 255;
    }

    public bool Equals(ValidatedName other)
    {
        return _value == other._value;
    }

    public override bool Equals(object obj)
    {
        if (obj is ValidatedName)
        {
            return Equals((ValidatedName)obj);
        }
        return false;
    }

    public static implicit operator string(ValidatedName x)
    {
        return x.ToString();
    }

    public static explicit operator ValidatedName(string x)
    {
        if (IsValid(x))
        {
            return new ValidatedName(x);
        }
        throw new InvalidCastException();
    }

    public static bool operator ==(ValidatedName x, ValidatedName y)
    {
        return x.Equals(y);
    }

    public static bool operator !=(ValidatedName x, ValidatedName y)
    {
        return !x.Equals(y);
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value;
    }
}

Contoh ini memperlihatkan stringpemain karena implicithal ini tidak akan pernah gagal tetapi dari stringpemain explicitkarena hal ini akan menghasilkan nilai yang tidak valid, tetapi tentu saja ini bisa berupa implicitatau explicit.

Perhatikan juga bahwa seseorang hanya dapat menginisialisasi struct ini dengan cara pemain dari string, tetapi orang dapat menguji apakah pemain tersebut akan gagal di muka menggunakan IsValid staticmetode ini.

Ini tampaknya menjadi pola yang baik untuk menegakkan validasi nilai domain yang dapat diwakili oleh tipe sederhana, tetapi saya tidak melihatnya sering atau disarankan dan saya tertarik mengapa.

Jadi pertanyaan saya adalah: apa yang Anda lihat sebagai keuntungan dan kerugian dari menggunakan pola ini, dan mengapa?

Jika Anda merasa ini adalah pola yang buruk, saya ingin memahami mengapa dan apa yang Anda rasakan adalah alternatif terbaik.

NB Saya awalnya mengajukan pertanyaan ini pada Stack Overflow tapi itu ditunda terutama berdasarkan opini (ironisnya subjektif sendiri) - mudah-mudahan bisa menikmati lebih banyak kesuksesan di sini.

Di atas adalah teks asli, di bawah beberapa pemikiran lagi, sebagian sebagai jawaban atas jawaban yang diterima di sana sebelum ditunda:

  • Salah satu poin utama yang dibuat oleh jawaban adalah sekitar jumlah kode pelat ketel yang diperlukan untuk pola di atas, terutama ketika banyak jenis seperti itu diperlukan. Namun dalam mempertahankan pola, ini sebagian besar bisa secara otomatis menggunakan template dan sebenarnya bagi saya sepertinya tidak terlalu buruk, tapi itu hanya pendapat saya.
  • Dari sudut pandang konseptual, apakah tidak aneh ketika bekerja dengan bahasa yang sangat diketik seperti C # untuk hanya menerapkan prinsip yang sangat diketik untuk nilai komposit, daripada memperluasnya ke nilai-nilai yang dapat diwakili oleh turunan dari tipe bawaan?

Anda bisa membuat versi templated yang menggunakan lambda bool (T)
ratchet freak

Jawaban:


4

Ini cukup umum dalam bahasa gaya-ML seperti Standar ML / OCaml / F # / Haskell di mana lebih mudah untuk membuat jenis pembungkus. Ini memberi Anda dua manfaat:

  • Ini memungkinkan sepotong kode untuk menegakkan bahwa string telah mengalami validasi, tanpa harus mengurus validasi itu sendiri.
  • Ini memungkinkan Anda untuk melokalkan kode validasi di satu tempat. Jika ValidatedNamepernah berisi nilai yang tidak valid, Anda tahu kesalahan ada di dalam IsValidmetode.

Jika Anda mendapatkan IsValidmetode yang benar, Anda memiliki jaminan bahwa fungsi apa pun yang menerima ValidatedNamesebenarnya menerima nama yang divalidasi.

Jika Anda perlu melakukan manipulasi string, Anda dapat menambahkan metode publik yang menerima fungsi yang mengambil String (nilai ValidatedName) dan mengembalikan String (nilai baru) dan memvalidasi hasil penerapan fungsi. Itu menghilangkan pelat untuk mendapatkan nilai String yang mendasarinya dan membungkusnya kembali.

Penggunaan terkait untuk nilai pembungkus adalah untuk melacak asalnya. Misalnya API OS berbasis C terkadang memberikan pegangan untuk sumber daya sebagai bilangan bulat. Anda dapat membungkus OS API untuk menggunakan Handlestruktur dan hanya menyediakan akses ke konstruktor ke bagian kode tersebut. Jika kode yang menghasilkan huruf Handles benar, maka hanya pegangan yang valid yang akan digunakan.


1

apa yang Anda lihat sebagai keuntungan dan kerugian dari menggunakan pola ini, dan mengapa?

Baik :

  • Itu mandiri. Terlalu banyak bit validasi memiliki sulur menjangkau ke tempat yang berbeda.
  • Ini membantu dokumentasi diri. Melihat metode take a ValidatedStringmembuatnya lebih jelas tentang semantik panggilan.
  • Ini membantu membatasi validasi ke satu tempat daripada perlu diduplikasi di seluruh metode publik.

Buruk :

  • Tipuan casting disembunyikan. Ini bukan C # yang idiomatis, sehingga dapat menyebabkan kebingungan saat membaca kode.
  • Itu melempar. Memiliki string yang tidak memenuhi validasi bukanlah skenario yang luar biasa. Melakukan IsValidsebelum para pemeran agak tidak menyenangkan.
  • Tidak dapat memberi tahu Anda mengapa ada sesuatu yang tidak valid.
  • Defaultnya ValidatedStringtidak valid / divalidasi.

Saya telah melihat hal semacam ini lebih sering dengan Userdan AuthenticatedUserhal-hal semacam itu, di mana objeknya benar-benar berubah. Ini bisa menjadi pendekatan yang baik, meskipun tampaknya tidak pada tempatnya di C #.


1
Terima kasih, saya pikir "con" keempat Anda adalah argumen yang paling menarik untuk menentangnya - menggunakan default atau array jenis ini dapat memberi Anda nilai yang tidak valid (tergantung pada apakah string nol / null adalah nilai yang valid tentu saja). Ini adalah (saya pikir) satu-satunya dua cara untuk mendapatkan nilai yang tidak valid. Tetapi kemudian, jika kita TIDAK MENGGUNAKAN pola ini, kedua hal ini masih akan memberi kita nilai yang tidak valid, tetapi saya kira setidaknya kita akan tahu bahwa mereka perlu divalidasi. Jadi ini berpotensi membatalkan pendekatan di mana nilai default tipe yang mendasarinya tidak valid untuk tipe kami.
gmoody1979

Semua yang kontra adalah masalah implementasi daripada masalah dengan konsep. Selain itu saya menemukan "pengecualian harus luar biasa" konsep yang kabur dan tidak jelas. Pendekatan yang paling pragmatis adalah menyediakan metode berbasis pengecualian dan non-pengecualian serta membiarkan pemanggil memilih.
Doval

@Doval Saya setuju kecuali sebagaimana tercantum dalam komentar saya yang lain. Inti dari pola ini adalah untuk mengetahui dengan pasti bahwa jika kita memiliki ValidatedName, itu harus valid. Ini rusak jika nilai default dari tipe yang mendasarinya juga bukan nilai yang valid dari tipe domain. Ini tentu saja tergantung pada domain tetapi lebih cenderung menjadi kasus (saya akan berpikir) untuk tipe berbasis string daripada untuk tipe numerik. Pola ini berfungsi paling baik di mana default dari tipe yang mendasarinya juga cocok sebagai default dari tipe domain.
gmoody1979

@Doval - Saya biasanya setuju. Konsepnya sendiri baik-baik saja, tetapi secara efektif mencoba menyemir tipe penyempurnaan menjadi bahasa yang tidak mendukungnya. Akan selalu ada masalah implementasi.
Telastyn

Karena itu, saya kira Anda bisa memeriksa nilai default pada pemain "outbound" dan di tempat lain yang diperlukan dalam metode struct dan melempar jika tidak diinisialisasi, tetapi itu mulai menjadi berantakan.
gmoody1979

0

Cara Anda cukup berat dan intensif. Saya biasanya mendefinisikan entitas domain seperti:

public class Institution
{
    private Institution() { }

    public Institution(int organizationId, string name)
    {
        OrganizationId = organizationId;            
        Name = name;
        ReplicationKey = Guid.NewGuid();

        new InstitutionValidator().ValidateAndThrow(this);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }        
    public virtual ICollection<Department> Departments { get; private set; }

    ... other properties    

    public Department AddDepartment(string name)
    {
        var department = new Department(Id, name);
        if (Departments == null) Departments = new List<Department>();
        Departments.Add(department);            
        return department;
    }

    ... other domain operations
}

Di konstruktor entitas, validasi dipicu menggunakan FluentValidation.NET, untuk memastikan Anda tidak dapat membuat entitas dengan keadaan tidak valid. Perhatikan bahwa semua properti hanya dapat dibaca - Anda hanya dapat mengaturnya melalui konstruktor atau operasi domain khusus.

Validasi entitas ini adalah kelas yang terpisah:

public class InstitutionValidator : AbstractValidator<Institution>
{
    public InstitutionValidator()
    {
        RuleFor(institution => institution.Name).NotNull().Length(1, 100).WithLocalizedName(() =>   Prim.Mgp.Infrastructure.Resources.GlobalResources.InstitutionName);       
        RuleFor(institution => institution.OrganizationId).GreaterThan(0);
        RuleFor(institution => institution.ReplicationKey).NotNull().NotEqual(Guid.Empty);
    }  
}

Validator ini juga dapat dengan mudah digunakan kembali, dan Anda menulis lebih sedikit kode boilerplate. Dan keuntungan lain adalah itu bisa dibaca.


Akankah pemilih yang rendah hati menjelaskan mengapa jawaban saya tidak dipilih?
L-Four

Pertanyaannya adalah tentang struct untuk membatasi tipe nilai, dan Anda beralih ke kelas tanpa menjelaskan MENGAPA. (Bukan downvoter, hanya membuat saran.)
DougM

Saya menjelaskan mengapa saya menemukan ini alternatif yang lebih baik, dan ini adalah salah satu pertanyaannya. Terima kasih balasannya.
L-Four

0

Saya suka pendekatan ini untuk tipe nilai. Konsepnya bagus, tapi saya punya beberapa saran / keluhan tentang implementasinya.

Casting : Saya tidak suka menggunakan casting dalam hal ini. Pemeran dari-string eksplisit tidak menjadi masalah, tetapi tidak ada banyak perbedaan antara (ValidatedName)nameValuedan yang baru ValidatedName(nameValue). Jadi sepertinya tidak perlu. Cast ke-string implisit adalah masalah terburuk. Saya pikir bahwa mendapatkan nilai string yang sebenarnya harus lebih eksplisit, karena mungkin secara tidak sengaja ditugaskan ke string dan kompiler tidak akan memperingatkan Anda tentang kemungkinan "kehilangan presisi". Kehilangan presisi seperti ini harus eksplisit.

ToString : Saya lebih suka menggunakan ToStringoverload hanya untuk keperluan debugging. Dan saya tidak berpikir mengembalikan nilai mentah karena itu adalah ide yang bagus. Ini adalah masalah yang sama dengan konversi implisit ke-string. Mendapatkan nilai internal harus operasi eksplisit. Saya percaya Anda mencoba untuk membuat struktur berperilaku sebagai string normal ke kode luar, tetapi saya pikir dalam melakukannya, Anda kehilangan beberapa nilai yang Anda dapatkan dari menerapkan jenis ini.

Equals dan GetHashCode : Structs menggunakan kesetaraan struktural secara default. Jadi Anda Equalsdan GetHashCodemenduplikasi perilaku default ini. Anda dapat menghapusnya dan itu akan menjadi hal yang hampir sama.


Casting: Secara semantis ini terasa lebih bagi saya seperti transformasi string ke ValidatedName daripada penciptaan ValidatedName baru: kami mengidentifikasi string yang ada sebagai ValidatedName. Oleh karena itu bagi saya para pemain tampaknya lebih benar secara semantik. Setuju ada sedikit perbedaan dalam pengetikan (dari jari-jari pada variasi keyboard). Saya tidak setuju pada pemain to-string: ValidatedName adalah bagian dari string, jadi tidak akan pernah ada kehilangan presisi ...
gmoody1979

ToString: Saya tidak setuju. Bagi saya ToString adalah metode yang sangat valid untuk digunakan di luar skenario debugging, dengan asumsi itu sesuai dengan persyaratan. Juga dalam situasi ini di mana suatu jenis adalah himpunan bagian dari jenis lain, saya pikir masuk akal untuk membuat kemampuan mengubah dari subset ke super-set semudah mungkin, sehingga jika pengguna menginginkan, mereka hampir dapat memperlakukannya sebagai dari tipe super-set, yaitu string ...
gmoody1979

Equals dan GetHashCode: Ya struct menggunakan kesetaraan struktural, tetapi dalam hal ini yang membandingkan referensi string, bukan nilai string. Karenanya kita perlu mengesampingkan Persamaan. Saya setuju bahwa ini tidak akan diperlukan jika tipe yang mendasarinya adalah tipe nilai. Dari pemahaman saya tentang implementasi GetHashCode default untuk tipe nilai (yang cukup terbatas), ini akan memberikan nilai yang sama tetapi akan lebih berkinerja. Saya benar-benar harus menguji apakah itu yang terjadi tetapi itu sedikit masalah sampingan ke poin utama dari pertanyaan. Terima kasih atas jawaban Anda :-).
gmoody1979

@ gmoody1979 Struktur dibandingkan menggunakan Persamaan pada setiap bidang secara default. Seharusnya tidak menjadi masalah dengan string. Sama dengan GetHashCode. Adapun struktur menjadi bagian dari string. Saya suka menganggap tipe itu sebagai jaring pengaman. Saya tidak ingin bekerja dengan ValidatedName dan kemudian secara tidak sengaja tergelincir menggunakan string. Saya lebih suka jika kompiler membuat saya secara eksplisit menentukan bahwa saya sekarang ingin bekerja dengan data yang tidak dicentang.
Euforia

Maaf ya, poin bagus tentang Persamaan. Meskipun override harus berkinerja lebih baik mengingat perilaku default perlu menggunakan refleksi untuk melakukan perbandingan. Casting: ya mungkin argumen yang bagus untuk membuatnya menjadi pemain yang eksplisit.
gmoody1979
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.