Много раз мы передаем строки в конструктор бизнес-объектов, и мы хотим быть уверены, что эти строки действительно несут в себе значение.В таком сценарии мы выполняем проверку параметров конструктора и выдаем ArgumentException
каждый раз, когда переданная строка является нулевым или пробелом.
Это пример того, что я имею в виду:
public class Person
{
public string Name { get; }
public Person(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("A person name cannot be null or white space", nameof(name));
}
this.Name = name;
}
}
Усталый от повторения себя, я решил разработать автоматический сейф, представляющий собой строку, не равную нулю или пробелу.Таким образом, я могу напрямую использовать экземпляры этого типа в своем бизнес-коде и избежать какой-либо проверки, потому что каждый экземпляр типа автоматически безопасен (другими словами, код проверки теперь находится в одном месте, код для самого типа).
Это структура NonEmptyString
(исходный код здесь ):
using System;
namespace Deltatre.Utils.Types
{
/// <summary>
/// This type wraps a string which is guaranteed to be neither null nor white space
/// </summary>
public struct NonEmptyString
{
/// <summary>
/// Implicit conversion from <see cref="NonEmptyString"/> to <see cref="string"/>
/// </summary>
/// <param name="nonEmptyString">The instance of <see cref="NonEmptyString"/> to be converted</param>
public static implicit operator string(NonEmptyString nonEmptyString)
{
return nonEmptyString.Value;
}
/// <summary>
/// Explicit conversion from <see cref="string"/> to <see cref="NonEmptyString"/>
/// </summary>
/// <param name="value">The instance of <see cref="string"/> to be converted</param>
/// <exception cref="InvalidCastException">Throws <see cref="InvalidCastException"/> when <paramref name="value"/> is null or white space</exception>
public static explicit operator NonEmptyString(string value)
{
try
{
return new NonEmptyString(value);
}
catch (ArgumentException ex)
{
throw new InvalidCastException($"Unable to convert the provided string to {typeof(NonEmptyString).Name}", ex);
}
}
/// <summary>
/// Creates new instance of <see cref="NonEmptyString"/>
/// </summary>
/// <param name="value">The string to be wrapped</param>
/// <exception cref="ArgumentException">Throws <see cref="ArgumentException"/> when parameter <paramref name="value"/> is null or white space</exception>
public NonEmptyString(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException($"Parameter {nameof(value)} cannot be null or white space", nameof(value));
this.Value = value;
}
/// <summary>
/// Gets the wrapped string
/// </summary>
public string Value { get; }
/// <summary>Indicates whether this instance and a specified object are equal.</summary>
/// <param name="obj">The object to compare with the current instance. </param>
/// <returns>
/// <see langword="true" /> if <paramref name="obj" /> and this instance are the same type and represent the same value; otherwise, <see langword="false" />. </returns>
public override bool Equals(object obj)
{
if (!(obj is NonEmptyString))
{
return false;
}
var other = (NonEmptyString)obj;
return this.Value == other.Value;
}
/// <summary>Returns the hash code for this instance.</summary>
/// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = (hash * 23) + (this.Value == null ? 0 : this.Value.GetHashCode());
return hash;
}
}
/// <summary>
/// Compares two instances of <see cref="NonEmptyString"/> for equality
/// </summary>
/// <param name="left">An instance of <see cref="NonEmptyString"/></param>
/// <param name="right">An instance of <see cref="NonEmptyString"/></param>
/// <returns></returns>
public static bool operator ==(NonEmptyString left, NonEmptyString right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two instances of <see cref="NonEmptyString"/> for inequality
/// </summary>
/// <param name="left">An instance of <see cref="NonEmptyString"/></param>
/// <param name="right">An instance of <see cref="NonEmptyString"/></param>
/// <returns></returns>
public static bool operator !=(NonEmptyString left, NonEmptyString right)
{
return !(left == right);
}
}
}
Используя мой новый тип, я могу изменить предыдущий код следующим образом:
public class Person
{
public NonEmptyString Name { get; }
public Person(NonEmptyString name)
{
this.Name = name;
}
}
Единственная проблема с этим дизайном представлена конструктором по умолчанию , который всегда доступен, потому что мой тип - struct
.
Если кто-то, использующий мой код, пишетvar myString = new NonEmptyString();
он получает экземпляр типа, который инкапсулирует ссылку null
: это то, чего я хотел бы избежать, потому что при этом вся цель моего авто-безопасного типа становится недействительной.Другими словами, я не хочу полагаться на то, что программист не вызывает конструктор по умолчанию, я хотел бы сделать невозможным неправильное использование этого типа.
Я пришел с парой идей:
предоставляет значение по умолчанию для свойства только для чтения Value
, что-то вроде "NA".Таким образом, даже когда вызывается конструктор по умолчанию, полученный экземпляр инкапсулирует ненулевое и непустое значение.
добавление флага, указывающего, был ли инициализирован тип, имеющего значение по умолчанию:false
.Это состояние доступно только для чтения, и оно изменяется на true
только при перегрузке конструктора, получающей строковый параметр.Таким образом, защита может быть добавлена к любому члену типа, так что InvalidOperationException
может быть вызвано каждый раз, когда программист пытается использовать неинициализированный экземпляр типа (то есть экземпляр типа, полученный путем вызоваконструктор по умолчанию).
У вас есть предложения?Какой подход вы предпочитаете?