As a thought experiment on a hobby project, I've bee开发者_开发技巧n 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?
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.
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.
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.
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.
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".
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!
}
精彩评论