MarketStrongly typed identifier
Company Profile

Strongly typed identifier

A strongly typed identifier is user-defined data type which serves as an identifier or key that is strongly typed. This is a solution to the "primitive obsession" code smell as mentioned by Martin Fowler. The data type should preferably be immutable if possible. It is common for implementations to handle equality testing, serialization and model binding.

Examples
Passing a strongly typed identifier throughout the layers of an example application. C# C# have records which provide immutability and equality testing. The record is sealed to prevent inheritance. It overrides the built-in ToString() method. This example implementation includes a static method which can be used to initialize a new instance with a randomly generated globally unique identifier (GUID). /// /// Represents a user identifier. /// /// The user identifier. public sealed record UserId(Guid Id) { /// /// Initializes a new instance of the record. /// /// A new UserId object. public static UserId New() => new(Guid.NewGuid()); public override string ToString() => Id.ToString(); } C++ C++ has structs but not immutability so here the id field is marked as private with a method named getId() to get the value. import std; using std::string; class UserId { private: const string id; public: explicit UserId(const string& id): id{id} {} nodiscard string getId() const noexcept { return id; } nodiscard bool operator==(const UserId& rhs) const noexcept { return id == rhs.getId(); } }; Crystal Crystal's standard library provides the record macro for creating records which are immutable structs and lets you create override the built-in to_s method. require "uuid" • Represents a user identifier. record UserId, id : String do def initialize() @id = UUID.v4.to_s end def to_s(io) io D D have immutable structs. import std; /** Represents a user identifier. */ immutable struct UserId { immutable UUID id; /** Initializes a new instance of the UserId struct. */ this(immutable string id) { this.id = UUID(id); } public static UserId create() { return UserId(randomUUID.toString()); } string toString() { return this.id.toString(); } } Dart Dart have classes with operator overloading. import 'package:meta/meta.dart'; /// Represents a user identifier. @immutable final class UserId { final String id; /// Initializes a new instance of the UserId struct. const UserId(this.id); @override operator ==(other) => other is UserId && other.id == id; @override int get hashCode => id.hashCode; @override String toString() => id; } F# F# lets you create override the Equals, GetHashCode and ToString methods. open System /// /// Represents a user identifier. /// /// The user identifier. type UserId(id: Guid) = member x.id = id static member New() = Guid.NewGuid() static member Empty = Guid.Empty override x.Equals(b) = match b with | :? UserId as p -> id = p.id | _ -> false override x.GetHashCode() = hash id override x.ToString() = id.ToString() Go Go have structs which provide equality testing. Go however does not provide immutability. // Represents a user identifier. type UserId struct{ id string } // Creates a new user identifier. func NewUserId(id string) UserId { return UserId{id: id} } func (x UserId) String() string { return x.id } Groovy Groovy have record classes which provide immutability and equality testing. /** * Represents a user identifier. * * @param id The user identifier. */ record UserId(String id) { String toString() { id } } Haskell Haskell can create user-defined custom data types using the newtype keyword. It provides equality testing using the Eq standard class and printing using the Read and Show standard classes. -- Represents a user identifier. newtype UserId = UserId String deriving (Eq, Read, Show) Java Java have records which provide equality testing. The record is declared using the final modifier keyword to prevent inheritance. It overrides the built-in toString() method. import java.util.UUID; /** * Represents a user identifier. * @param id The user identifier. */ public final record UserId(UUID id) { /** * Initializes a new instance of the UserId record. * @return A new UserId object. */ public static UserId newId() { return new UserId(UUID.randomUUID()); } public String toString() { return id.toString(); } } JavaScript This JavaScript example implementation provides the toJSON method used by the JSON.stringify() function to serialize the class into a simple string instead of a composite data type. It calls Object.freeze() to make the instance immutable. It overrides the built-in toString() method and the valueOf() method. class UserId { #id; constructor(id) { if (id == undefined) { throw new TypeError("Argument is null or undefined."); } this.#id = id; Object.freeze(this); } static empty = new this.prototype.constructor("00000000-0000-0000-0000-000000000000"); static new() { return new this.prototype.constructor(crypto.randomUUID()); } equals(id) { return id instanceof this.constructor && this.#id === id.valueOf(); } toJSON() { return this.#id; } toString() { return this.#id; } valueOf() { return this.#id; } } Julia Julia have immutable composite data types. using UUIDs "Represents a user identifier." struct UserId id::UUID end Base.string(userId::UserId) = userId.id Kotlin Kotlin have "inline classes". /** * Represents a user identifier. * * @property id The user identifier. * @constructor Creates a user identifier. */ @JvmInline public value class UserId(public val id: String) { override fun toString() = id } Nim Nim have "distinct types". • Represents a user identifier. type UserId* = distinct string PHP This PHP example implementation implements the __toString() magic method. Furthermore, it implements the JsonSerializable interface which is used by the built-in json_encode function to serialize the class into a simple string instead of a composite data type. The class is declared using the final modifier keyword to prevent inheritance. PHP has traits as a way to re-use code. /** * Represents a user identifier. */ final class UserId implements JsonSerializable { use StronglyTypedIdentifier; } /** * Provides methods for use with strongly typed identifiers. */ trait StronglyTypedIdentifier { /** * Initializes a new instance of the UserId object. * @param string $id The user identifier. */ public function __construct(public readonly string $id) {} /** * Creates a new user identifier. */ public static function new(): self { return new self(bin2hex(random_bytes(16))); } public function jsonSerialize(): string { return $this->id; } public function __toString(): string { return $this->id; } } Python Python has data classes which provides equality testing and can be made immutable using the frozen parameter. It overrides the __str__ dunder method. This example implementation includes a static method which can be used to initialize a new instance with a randomly generated universally unique identifier (UUID). import uuid from dataclasses import dataclass from uuid import UUID @dataclass(frozen=True) class UserId: """Represents a user identifier.""" id: UUID @staticmethod def new() -> Self: """Create a new user identifier.""" return __class__(uuid.uuid4()) def __str__(self) -> str: return str(self.id) Python also has NewType which can be used to create new data types. from typing import NewType UserId: NewType = NewType('UserId', int) Raku Raku have classes which provides equality testing and are immutable. It overrides the built-in method. This example overrides the default gist method. It uses roles which are mixed in into classes to be re-usable. role StronglyTypedIdentifier { has Str $.id is required; multi method new(Str $id) { self.bless(:$id); } multi method gist(StronglyTypedIdentifier:U:) { self.^name } multi method gist(StronglyTypedIdentifier:D:) { self.id } method Str() { "$!id" } method empty() { self.new("00000000-0000-0000-0000-000000000000") } } class UserId does StronglyTypedIdentifier {} Ruby Ruby have data classes which provides equality testing and are immutable. It overrides the built-in to_s method. This example implementation includes a static method which can be used to initialize a new instance with a randomly generated universally unique identifier (UUID). require 'securerandom' • Represents a user identifier. UserId = Data.define(:id) do # Create a new user identifier. def self.create self.new(SecureRandom.uuid) end def self.empty self.new('00000000-0000-0000-0000-000000000000') end def to_s id end end Rust In Rust this can be done using a tuple struct containing a single value. This example implementation implements the Debug and the PartialEq traits. The PartialEq trait provides equality testing. // Represents a user identifier. • [derive(Debug, PartialEq)] pub struct UserId(String); Scala Scala have case classes which provide immutability and equality testing. The case class is sealed to prevent inheritance. import java.util.UUID /** Represents a user identifier. * * @constructor * Create a new user identifier. * @param id * The user identifier. */ sealed case class UserId(id: UUID) object UserId: /** Initializes a new instance of the UserId class. */ def create(): UserId = UserId(UUID.randomUUID()) Swift Swift have the CustomStringConvertible protocol which can be used to provide its own representation to be used when converting an instance to a string, and the Equatable protocol which provides equality testing. import Foundation /// Represents a user identifier. struct UserId: CustomStringConvertible, Equatable { private let id: UUID init(_ id: UUID) { self.id = id } var description: String { return id.uuidString.lowercased } /// Creates a new user identifier. static func new() -> Self { return Self(UUID()) } } Zig Zig have structs with constants but by design does not have operator overloading and method overriding. /// Represents a user identifier. const UserId = struct { value: i32, /// Initializes a new instance of the UserId struct. pub fn init(value: i32) UserId { return UserId{ .value = value }; } }; == See also ==
tickerdossier.comtickerdossier.substack.com