3

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.

  • 2
    Given your constraints, it looks like you need a class. – Robert Harvey Feb 3 at 19:12
  • 1
    Is there a meaningful default value other than 0? If so, you could encapsulate the access to the value through a property-getter and replace the 0 by the default value as soon as the value is accessed the first time. That keeps the "0"-check an implementation detail of that property. – Doc Brown Feb 3 at 19:40
  • @DocBrown unfortunately there is no obvious default value. – Bill Tür Feb 3 at 20:01
  • Do we know anything else about the valid values other than 0 is not one of them? Is this only for integers? Are there valid negatives? Is there a minimum or a maximum? Can the set of valid values change in runtime? Why is 0 special to the point of you being aware it is not valid? – Theraot Feb 3 at 20:17
  • 1
    I don't get the question... use the tools the language offers you to implement your requirements. That's a class. Why is having a struct a goal or requirement? – Martin K Feb 3 at 20:58
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.

| improve this answer | |
3

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.

| improve this answer | |
  • You're right, integer is not all to precise, I edited the question to be more precise about the valid values. – Bill Tür Feb 3 at 20:41
  • You wrote Only allow construction to succeed when the validation rules allow it. That's exactly what I want to do and why I want to do the validation in the constructor. – Bill Tür Feb 3 at 20:44
  • 5
    "Also you're acting like you must be able to call this through a parameterless constructor" - it's not that; the OP is considering to use C# structs (value types), however, C# forbids defining a custom default ctor for value types, so this creates a loophole where one can create an invalid object. – Filip Milovanović Feb 3 at 21:02
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

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.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.