5

I have a business object which is basically a wrapper around a value of type int. There are some constraints for the value/object:

  • Not every value in int's range is valid1
  • The valid values are not a predefined discrete set, therefore an enum is not an option
  • Two objects with the same value are always considered equal
  • The validity of a value should be checked in the constructor
  • 0 is not an valid value

If I just consider the first three oft theses constraints I'd say this is a predestined use case for an immutable struct (which I would prefer). The problem lies within the last two ones:

Since I can't have a parameterless constructor in a struct an object with 0 as a value can be constructed. I could treat this as a special value like a null value. But this would force me to "null check" and I could take a class as well. Are there any more reasons for using a struct in this use case?


1 To be more precise at the valid values: they have to have 5 or 6 digits. The first 4 can have any value between 1000 and 9999, the remaining digit(s) are either between 1 and 4 or 1 and 12.

3 Answers3

3

To be more precise at the valid values: they have to have 5 or 6 digits. The first 4 can have any value between 1000 and 9999, the remaining digit(s) are either between 1 and 4 or 1 and 12

Given these requirements, one way to solve this specifically with C# is to use a struct, but to take into account that all types in .NET are zero-initialised. To work around it, we therefore need to incur a slight performance overhead:

public readonly struct IntWrapper
{
    private const int ZeroOffSet = 10_001;

    private readonly int _valueOffsetFromZero;

    public IntWrapper(int value)
    {
        // validate value, throw if invalid

        _valueOffsetFromZero = value - ZeroOffSet;
    }

    public int Value => _valueOffsetFromZero + ZeroOffSet;
}

This solution ensures that if default(IntWrapper) or new IntWrapper() are used, the value is valid, working around the "zero default" issue of .NET structs. And since it's a struct, there's no issues around null either. The downside is that every call to Value incurs the overhead of _valueOffsetFromZero + ZeroOffSet, which could be an issue if it's repeatedly read in apps where performance is critical.

David Arno
  • 39,599
  • 9
  • 94
  • 129
2

You need a requirements review.

  1. Not every integer value is valid

This is a very weak requirement. There is no computer in existence for which this isn't true. Some ints are so long they'd fill up your HD. So having this requirement here doesn't help much. Consider restating what this was meant to say or removing this entirely.

  1. The valid values are not a predefined discrete set, therefore an enum is not an option

This is useful info

  1. Two objects with the same value are always considered equal

We call these value objects. They don't have to have getters and setters.

  1. The validity of a value should be checked in the constructor

This leaves out mutable beans. No setters. Immutable objects are still eligible. What makes this weak is that this is about design & implementation not requirements. It doesn't tell us why it has to be a constructor.

  1. 0 is not an valid value

This is woefully incomplete. Is there a max? A min? As the dynamic set of valid values changes, must all existing objects be re-validated? Is this set unique to each object?

Also you're acting like you must be able to call this through a parameterless constructor yet you never gave that as a requirement or justified this need.

The most elaborate design for this that I'd entertain would be a class that took an int value and an immutable validation rules object that could be reused but not changed once in place. Only allow construction to succeed when the validation rules allow it.

This lets you react to change yet keep everything immutable. Keep in mind this is overdesigned simply because these requirements are so vague. 5 minutes spent making the requirements clearer could save 5 days worth of coding.

candied_orange
  • 119,268
2

In a comment, you wrote

with a struct I didn't need to explicitely implement equality and I didn't have to do null checks. Therefore I'd prefer a struct.

If the requirement is just to wrap an int, implementing equality in a class should be pretty trivial. And for avoiding null checks, I think your best bet is to use the new C# 8.0 feature "Nullable Reference Types" - which is arguably a misnomer, since it makes reference types not-nullable by default.

So I would recommend to use a class, and live with the current restrictions of the C# language until your team is ready to switch to C# 8.0.

Doc Brown
  • 218,378