Tiny types (also called micro types) can make code easier to read and navigate. Also validation can be done at a single place.
Please take a look to the code this method:
private void SendMessage(int portNumber, int timeout)
{
// Implementation
}
When calling this message, the parameters can be unintentionally swapped and it still compiles. This is one of the problems with this anti-pattern called primitive obsession.
Imagine that a new port number must be used everywhere in the solution and you need to check all places where a port number is used. 'Find all references' of int will not help you.
private void SendMessage(PortNumber portNumber, Miliseconds timeout)
{
// Implementation
}
This solution is more useful:
When using a base class that do most of the stuff vor you, this is how to create a tiny type:
public class PortNumber : TinyType<int>
{
protected PortNumber(int value) : base(value)
{
if (value < 0)
{
throw new ArgumentException($"A {nameof(PortNumber)} must have a value of at least 0.", nameof(value));
}
}
public static implicit operator PortNumber(int value) => new(value);
}
Note that the int is a struct and it is wrapped by a class. A more performant solution would be to use a struct as a wrapper but then you can not use inheritance. In this case a lot of functionality is implemented in the base class.
The code for Miliseconds is almost the same as the PortNumber class so you can even create an abstract class to save duplicate code. This makes the code of the 2 classes even shorter.
public abstract class NonNegativeNumber : TinyType<int>
{
protected NonNegativeNumber(int value) : base(value)
{
if (value < 0)
{
throw new ArgumentException($"A {nameof(NonNegativeNumber)} must have a value of at least 0.", nameof(value));
}
}
}
public class PortNumber : NonNegativeNumber
{
protected PortNumber(int value) : base(value)
{
}
public static implicit operator PortNumber(int value) => new(value);
}
public class Miliseconds : NonNegativeNumber
{
protected Miliseconds(int value) : base(value)
{
}
public static implicit operator Miliseconds(int value) => new(value);
}
The TinyType
Miliseconds m1 = 200; // Assign like an int
Miliseconds m2 = 400;
Miliseconds m3 = 200;
var isSame = m1 == m3; // true
var isGreaterThan = m2 > m1; // true
var isComparedToInt = m2 > 5; // true, compares to int
// Support for ordering / IComparable<T>
var orderedItems = new[] { m1, m2, m3 }.OrderBy(m => m);
// Support for GetHashCode for quick lookup in Dictionary
var dictionary = new Dictionary<Miliseconds, string>
{
[m1] = "1",
[m2] = "2"
};
The examples above works for structs, for classes like strings there is a second base class that works very simular:
EmailAddress emailAddress = "alex@siepman.nl";
Name firstName = "Alex";
Examples of the type implementations:
public class Name : TinyTypeClass<string>
{
public Name(string value) : base(value.Trim())
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"A {nameof(Name)} must have a length of at least 1.", nameof(value));
}
}
public static implicit operator Name(string value) => new(value);
}
public class EmailAddress : TinyTypeClass<string>
{
public EmailAddress(string value) : base(value.Trim())
{
if (!IsValidEmail(value))
{
throw new ArgumentException($"The format of the {nameof(EmailAddress)} is invalid.", nameof(value));
}
}
public static implicit operator EmailAddress(string value) => new(value);
private static bool IsValidEmail(string email)
{
if (email.Trim().EndsWith("."))
{
return false;
}
try
{
var address = new System.Net.Mail.MailAddress(email);
return address.Address == email;
}
catch
{
return false;
}
}
}
public abstract class TinyType<T> : IComparable<TinyType<T>>
where T : struct, IComparable<T>
{
public T Value { get; }
protected TinyType(T value)
{
Value = value;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((TinyType<T>)obj);
}
protected bool Equals(TinyType<T> value)
{
return Value.Equals(value.Value);
}
public bool Equals(T value)
{
return Value.Equals(value);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public override string? ToString()
{
return Value.ToString();
}
public static bool operator <(TinyType<T> left, TinyType<T> right)
{
return Comparer<TinyType<T>>.Default.Compare(left, right) < 0;
}
public static bool operator >(TinyType<T> left, TinyType<T> right)
{
return Comparer<TinyType<T>>.Default.Compare(left, right) > 0;
}
public static bool operator <=(TinyType<T> left, TinyType<T> right)
{
return Comparer<TinyType<T>>.Default.Compare(left, right) <= 0;
}
public static bool operator >=(TinyType<T> left, TinyType<T> right)
{
return Comparer<TinyType<T>>.Default.Compare(left, right) >= 0;
}
public static bool operator ==(TinyType<T> left, TinyType<T> right)
{
return Comparer<TinyType<T>>.Default.Compare(left, right) == 0;
}
public static bool operator !=(TinyType<T> left, TinyType<T> right)
{
return Comparer<TinyType<T>>.Default.Compare(left, right) != 0;
}
public static bool operator <(TinyType<T> left, T right)
{
return Comparer<T>.Default.Compare(left.Value, right) < 0;
}
public static bool operator >(TinyType<T> left, T right)
{
return Comparer<T>.Default.Compare(left.Value, right) > 0;
}
public static bool operator <(T left, TinyType<T> right)
{
return Comparer<T>.Default.Compare(left, right.Value) < 0;
}
public static bool operator >=(T left, TinyType<T> right)
{
return Comparer<T>.Default.Compare(left, right.Value) >= 0;
}
public static bool operator <=(TinyType<T> left, T right)
{
return Comparer<T>.Default.Compare(left.Value, right) <= 0;
}
public static bool operator >=(TinyType<T> left, T right)
{
return Comparer<T>.Default.Compare(left.Value, right) >= 0;
}
public static bool operator <=(T left, TinyType<T> right)
{
return Comparer<T>.Default.Compare(left, right.Value) <= 0;
}
public static bool operator >(T left, TinyType<T> right)
{
return Comparer<T>.Default.Compare(left, right.Value) > 0;
}
public static bool operator ==(TinyType<T> left, T right)
{
return Comparer<T>.Default.Compare(left.Value, right) == 0;
}
public static bool operator !=(TinyType<T> left, T right)
{
return Comparer<T>.Default.Compare(left.Value, right) != 0;
}
public static bool operator ==(T left, TinyType<T> right)
{
return Comparer<T>.Default.Compare(left, right.Value) == 0;
}
public static bool operator !=(T left, TinyType<T> right)
{
return Comparer<T>.Default.Compare(left, right.Value) != 0;
}
public int CompareTo(TinyType<T>? other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
return Value.CompareTo(other.Value);
}
}
public abstract class TinyTypeClass<T> : IComparable<T>, IComparable<TinyTypeClass<T>>
where T : IComparable<T>
{
public T Value { get; }
protected TinyTypeClass(T value)
{
Value = value ?? throw new ArgumentNullException($"If you want the value to be null, make the {nameof(TinyTypeClass<T>)} null.",nameof(value));
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((TinyTypeClass<T>)obj);
}
protected bool Equals(TinyTypeClass<T> value)
{
return EqualityComparer<T>.Default.Equals(Value, value.Value);
}
public bool Equals(T value)
{
return Value.Equals(value);
}
public override int GetHashCode()
{
return EqualityComparer<T>.Default.GetHashCode(Value);
}
public override string ToString()
{
return Value.ToString() ?? "";
}
public int CompareTo(T? other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
return Value.CompareTo(other);
}
public int CompareTo(TinyTypeClass<T>? other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
return Value.CompareTo(other.Value);
}
public static bool operator <(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
{
return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) < 0;
}
public static bool operator >(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
{
return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) > 0;
}
public static bool operator <=(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
{
return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) <= 0;
}
public static bool operator >=(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
{
return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) >= 0;
}
public static bool operator ==(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
{
return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) == 0;
}
public static bool operator !=(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
{
return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) != 0;
}
public static bool operator <(TinyTypeClass<T>? left, T right)
{
if (left == null) return false;
return Comparer<T>.Default.Compare(left.Value, right) < 0;
}
public static bool operator >(TinyTypeClass<T>? left, T right)
{
if (left == null) return true;
return Comparer<T>.Default.Compare(left.Value, right) > 0;
}
public static bool operator <(T left, TinyTypeClass<T>? right)
{
if (right == null) return true;
return Comparer<T>.Default.Compare(left, right.Value) < 0;
}
public static bool operator >=(T left, TinyTypeClass<T>? right)
{
if (right == null) return false;
return Comparer<T>.Default.Compare(left, right.Value) >= 0;
}
public static bool operator <=(TinyTypeClass<T>? left, T right)
{
if (left == null) return false;
return Comparer<T>.Default.Compare(left.Value, right) <= 0;
}
public static bool operator >=(TinyTypeClass<T>? left, T right)
{
if (left == null) return true;
return Comparer<T>.Default.Compare(left.Value, right) >= 0;
}
public static bool operator <=(T left, TinyTypeClass<T>? right)
{
if (right == null) return true;
return Comparer<T>.Default.Compare(left, right.Value) <= 0;
}
public static bool operator >(T left, TinyTypeClass<T>? right)
{
if (right == null) return false;
return Comparer<T>.Default.Compare(left, right.Value) > 0;
}
public static bool operator ==(TinyTypeClass<T>? left, T right)
{
if (left == null) return false;
return Comparer<T>.Default.Compare(left.Value, right) == 0;
}
public static bool operator !=(TinyTypeClass<T>? left, T right)
{
if (left == null) return true;
return Comparer<T>.Default.Compare(left.Value, right) != 0;
}
public static bool operator ==(T left, TinyTypeClass<T>? right)
{
if (right == null) return true;
return Comparer<T>.Default.Compare(left, right.Value) == 0;
}
public static bool operator !=(T left, TinyTypeClass<T>? right)
{
if (right == null) return false;
return Comparer<T>.Default.Compare(left, right.Value) != 0;
}
}
"Fantastisch initiatief! Het is geweldig om te zien dat jullie actief bezig zijn met geld ophalen voor de vereniging. Het is cruciaal om de betrokkenheid van de gemeenschap te vergroten en zo de ondersteuning te versterken. Samen kunnen we bijdragen aan het succes van de vereniging en haar doelen realiseren.