Przekazać identyfikator lub obiekt?

38

Czy przy podawaniu metody logiki biznesowej w celu uzyskania encji domeny parametr powinien akceptować obiekt lub identyfikator? Na przykład, czy powinniśmy to zrobić:

public Foo GetItem(int id) {}

albo to:

public Foo GetItem(Foo foo) {}

Wierzę w przekazywanie obiektów w całości, ale co z tym przypadkiem, w którym otrzymujemy przedmiot i znamy tylko identyfikator? Czy osoba dzwoniąca powinna utworzyć pusty Foo i ustawić identyfikator, czy też powinna po prostu przekazać identyfikator do metody? Ponieważ przychodzące Foo będzie puste, z wyjątkiem identyfikatora, nie widzę korzyści, że osoba dzwoniąca musi utworzyć Foo i ustawić swój identyfikator, gdy może po prostu wysłać identyfikator do metody GetItem ().

Bob Horn
źródło

Odpowiedzi:

42

Tylko jedno pole jest używane do wyszukiwania.

Dzwoniący nie ma Foo, próbuje go zdobyć. Jasne, że możesz tymczasowo Foopozostawić puste pola, ale działa to tylko w przypadku trywialnych struktur danych. Większość obiektów ma niezmienniki, które zostałyby naruszone przez podejście polegające głównie na pustych obiektach, więc należy tego unikać.

Ben Voigt
źródło
Dziękuję Ci. Podoba mi się ta odpowiedź z punktem 2 Amirama w jego odpowiedzi.
Bob Horn
3
To wydaje się logiczne. Ale pod względem wydajności natknąłem się na obszary, w których osoba dzwoniąca może mieć obiekt, a może nie. Dopiero przekazanie identyfikatora może doprowadzić do dwukrotnego odczytu wspomnianego obiektu z bazy danych. Czy to tylko akceptowalny hit wydajności? Czy zapewniasz możliwość przekazania zarówno identyfikatora, jak i obiektu?
computrius
Przyjmuję teraz te zasady „nigdy nie przekazuj obiektu” z odrobiną soli. To zależy tylko od kontekstu / scenariusza.
Bruno,
12

Czy będzie to miało miejsce w dowolnym momencie (w przyszłości)? Preferuj typ pojedynczego identyfikatora zamiast pełnego obiektu, który wie, jak.

Jeśli szukasz bezpieczeństwa typu identyfikatora dla jego encji, istnieją również rozwiązania kodowe. Daj mi znać, jeśli potrzebujesz przykładu.

Edycja: rozszerzenie na bezpieczeństwo typu ID:

Więc weźmy swoją metodę:

public Foo GetItem(int id) {}

Mamy tylko nadzieję, że przekazana liczba całkowita iddotyczy Fooobiektu. Ktoś mógłby go źle użyć i przekazać Baridentyfikator obiektu lub nawet ręcznie wpisać 812341. To nie jest bezpieczne Foo. Po drugie, nawet jeśli użyłeś podania obiektu Foo, na pewno mam Foopole ID, intktóre ktoś może ewentualnie zmodyfikować. I wreszcie, nie można użyć przeciążenia metody, jeśli istnieją one razem w klasie, ponieważ zmienia się tylko typ zwracany. Przepiszmy tę metodę trochę, aby wyglądała na bezpieczną dla tekstu w C #:

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

Więc wprowadziłem klasę o nazwie, IntIdktóra zawiera ogólny element. W tym konkretnym przypadku chcę tylko intskojarzenia Foo. Nie mogę po prostu wpaść nago inti nie mogę IntId<Bar>jej przypadkowo przypisać . Poniżej znajduje się sposób, w jaki napisałem te bezpieczne typy. Należy pamiętać, że manipulowanie faktycznym instrumentem bazowym intodbywa się tylko w warstwie dostępu do danych. Wszystko powyżej, co widzi tylko silny typ i nie ma (bezpośredniego) dostępu do swojego wewnętrznego intidentyfikatora. To nie powinno mieć żadnego powodu.

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

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

i, dla kompletności mojej bazy kodu, napisałem również jeden dla encji 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
źródło
Tak, idzie przez drut. Nie wiem, czy potrzebuję bezpieczeństwa typu identyfikatora dla jego podmiotu, ale interesuje mnie to, co masz na myśli. Więc tak, jeśli możesz to rozwinąć, byłoby miło.
Bob Horn
Zrobiłem to. Stałem się trochę obciążony kodem :)
Jesse C. Slicer
1
Nawiasem mówiąc, nie wyjaśniłem tej Originwłaściwości: to bardzo przypomina schemat w języku SQL Server. Możesz mieć taki, Fooktóry jest używany w twoim oprogramowaniu księgowym, a drugi, Fooktóry dotyczy zasobów ludzkich, i to małe pole istnieje, aby odróżnić je na twojej warstwie dostępu do danych. Lub, jeśli nie masz konfliktów, zignoruj ​​to tak jak ja.
Jesse C. Slicer
1
@ JesseC.Slicer: na pierwszy rzut oka wygląda to na doskonały przykład nadmiernej inżynierii.
Doc Brown
2
@DocBrown wzruszają ramionami do siebie. Jest to rozwiązanie, którego potrzebują niektórzy ludzie. Niektórzy nie. Jeśli YAGNI, nie używaj go. Jeśli potrzebujesz, to jest.
Jesse C. Slicer,
5

Z pewnością zgadzam się z twoim wnioskiem. Przekazywanie identyfikatora jest preferowane z kilku powodów:

  1. To jest proste. interfejs między komponentami powinien być prosty.
  2. Tworzenie Fooobiektu tylko dla identyfikatora oznacza tworzenie fałszywych wartości. Ktoś może pomylić się i użyć tych wartości.
  3. intma większą platformę i może być zadeklarowany natywnie we wszystkich nowoczesnych językach. Aby utworzyć Fooobiekt za pomocą metody wywołującej, prawdopodobnie musisz utworzyć złożoną strukturę danych (np. Obiekt json).
Amiram Korach
źródło
4

Myślę, że rozsądnie byłoby ustalić wyszukiwanie identyfikatora obiektu, jak zasugerował Ben Voigt.

Pamiętaj jednak, że typ identyfikatora obiektu może ulec zmianie. Jako taki, stworzyłbym klasę identyfikatorów dla każdego z moich przedmiotów i pozwoliłbym na wyszukiwanie przedmiotów tylko poprzez te wystąpienia tych identyfikatorów. Zobacz następujący przykład:

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

  public ItemId Id; { get; set; }
}

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

Użyłem enkapsulacji, ale możesz też Itemodziedziczyć po niej ItemId.

W ten sposób, jeśli typ identyfikatora zmienia się po drodze, nie musisz nic zmieniać w Itemklasie ani w podpisie metody GetItem. Tylko przy wdrażaniu usługi musiałbyś zmienić swój kod (co jest jedyną rzeczą, która zmienia się we wszystkich przypadkach)

Jalayn
źródło
2

To zależy od tego, co robi twoja metoda.

Ogólnie rzecz biorąc Get methods, zdrowym rozsądkiem jest przekazywanie id parameteri odzyskiwanie obiektu. Podczas aktualizacji lub SET methodswysłałbyś cały obiekt do ustawienia / aktualizacji.

W niektórych innych przypadkach, gdy method is passing search parameters(jako zbiór pojedynczych typów pierwotnych), aby pobrać zestaw wyników, może być mądre do use a container to holdparametrów wyszukiwania. Jest to przydatne, jeśli na dłuższą metę zmieni się liczba parametrów. W ten sposób możesz would not needzmienić signature of your method, add or remove parameter in all over the places.

EL Yusubov
źródło