ID de passe ou objet?

38

Lorsque vous fournissez une méthode de logique métier pour obtenir une entité de domaine, le paramètre doit-il accepter un objet ou un ID? Par exemple, devrions-nous faire ceci:

public Foo GetItem(int id) {}

ou ca:

public Foo GetItem(Foo foo) {}

Je crois en la possibilité de faire circuler des objets dans leur intégralité, mais qu'en est-il du cas où nous obtenons un objet et que nous ne connaissons que l'identifiant? L'appelant doit-il créer un Foo vide et définir l'ID, ou doit-il simplement transmettre l'ID à la méthode? Puisque le Foo entrant sera vide, à l'exception de l'ID, je ne vois pas l'avantage pour l'appelant de devoir créer un Foo et définir son ID lorsqu'il pourrait simplement envoyer l'ID à la méthode GetItem ().

Bob Horn
la source

Réponses:

42

Juste le seul champ utilisé pour la recherche.

L'appelant n'a pas de Foo, il essaie de l'obtenir. Bien sûr, vous pouvez créer un temporaire Fooavec tous les autres champs laissés en blanc, mais cela ne fonctionne que pour les structures de données triviales. La plupart des objets ont des invariants qui seraient violés par l'approche à objets principalement vides, alors évitez-les.

Ben Voigt
la source
Merci. J'aime cette réponse avec le point n ° 2 d'Amiram dans sa réponse.
Bob Horn
3
Cela semble logique. Mais en ce qui concerne les performances, j’ai rencontré des domaines dans lesquels l’appelant peut avoir l’objet ou non. Seul le passage de l'identifiant peut entraîner la lecture de cet objet deux fois dans la base de données. Est-ce juste une performance acceptable? Ou fournissez-vous la possibilité de l'identifiant ou de l'objet à transmettre?
Computrius
Je prends ces règles du «ne jamais passer l'objet» avec un grain de sel ces jours-ci. Cela dépend de votre contexte / scénario.
Bruno
12

Est-ce que cela va passer par le fil (sérialisé / désérialisé) à tout moment maintenant ou à l'avenir? Privilégiez le type d'identifiant unique par rapport à l'objet complet who-know-how-large.

Si vous recherchez une sécurité de type de l'ID vers son entité, il existe également des solutions de code. Faites-moi savoir si vous avez besoin d'un exemple.

Edit: développer la sécurité de type de l'ID:

Alors, prenons votre méthode:

public Foo GetItem(int id) {}

Nous espérons seulement que le nombre entier idtransmis est destiné à un Fooobjet. Quelqu'un pourrait en abuser et transmettre Barl'ID entier d' un objet ou même simplement à la main 812341. Ce n'est pas sûr de taper Foo. Deuxièmement, même si vous utilisiez le passage une Fooversion d’ objet , je suis sûr Fooqu’un champ d’identification est celui intque quelqu'un peut éventuellement modifier. Enfin, vous ne pouvez pas utiliser la surcharge de méthodes si elles existent dans une classe, car seul le type de retour varie. Réécrivons un peu cette méthode pour avoir une apparence de type sécurisé en C #:

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

J'ai donc introduit une classe nommée IntIdqui contient un élément générique. Dans ce cas particulier, je veux un intqui est associé à Fooseulement. Je ne peux pas simplement passer nu intet je ne peux pas lui attribuer un IntId<Bar>accidentellement. Voici donc comment j'ai écrit ces identifiants de type sécurisé. Prenez note que la manipulation du sous int- jacent réel n’est que sur votre couche d’accès aux données. Tout ce qui est au-dessus ne voit que le type fort et n'a pas d'accès (direct) à son intID interne . Il ne devrait y avoir aucune raison de

Interface 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 classe de base:

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;
        }
    }
}

et, par souci d'exhaustivité de ma base de code, j'en ai aussi écrit un pour les entités 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
la source
Oui, ça passe sur le fil. Je ne sais pas si j'ai besoin de la sécurité de type de l'ID de son entité, mais je suis intéressé de voir ce que vous entendez par là. Donc oui, si vous pouviez développer cela, ce serait bien.
Bob Horn
Je l'ai fait Est devenu un peu lourd en code :)
Jesse C. Slicer Le
1
En passant, je n’ai pas expliqué la Originpropriété: cela ressemble beaucoup à un schéma dans le jargon de SQL Server. Vous avez peut-être un Foologiciel utilisé dans votre logiciel de comptabilité et un autre Foodestiné aux ressources humaines. Il existe peu de champs pour les distinguer au niveau de votre couche d'accès aux données. Ou, si vous n’avez pas de conflit, ignorez-le comme moi.
Jesse C. Slicer
1
@ JesseC.Slicer: au premier abord, cela ressemble à un exemple parfait pour sur-ingénierie.
Doc Brown
2
@DocBrown haussement d' épaules à chacun. C'est une solution dont certaines personnes ont besoin. Certaines personnes ne le font pas. Si YAGNI, alors ne l'utilisez pas. Si vous en avez besoin, le voici.
Jesse C. Slicer
5

Je suis certainement d'accord avec votre conclusion. Passer un identifiant est préférable pour certaines raisons:

  1. C'est simple. l'interface entre les composants doit être simple.
  2. Créer un Fooobjet juste pour l'id signifie créer de fausses valeurs. Quelqu'un peut faire une erreur et utiliser ces valeurs.
  3. intest plus large et peut être déclaré nativement dans toutes les langues modernes. Pour créer un Fooobjet à l'aide de l'appelant de méthode, vous devrez probablement créer une structure de données complexe (comme un objet json).
Amiram Korach
la source
4

Je pense que vous seriez sage d’établir la recherche sur l’identifiant de l’objet, comme l’a suggéré Ben Voigt.

Cependant, rappelez-vous que le type d'identifiant de votre objet peut changer. En tant que tel, je créerais une classe d'identifiant pour chacun de mes éléments et n'autoriserais que la recherche d'éléments via ces instances de ces identificateurs. Voir l'exemple suivant:

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

  public ItemId Id; { get; set; }
}

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

J'ai utilisé l'encapsulation, mais vous pouvez aussi faire Itemhériter de ItemId.

Ainsi, si le type de votre identifiant change en cours de route, vous ne devez rien changer à la Itemclasse ni à la signature de la méthode GetItem. Ce n'est que dans la mise en œuvre du service que vous devrez changer votre code (ce qui est la seule chose qui change dans tous les cas de toute façon)

Jalayn
la source
2

Cela dépend de ce que fait votre méthode.

En règle générale Get methods, il est de bon sens de passer id parameteret de récupérer l'objet. En attente de mise à jour ou SET methodsd’envoi de l’ensemble de l’objet à définir / mettre à jour.

Dans certains autres cas où votre method is passing search parameters(en tant que collection de types primitifs individuels) pour récupérer un ensemble de résultats, il peut être judicieux de définir use a container to holdvos paramètres de recherche. Ceci est utile si, à long terme, le nombre de paramètres change. Ainsi, vous would not needchangez le signature of your method, add or remove parameter in all over the places.

EL Yusubov
la source