Neil Barnwell
Neil Barnwell

Reputation: 42125

Strongly-typed integers

As a thought experiment on a hobby project, I've been thinking of a way to ensure that this sort of subtle bug/typo doesn’t happen:

public void MyMethod(int useCaseId)
{
    // Do something with the useCaseId
}

public void SomeOtherMethod()
{
    int userId = 12;
    int useCaseId = 15;
    MyMethod(userId); // Ooops! Used the wrong value!
}

This bug would be hard to find because there’s no compile-time error, and you wouldn’t necessarily even get an exception at run-time. You’d just get "unexpected results".

To resolve this in a simple way, I’ve experimented with using empty enum definitions. Effectively making a user id a data type (without going quite as far as a class or a struct):

public enum UseCaseId { // Empty… }

public enum UserId { // Empty… }

public void MyMethod(UseCaseId useCaseId)
{
   // Do something with the useCaseId
}

public void SomeOtherMethod()
{
   UserId userId = (UserId)12;
   UseCaseId useCaseId = (UseCaseId)15;
   MyMethod(userId); // Compile error!!
}

What d’you think?

Upvotes: 6

Views: 852

Answers (7)

Steve Dunn
Steve Dunn

Reputation: 21761

Very late to the party, but I think you're right to strongly type such concepts instead of using primitives.

You might find this project useful:

https://github.com/SteveDunn/Vogen

Upvotes: 1

Darrel Miller
Darrel Miller

Reputation: 142044

I have wanted to do something similar for a while and your post prompted me to try the following:

 public class Id<T> {
    private readonly int _Id;

    private Id(int id) {
        _Id = id;
    }

    public static implicit operator int(Id<T> id) {
        return id._Id;
    }

    public static implicit operator Id<T>(int id) {
        return new Id<T>(id);
    }
}

which I can use as follows

public void MyMethod(Id<UseCase> useCaseId)
{
   // Do something with the useCaseId
}

public void SomeOtherMethod()
{
   Id<User> userId = 12;
   Id<UseCase> useCaseId = 15;
   MyMethod(userId); // Compile error!!
}

I think passing this type of Id object is better than passing the entire domain object because it makes the contract more explicit between the caller and the callee. If you pass just the id, you know that the callee is not accessing any other property on the object.

Upvotes: 2

wageoghe
wageoghe

Reputation: 27608

Late to the game, but FWIW ... this project on codeplex has defined several "strongly typed" scalars like Angle, Azimuth, Distance, Latitude, Longitude, Radian, etc. Actually, each of these is a struct with a single scalar member and several methods/properties/constructors to manipulate it "correctly". Not really much different than making each of these a class, aside from the fact that these are value types rather than reference types. While I have not used the framework, I can see the value of possibly making these types first class citizens.

Don't know whether it is ultimately a good idea or not, but it does seem useful to be able to write code like this (similar to your original example) with type safety (and value safety) ensured:

Angle a = new Angle(45); //45 degrees
SomeObject o = new SomeObject();
o.Rotate(a); //Ensure that only Angle can be sent to Rotate

or

Angle a = (Angle)45.0;

or

Radians r = Math.PI/2.0;
Angle a = (Angle)r;

It seems like this pattern would be most useful if your domain has a lot of scalar "types" with value semantics and potentially many instances of these types. Modeling each as a struct with a single scalar gives a very compact representation (compared making each a full blown class). While it might be somewhat of a pain to implement this level of abstraction (rather than just using "naked" scalars to represent domain values) and discreteness, the resultant API seems like it would be much easier to use "correctly".

Upvotes: 1

Joey Adams
Joey Adams

Reputation: 43380

If it were Haskell and I wanted to do this, I might do it like:

data UserId    = UserId    Int
data UseCaseId = UseCaseId Int

This way, functions will accept a UserId instead of an Int, and creating a UserId is always explicit, something like:

doSomething (UserId 12) (UseCaseId 15)

This is similar to Niall C.'s solution of creating a type to wrap around an Int. However, it'd be nice if it didn't take 10 lines to implement per type.

Upvotes: 2

Niall C.
Niall C.

Reputation: 10918

If you're going to the trouble of creating new types to hold UserId and UseCaseId, you could almost as easily make them into simple classes and use an implicit conversion operator from int to give you the syntax you want:

public class UserId
{
    public int Id { get; private set; }

    public UserId(int id)
    {
       id_ = id;
    }

    public static implicit operator UserId(int id)
    {
        return new UserId(id);
    }

    public static void Main()
    {
        UserId id1 = new UserId(1);
        UserId id2 = 2;
    }
}

That way, you get the type safety without having to litter your code with casts.

Upvotes: 6

this. __curious_geek
this. __curious_geek

Reputation: 43207

I'd rather prefer to validate the argument in MyMethod and raise appropriate exception in case of error-condition.

public void MyMethod(int useCaseId)
{
    if(!IsValidUseCaseId(useCaseId))
    {
         throw new ArgumentException("Invalid useCaseId.");
    }
    // Do something with the useCaseId
}

public bool IsValidUseCaseId(int useCaseId)
{
    //perform validation here, and return True or False based on your condition.
}

public void SomeOtherMethod()
{
    int userId = 12;
    int useCaseId = 15;
    MyMethod(userId); // Ooops! Used the wrong value!
}

Upvotes: 0

Julius A
Julius A

Reputation: 39622

I personally think it is unnecessary to be honest.
It is down to the developer to implement the logic properly and you can not rely on compile time errors for such bugs.

Upvotes: 1

Related Questions