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.
The strongly typed identifier commonly wraps the data type used as the primary key in the database, such as a string, an integer or universally unique identifier (UUID).
Web frameworks can often be configured to model bind properties on view models that are strongly typed identifiers. Object–relational mappers can often be configured with value converters to map data between the properties on a model using strongly typed identifier data types and database columns.
C# have records which provide immutability and equality testing.[1] The record is sealed to prevent inheritance.[2] It overrides the built-in ToString
method.[3]
This example implementation includes a static method which can be used to initialize a new instance with a randomly generated globally unique identifier (GUID).
C++ have structs but not immutability so here the id field is marked as private with a method named value
to get the value.
ostream& operator << (ostream &os, const UserId &id)
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.[4]
record UserId, id : String do def initialize @id = UUID.v4.to_s end
def to_s(io) io << id end
def self.empty self.new(UUID.empty.to_s) endend
/** Represents a user identifier. */immutable struct UserId
Dart have classes with operator overloading.
/// Represents a user identifier.@immutablefinal class UserId
F# lets you create override the Equals
, GetHashCode
and ToString
methods.
///
Go have structs which provide equality testing. Go however does not provide immutability.
// Creates a new user identifier.func NewUserId(id string) UserId
func (x UserId) String string
Groovy have record classes which provide immutability and equality testing.[6]
Haskell can create user-defined custom data types using the newtype
keyword.[7] It provides equality testing using the Eq
standard class and printing using the Read
and Show
standard classes.
Java have records which provide equality testing.[8] The record is declared using the final
modifier keyword to prevent inheritance. It overrides the built-in toString
method.
/** * Represents a user identifier. * @param id The user identifier. */public final record UserId(UUID id)
This JavaScript example implementation provides the toJSON
method used by the JSON.stringify
[9] function to serialize the class into a simple string instead of a composite data type.It calls Object.freeze
to make the instance immutable.[10] It overrides the built-in toString
method[11] and the valueOf
method.[12]
Julia have immutable composite data types.[13]
"Represents a user identifier."struct UserId id::UUIDend
Base.string(userId::UserId) = userId.id
Kotlin have "inline classes".[14]
Nim have "distinct types".[15] [16]
type UserId* = distinct string
This PHP example implementation implements the __toString
magic method.[17] 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.[18] The class is declared using the final
modifier keyword to prevent inheritance.[19] PHP has traits as a way to re-use code.[20]
/** * Provides methods for use with strongly typed identifiers. */trait StronglyTypedIdentifier
Python has data classes which provides equality testing and can be made immutable using the frozen
parameter.[21] It overrides the __str__
dunder method.[22]
This example implementation includes a static method which can be used to initialize a new instance with a randomly generated universally unique identifier (UUID).
@dataclass(frozen=True)class UserId: """Represents a user identifier."""
id: uuid.UUID
@staticmethod def new -> Self: """Create a new user identifier.""" return __class__(uuid.uuid4)
def __str__(self): return str(self.id)
Python also has NewType
which can be used to create new data types.[23]
UserId = NewType('UserId', int)
Ruby have data classes which provides equality testing and are immutable.[24] 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).
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 endend
In Rust this can be done using a tuple struct containing a single value.[25] This example implementation implements the Debug
[26] and the PartialEq
[27] traits. The PartialEq
trait provides equality testing.
pub struct UserId(String);
Scala have case classes which provide immutability and equality testing.[28] The case class is sealed to prevent inheritance.
/** 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 have the CustomStringConvertible
protocol which can be used to provide its own representation to be used when converting an instance to a string,[29] and the Equatable
protocol which provides equality testing.[30]
/// Represents a user identifier.struct UserId: CustomStringConvertible, Equatable