Передати ідентифікатор або об’єкт?


38

Надаючи метод бізнес-логіки для отримання об'єкта домену, чи повинен параметр приймати об'єкт або ідентифікатор? Наприклад, чи слід це робити:

public Foo GetItem(int id) {}

або це:

public Foo GetItem(Foo foo) {}

Я вірю в передачу об'єктів навколо, у всій їх повноті, але як бути з цим випадком, коли ми отримуємо об'єкт, і ми знаємо лише ідентифікатор? Чи повинен абонент створити порожній Foo та встановити ідентифікатор, чи він повинен просто передати ідентифікатор методу? Оскільки вхідний Foo буде порожнім, за винятком ідентифікатора, я не бачу переваги абонента, який повинен створити Foo і встановити його ідентифікатор, коли він може просто надіслати ідентифікатор методу GetItem ().

Відповіді:


42

Тільки одне поле, яке використовується для пошуку.

У абонента немає Foo, він намагається отримати його. Звичайно, ви можете зробити тимчасовим, Fooякщо всі інші поля залишені порожніми, але це працює лише для тривіальних структур даних. Більшість об’єктів мають інваріанти, які були б порушені підходом, який здебільшого порожній-об'єкт, тому уникайте цього.


Дякую. Мені подобається ця відповідь з точки Амірама №2 у його відповіді.
Боб Хорн

3
Це здається логічним. Але ефективніше, я зіткнувся з областями, де абонент може мати об'єкт, а він не може. Тільки передача ідентифікатора може призвести до того, що згаданий об’єкт читається з бази даних двічі. Це просто прийнятний показник ефективності? Або ви надаєте можливість передачі ідентифікатора, або об'єкта?
computrius

Я приймаю ці правила "ніколи не передавай об'єкт" із зерном солі. Це просто залежить від вашого контексту / сценарію.
Бруно

12

Чи відбудеться це через провід (серіалізований / десеріалізований) будь-який час зараз чи в майбутньому? Віддайте перевагу типу одиночного ідентифікатора над великим повним об'єктом, який знає, як.

Якщо ви шукаєте безпеку типу ідентифікатора для своєї сутності, то також є рішення з кодом. Дайте мені знати, якщо вам потрібен приклад.

Редагувати: розширення щодо безпеки типу ID:

Отже, візьмемо ваш метод:

public Foo GetItem(int id) {}

Ми лише сподіваємось , idщо передане ціле число призначене для Fooоб'єкта. Хтось може його неправильно використати та передати Barцілий ідентифікатор об'єкта чи навіть просто ввести рукою 812341. Це не безпечно для типу Foo. По-друге, навіть якщо ви використовували Fooверсію об'єкта pass , я впевнений, що в Fooньому є поле ідентифікатора, intяке хтось може змінити. І нарешті, ви не можете використовувати перевантаження методу, якщо вони існують у класі разом, оскільки змінюється лише тип повернення. Давайте трохи перепишемо цей метод, щоб виглядати безпечним для типу в C #:

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

Тому я представив клас, на ім'я IntIdякого є загальний твір. У цьому конкретному випадку я хочу, intщоб це асоціювалося Fooтільки. Я не можу просто пройти оголеним, intа також не можу призначити IntId<Bar>йому випадково. Отже нижче - як я написав ці безпечні для типу ідентифікатори. Візьміть до уваги, що маніпулювання фактичним підґрунтям intвідбувається лише на рівні доступу до даних. Все вище, що бачить лише сильний тип і не має (прямого) доступу до свого внутрішнього intідентифікатора. Це не повинно мати причин.

Інтерфейс 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);
    }
}

Базовий клас 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;
        }
    }
}

і для повноти моєї бази даних я також написав її для сутностей 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;
        }
    }
}

Так, це переходить через дріт. Я не знаю, що мені потрібна безпека типу ID для його сутності, але мені цікаво побачити, що ви маєте на увазі під цим. Так так, якщо ви можете розширити це, було б добре.
Боб Хорн

Я це зробив. Став трохи важким кодом :)
Джессі С. Слікер

1
До речі, я не пояснив Originвластивість: це дуже схоже на схему на мові SQL Server. У вас може бути Fooте, що використовується у вашому програмному забезпечення для бухгалтерського обліку та інше, Fooщо призначене для людських ресурсів, і це маленьке поле існує, щоб розрізнити їх на рівні доступу до даних. Або, якщо у вас немає конфліктів, ігноруйте це, як і я.
Джессі К. Слікер

1
@ JesseC.Slicer: на перший погляд, це виглядає як ідеальний приклад для надмірної інженерії.
Док Браун

2
@DocBrown знизав плечима до кожного. Це рішення, яке потрібно деяким людям. Деякі люди цього не роблять. Якщо YAGNI, то не використовуйте його. Якщо вам це потрібно, воно є.
Джессі К. Слікер

5

Я, безумовно, згоден з вашим висновком. Передача ідентифікатора бажана з деяких причин:

  1. Це просто. інтерфейс між компонентами повинен бути простим.
  2. Створення Fooоб'єкта лише для id означає створення помилкових значень. Хтось може помилитися і використовувати ці значення.
  3. intє більш платформою і її можна декларувати на всій сучасній мові. Щоб створити Fooоб’єкт за допомогою виклику методу, вам, ймовірно, потрібно буде створити складну структуру даних (наприклад, об’єкт json).

4

Я думаю, вам було б розумно встановити пошук на ідентифікаторі об'єкта, як запропонував Бен Войгт.

Однак пам’ятайте, що тип ідентифікатора вашого об'єкта може змінюватися. Як такий, я створив би клас ідентифікаторів для кожного зі своїх елементів і дозволяв би шукати елементи лише через ці екземпляри цих ідентифікаторів. Дивіться наступний приклад:

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

  public ItemId Id; { get; set; }
}

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

Я використовував інкапсуляцію, але ви також можете зробити Itemспадщину відItemId .

Таким чином, якщо тип вашого ідентифікатора змінюється вздовж дороги, вам не доведеться нічого змінювати в Itemкласі або в підписі методу GetItem. Тільки під час використання послуги вам доведеться змінити свій код (це єдине, що все-таки змінюється у всіх випадках)


2

Це залежить від того, що робить ваш метод.

Як правило, для Get methodsздорового глузду здати id parameterоб'єкт назад і повернути його. У той час як для оновлення, або SET methodsви надіслали б весь об'єкт для встановлення / оновлення.

У деяких інших випадках, коли ваш method is passing search parameters(як колекція окремих примітивних типів) для отримання набору результатів, може бути розумним для use a container to holdваших параметрів пошуку. Це корисно, якщо в перспективі кількість параметрів зміниться. Таким чином, ви would not needзмінити signature of your method, add or remove parameter in all over the places.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.