Pass ID atau Object?

38

Saat memberikan metode logika bisnis untuk mendapatkan entitas domain, haruskah parameter menerima objek atau ID? Sebagai contoh, harus kita lakukan ini:

public Foo GetItem(int id) {}

atau ini:

public Foo GetItem(Foo foo) {}

Saya percaya pada melewatkan objek di sekitar, secara keseluruhan, tetapi bagaimana dengan kasus ini di mana kita mendapatkan objek dan kita hanya tahu ID? Haruskah penelepon membuat Foo kosong dan mengatur ID, atau haruskah itu hanya meneruskan ID ke metode? Karena Foo yang masuk akan kosong, kecuali untuk ID, saya tidak melihat manfaat dari pemanggil harus membuat Foo dan mengatur ID-nya ketika hanya bisa mengirim ID ke metode GetItem ().

Bob Horn
sumber

Jawaban:

42

Hanya satu bidang yang digunakan untuk pencarian.

Penelepon tidak memiliki Foo, ia mencoba mendapatkannya. Tentu, Anda dapat membuat sementara Foodengan semua bidang lainnya dibiarkan kosong, tetapi itu hanya berfungsi untuk struktur data sepele. Sebagian besar objek memiliki invarian yang akan dilanggar oleh pendekatan objek yang sebagian besar kosong, jadi hindarilah.

Ben Voigt
sumber
Terima kasih. Saya suka jawaban ini dengan poin # 2 Amiram dalam jawabannya.
Bob Horn
3
Ini sepertinya logis. Tapi kinerja bijaksana, saya telah lari ke daerah-daerah di mana penelepon mungkin memiliki objek dan mungkin tidak. Hanya lewat id saja yang bisa mengarah ke objek yang sedang dibaca dari database dua kali. Apakah ini hanya hit kinerja yang dapat diterima? Atau apakah Anda memberikan kemungkinan id atau objek untuk diteruskan?
computrius
Saya mengambil aturan 'tidak pernah lulus objek' ini dengan sebutir garam akhir-akhir ini. Itu hanya tergantung pada konteks / skenario Anda.
Bruno
12

Apakah ini akan melewati kawat (serial / deserialized) kapan saja sekarang atau di masa depan? Pilih tipe ID tunggal daripada objek penuh yang tahu-bagaimana-besar.

Jika Anda mencari tipe-keamanan ID untuk entitasnya, maka ada solusi kode juga. Beri tahu saya jika Anda membutuhkan contoh.

Sunting: memperluas jenis-keamanan ID:

Jadi, mari kita ambil metode Anda:

public Foo GetItem(int id) {}

Kami hanya berharap bahwa bilangan bulat idyang diteruskan adalah untuk Fooobjek. Seseorang dapat menyalahgunakannya dan memasukkan BarID integer suatu objek atau bahkan hanya dengan mengetik tangan 812341. Ini bukan tipe aman untuk Foo. Kedua, bahkan jika Anda menggunakan pass Fooversi objek , saya yakin Foomemiliki bidang ID intyang dapat dimodifikasi seseorang. Dan terakhir, Anda tidak bisa menggunakan metode overloading jika ini ada di kelas bersama karena hanya tipe pengembalian bervariasi. Mari kita tulis ulang metode ini sedikit agar terlihat aman di C #:

public Foo GetItem(IntId<Foo> id) {}

Jadi saya telah memperkenalkan sebuah kelas bernama IntIdyang memiliki bagian generik untuknya. Dalam kasus khusus ini, saya ingin intyang terkait dengan Foosaja. Saya tidak bisa begitu saja mengenakan pakaian telanjang intdan saya tidak bisa memberikannya secara IntId<Bar>tidak sengaja. Jadi di bawah ini adalah bagaimana saya telah menulis pengidentifikasi tipe-aman ini. Apakah mengambil catatan bahwa manipulasi yang mendasari sebenarnya intadalah hanya di lapisan akses data Anda. Apa pun di atas yang hanya melihat tipe kuat dan tidak memiliki (langsung) akses ke intID internalnya . Seharusnya tidak ada alasan untuk itu.

Antarmuka IModelId.cs:

namespace GenericIdentifiers
{
    using System.Runtime.Serialization;
    using System.ServiceModel;

    /// <summary>
    /// Defines an interface for an object's unique key in order to abstract out the underlying key
    /// generation/maintenance mechanism.
    /// </summary>
    /// <typeparam name="T">The type the key is representing.</typeparam>
    [ServiceContract]
    public interface IModelId<T> where T : class
    {
        /// <summary>
        /// Gets a string representation of the domain the model originated from.
        /// </summary>
        /// <value>The origin.</value>
        [DataMember]
        string Origin
        {
            [OperationContract]get;
        }

        /// <summary>
        /// The model instance identifier for the model object that this <see cref="IModelId{T}"/> refers to.
        /// Typically, this is a database key, file name, or some other unique identifier.
        /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
        /// </summary>
        /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
        /// <returns>The unique key as the data type specified.</returns>
        [OperationContract]
        TKeyDataType GetKey<TKeyDataType>();

        /// <summary>
        /// Performs an equality check on the two model identifiers and returns <c>true</c> if they are equal; otherwise
        /// <c>false</c> is returned.  All implementations must also override the equal operator.
        /// </summary>
        /// <param name="obj">The identifier to compare against.</param>
        /// <returns><c>true</c> if the identifiers are equal; otherwise <c>false</c> is returned.</returns>
        [OperationContract]
        bool Equals(IModelId<T> obj);
    }
}

Kelas dasar ModelIdBase.cs:

namespace GenericIdentifiers
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.Serialization;

    /// <summary>
    /// Represents an object's unique key in order to abstract out the underlying key generation/maintenance mechanism.
    /// </summary>
    /// <typeparam name="T">The type the key is representing.</typeparam>
    [DataContract(IsReference = true)]
    [KnownType("GetKnownTypes")]
    public abstract class ModelIdBase<T> : IModelId<T> where T : class
    {
        /// <summary>
        /// Gets a string representation of the domain the model originated from.
        /// </summary>
        [DataMember]
        public string Origin
        {
            get;

            internal set;
        }

        /// <summary>
        /// The model instance identifier for the model object that this <see cref="ModelIdBase{T}"/> refers to.
        /// Typically, this is a database key, file name, or some other unique identifier.
        /// </summary>
        /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
        /// <returns>The unique key as the data type specified.</returns>
        public abstract TKeyDataType GetKey<TKeyDataType>();

        /// <summary>
        /// Performs an equality check on the two model identifiers and returns <c>true</c> if they are equal;
        /// otherwise <c>false</c> is returned. All implementations must also override the equal operator.
        /// </summary>
        /// <param name="obj">The identifier to compare against.</param>
        /// <returns>
        ///   <c>true</c> if the identifiers are equal; otherwise <c>false</c> is returned.
        /// </returns>
        public abstract bool Equals(IModelId<T> obj);

        protected static IEnumerable<Type> GetKnownTypes()
        {
            return new[] { typeof(IntId<T>), typeof(GuidId<T>) };
        }
    }
}

IntId.cs:

namespace GenericIdentifiers
{
    // System namespaces
    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.Runtime.Serialization;

    /// <summary>
    /// Represents an abstraction of the database key for a Model Identifier.
    /// </summary>
    /// <typeparam name="T">The expected owner data type for this identifier.</typeparam>
    [DebuggerDisplay("Origin={Origin}, Integer Identifier={Id}")]
    [DataContract(IsReference = true)]
    public sealed class IntId<T> : ModelIdBase<T> where T : class
    {
        /// <summary>
        /// Gets or sets the unique ID.
        /// </summary>
        /// <value>The unique ID.</value>
        [DataMember]
        internal int Id
        {
            get;

            set;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="intIdentifier1">The first Model Identifier to compare.</param>
        /// <param name="intIdentifier2">The second Model Identifier to compare.</param>
        /// <returns>
        ///   <c>true</c> if the instances are equal; otherwise <c>false</c> is returned.
        /// </returns>
        public static bool operator ==(IntId<T> intIdentifier1, IntId<T> intIdentifier2)
        {
            return object.Equals(intIdentifier1, intIdentifier2);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="intIdentifier1">The first Model Identifier to compare.</param>
        /// <param name="intIdentifier2">The second Model Identifier to compare.</param>
        /// <returns>
        ///   <c>true</c> if the instances are equal; otherwise <c>false</c> is returned.
        /// </returns>
        public static bool operator !=(IntId<T> intIdentifier1, IntId<T> intIdentifier2)
        {
            return !object.Equals(intIdentifier1, intIdentifier2);
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="IntId{T}"/> to <see cref="System.Int32"/>.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator int(IntId<T> id)
        {
            return id == null ? int.MinValue : id.GetKey<int>();
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="System.Int32"/> to <see cref="IntId{T}"/>.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator IntId<T>(int id)
        {
            return new IntId<T> { Id = id };
        }

        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current
        /// <see cref="T:System.Object"/>.
        /// </summary>
        /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current
        /// <see cref="T:System.Object"/>.</param>
        /// <returns>true if the specified <see cref="T:System.Object"/> is equal to the current
        /// <see cref="T:System.Object"/>; otherwise, false.</returns>
        /// <exception cref="T:System.NullReferenceException">The <paramref name="obj"/> parameter is null.</exception>
        public override bool Equals(object obj)
        {
            return this.Equals(obj as IModelId<T>);
        }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"/>.
        /// </returns>
        public override int GetHashCode()
        {
            unchecked
            {
                var hash = 17;

                hash = (23 * hash) + (this.Origin == null ? 0 : this.Origin.GetHashCode());
                return (31 * hash) + this.GetKey<int>().GetHashCode();
            }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return this.Origin + ":" + this.GetKey<int>().ToString(CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Performs an equality check on the two model identifiers and returns <c>true</c> if they are equal;
        /// otherwise <c>false</c> is returned.  All implementations must also override the equal operator.
        /// </summary>
        /// <param name="obj">The identifier to compare against.</param>
        /// <returns>
        ///   <c>true</c> if the identifiers are equal; otherwise <c>false</c> is returned.
        /// </returns>
        public override bool Equals(IModelId<T> obj)
        {
            if (obj == null)
            {
                return false;
            }

            return (obj.Origin == this.Origin) && (obj.GetKey<int>() == this.GetKey<int>());
        }

        /// <summary>
        /// The model instance identifier for the model object that this <see cref="ModelIdBase{T}"/> refers to.
        /// Typically, this is a database key, file name, or some other unique identifier.
        /// </summary>
        /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
        /// <returns>The unique key as the data type specified.</returns>
        public override TKeyDataType GetKey<TKeyDataType>()
        {
            return (TKeyDataType)Convert.ChangeType(this.Id, typeof(TKeyDataType), CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Generates an object from its string representation.
        /// </summary>
        /// <param name="value">The value of the model's type.</param>
        /// <returns>A new instance of this class as it's interface containing the value from the string.</returns>
        internal static ModelIdBase<T> FromString(string value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            int id;
            var originAndId = value.Split(new[] { ":" }, StringSplitOptions.None);

            if (originAndId.Length != 2)
            {
                throw new ArgumentOutOfRangeException("value", "value must be in the format of Origin:Identifier");
            }

            return int.TryParse(originAndId[1], NumberStyles.None, CultureInfo.InvariantCulture, out id)
                ? new IntId<T> { Id = id, Origin = originAndId[0] }
                : null;
        }
    }
}

dan, demi kelengkapan basis kode saya, saya juga menulis satu untuk entitas GUID, GuidId.cs:

namespace GenericIdentifiers
{
    // System namespaces
    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.Runtime.Serialization;

    /// <summary>
    /// Represents an abstraction of the database key for a Model Identifier.
    /// </summary>
    /// <typeparam name="T">The expected owner data type for this identifier.</typeparam>
    [DebuggerDisplay("Origin={Origin}, GUID={Id}")]
    [DataContract(IsReference = true)]
    public sealed class GuidId<T> : ModelIdBase<T> where T : class
    {
        /// <summary>
        /// Gets or sets the unique ID.
        /// </summary>
        /// <value>The unique ID.</value>
        [DataMember]
        internal Guid Id
        {
            get;

            set;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="guidIdentifier1">The first Model Identifier to compare.</param>
        /// <param name="guidIdentifier2">The second Model Identifier to compare.</param>
        /// <returns>
        ///   <c>true</c> if the instances are equal; otherwise <c>false</c> is returned.
        /// </returns>
        public static bool operator ==(GuidId<T> guidIdentifier1, GuidId<T> guidIdentifier2)
        {
            return object.Equals(guidIdentifier1, guidIdentifier2);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="guidIdentifier1">The first Model Identifier to compare.</param>
        /// <param name="guidIdentifier2">The second Model Identifier to compare.</param>
        /// <returns>
        ///   <c>true</c> if the instances are equal; otherwise <c>false</c> is returned.
        /// </returns>
        public static bool operator !=(GuidId<T> guidIdentifier1, GuidId<T> guidIdentifier2)
        {
            return !object.Equals(guidIdentifier1, guidIdentifier2);
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="GuidId{T}"/> to <see cref="System.Guid"/>.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator Guid(GuidId<T> id)
        {
            return id == null ? Guid.Empty : id.GetKey<Guid>();
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="System.Guid"/> to <see cref="GuidId{T}"/>.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator GuidId<T>(Guid id)
        {
            return new GuidId<T> { Id = id };
        }

        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current
        /// <see cref="T:System.Object"/>.
        /// </summary>
        /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current
        /// <see cref="T:System.Object"/>.</param>
        /// <returns>true if the specified <see cref="T:System.Object"/> is equal to the current
        /// <see cref="T:System.Object"/>; otherwise, false.</returns>
        /// <exception cref="T:System.NullReferenceException">The <paramref name="obj"/> parameter is null.</exception>
        public override bool Equals(object obj)
        {
            return this.Equals(obj as IModelId<T>);
        }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"/>.
        /// </returns>
        public override int GetHashCode()
        {
            unchecked
            {
                var hash = 17;

                hash = (23 * hash) + (this.Origin == null ? 0 : this.Origin.GetHashCode());
                return (31 * hash) + this.GetKey<Guid>().GetHashCode();
            }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return this.Origin + ":" + this.GetKey<Guid>();
        }

        /// <summary>
        /// Performs an equality check on the two model identifiers and returns <c>true</c> if they are equal;
        /// otherwise <c>false</c> is returned.  All implementations must also override the equal operator.
        /// </summary>
        /// <param name="obj">The identifier to compare against.</param>
        /// <returns>
        ///   <c>true</c> if the identifiers are equal; otherwise <c>false</c> is returned.
        /// </returns>
        public override bool Equals(IModelId<T> obj)
        {
            if (obj == null)
            {
                return false;
            }

            return (obj.Origin == this.Origin) && (obj.GetKey<Guid>() == this.GetKey<Guid>());
        }

        /// <summary>
        /// The model instance identifier for the model object that this <see cref="ModelIdBase{T}"/> refers to.
        /// Typically, this is a database key, file name, or some other unique identifier.
        /// </summary>
        /// <typeparam name="TKeyDataType">The expected data type of the identifier.</typeparam>
        /// <returns>The unique key as the data type specified.</returns>
        public override TKeyDataType GetKey<TKeyDataType>()
        {
            return (TKeyDataType)Convert.ChangeType(this.Id, typeof(TKeyDataType), CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Generates an object from its string representation.
        /// </summary>
        /// <param name="value">The value of the model's type.</param>
        /// <returns>A new instance of this class as it's interface containing the value from the string.</returns>
        internal static ModelIdBase<T> FromString(string value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            Guid id;
            var originAndId = value.Split(new[] { ":" }, StringSplitOptions.None);

            if (originAndId.Length != 2)
            {
                throw new ArgumentOutOfRangeException("value", "value must be in the format of Origin:Identifier");
            }

            return Guid.TryParse(originAndId[1], out id) ? new GuidId<T> { Id = id, Origin = originAndId[0] } : null;
        }
    }
}
Jesse C. Slicer
sumber
Ya, itu sudah lewat kawat. Saya tidak tahu bahwa saya memerlukan ID-type keselamatan untuk entitasnya, tapi saya tertarik untuk melihat apa yang Anda maksud dengan itu. Jadi ya, jika Anda bisa mengembangkannya, itu akan menyenangkan.
Bob Horn
Saya sudah melakukannya. Menjadi sedikit kode-berat :)
Jesse C. Slicer
1
By the way, saya tidak menjelaskan Originproperti: ini sangat mirip skema dalam bahasa SQL Server. Anda mungkin memiliki Fooyang digunakan dalam perangkat lunak Akuntansi Anda dan yang lain Foountuk Sumber Daya Manusia dan ada sedikit bidang untuk membedakannya pada lapisan akses data Anda. Atau, jika Anda tidak memiliki konflik, abaikan seperti saya.
Jesse C. Slicer
1
@ JesseC.Slicer: pada pandangan pertama, sepertinya contoh sempurna untuk sesuatu rekayasa berlebihan.
Doc Brown
2
@DocBrown mengangkat bahu masing-masing. Ini solusi yang dibutuhkan sebagian orang. Beberapa orang tidak. Jika YAGNI, maka jangan menggunakannya. Jika Anda membutuhkannya, itu dia.
Jesse C. Slicer
5

Saya tentu setuju dengan kesimpulan Anda. Melewati id lebih disukai karena beberapa alasan:

  1. Sederhana saja. antarmuka antar komponen harus sederhana.
  2. Membuat Fooobjek hanya untuk id berarti membuat nilai yang salah. Seseorang dapat membuat kesalahan dan menggunakan nilai-nilai ini.
  3. intlebih platform-lebar dan dapat dinyatakan secara asli dalam semua bahasa modern. Untuk membuat Fooobjek dengan pemanggil metode, Anda mungkin perlu membuat struktur data yang kompleks (seperti objek json).
Amiram Korach
sumber
4

Saya pikir Anda akan lebih bijaksana untuk membangun pencarian pada pengidentifikasi objek seperti yang disarankan Ben Voigt.

Namun, ingat bahwa jenis pengidentifikasi objek Anda dapat berubah. Karena itu, saya akan membuat kelas pengidentifikasi untuk masing-masing item saya, dan hanya memungkinkan mencari item melalui instance pengidentifikasi ini. Lihat contoh berikut:

public class Item
{
  public class ItemId
  {
    public int Id { get; set;}
  }

  public ItemId Id; { get; set; }
}

public interface Service
{
  Item GetItem(ItemId id);
}

Saya menggunakan enkapsulasi, tetapi Anda juga bisa membuatnya Item dari ItemId.

Dengan begitu, jika jenis id Anda berubah di sepanjang jalan, Anda tidak perlu mengubah apa pun di Itemkelas, atau dalam tanda tangan metode GetItem. Hanya dalam implementasi layanan Anda harus mengubah kode Anda (yang merupakan satu-satunya hal yang berubah dalam semua kasus)

Jalayn
sumber
2

Tergantung pada apa metode Anda.

Umumnya untuk Get methods, itu adalah akal sehat untuk lulus id parameterdan mendapatkan objek kembali. Sementara untuk pembaruan atauSET methods Anda akan mengirim seluruh objek untuk diatur / diperbarui.

Dalam beberapa kasus lain di mana Anda method is passing search parameters(sebagai kumpulan dari jenis primitif individu) untuk mengambil satu set hasil, mungkin bijaksana untuk use a container to holdparameter pencarian Anda. Ini berguna jika dalam jangka panjang jumlah parameter akan berubah. Jadi, Anda would not needharus mengubah signature of your method, add or remove parameter in all over the places.

EL Yusubov
sumber