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.

  • 2
    Given your constraints, it looks like you need a class. – Robert Harvey Feb 03 '20 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 03 '20 at 19:40
  • @DocBrown unfortunately there is no obvious default value. – Bill Tür stands with Ukraine Feb 03 '20 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 03 '20 at 20:17
  • @Theraot valid values have to have 5 or 6 digits. The first 4 can have any value between 1000 and 9999, the remaining are either between 1 and 4 or 1 and 12. – Bill Tür stands with Ukraine Feb 03 '20 at 20:22
  • @Theraot I do care for the 0 beacuse this is the default value of an `int` – Bill Tür stands with Ukraine Feb 03 '20 at 20:23
  • Let me ask this differently: if you could have a parameterless constructor in a struct, what kind of default initialization would you choose? – Doc Brown Feb 03 '20 at 20:41
  • @DocBrown I'd throw an exception just as I would do when the parametrized constructor gets an invalid value. – Bill Tür stands with Ukraine Feb 03 '20 at 20:42
  • 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 03 '20 at 20:58
  • How are these values/objects going to be used? (Are they going to be passed around a lot? How often will they be created? In what context are they going to be retrieved? What do you do with them - comparisons, computation?) – Filip Milovanović Feb 03 '20 at 21:06
  • @MartinK 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. – Bill Tür stands with Ukraine Feb 03 '20 at 21:06
  • @FilipMilovanović they are either read from a database or created from user input. They are mainly used as Dictionary keys or key properties for more complex business objects. – Bill Tür stands with Ukraine Feb 03 '20 at 21:12
  • I see. Well, there is a way to implement the parameterless constructor of a struct in .NET, but won't compile in C#, you got to do some [trickery](https://codeblog.jonskeet.uk/2008/12/10/value-types-and-parameterless-constructors/). However, there are workarounds to get an all zero value even if the parameterless constructor does not let you. How about having a dummy value in your dictionary (so you can return that for the default key)? It could be a null object pattern. – Theraot Feb 03 '20 at 21:55

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
  • 38,972
  • 9
  • 88
  • 121
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
  • 102,279
  • 24
  • 197
  • 315
  • You're right, _integer_ is not all to precise, I edited the question to be more precise about the valid values. – Bill Tür stands with Ukraine Feb 03 '20 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 stands with Ukraine Feb 03 '20 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 03 '20 at 21:02
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
  • 199,015
  • 33
  • 367
  • 565