Billy ONeal
Billy ONeal

Reputation: 106549

Immutable Design: Dealing with Constructor Insanity

For various reasons I'd like to start using more immutable types in designs. At the moment, I'm working with a project which has an existing class like this:

public class IssueRecord
{
    // The real class has more readable names :)
    public string Foo { get; set; }
    public string Bar { get; set; }
    public int Baz { get; set; }
    public string Prop { get; set; }
    public string Prop2 { get; set; }
    public string Prop3 { get; set; }
    public string Prop4 { get; set; }
    public string Prop5 { get; set; }
    public string Prop6 { get; set; }
    public string Prop7 { get; set; } 
    public string Prop8 { get; set; } 
    public string Prop9 { get; set; }
    public string PropA { get; set; }
}

This class represents some on-disk format which really does have this many properties, so refactoring it into smaller bits is pretty much out of the question at this point.

Does this mean that the constructor on this class really needs to have 13 parameters in an immutable design? If not, what steps might I take to reduce the number of parameters accepted in the constructor if I were to make this design immutable?

Upvotes: 17

Views: 1521

Answers (6)

aiodintsov
aiodintsov

Reputation: 2605

If your intent is prohibit assignments during compilation time, than you have to stick to constructor assignments and private setters. However it has numerous disadvantages - you are not able to use new member initialization, nor xml deseralization and etc.

I would suggest something like this:

    public class IssuerRecord
    {
        public string PropA { get; set; }
        public IList<IssuerRecord> Subrecords { get; set; }
    }

    public class ImmutableIssuerRecord
    {
        public ImmutableIssuerRecord(IssuerRecord record)
        {
            PropA = record.PropA;
            Subrecords = record.Subrecords.Select(r => new ImmutableIssuerRecord(r));
        }

        public string PropA { get; private set; }
        // lacks Count and this[int] but it's IReadOnlyList<T> is coming in 4.5.
        public IEnumerable<ImmutableIssuerRecord> Subrecords { get; private set; }

        // you may want to get a mutable copy again at some point.
        public IssuerRecord GetMutableCopy()
        {
            var copy = new IssuerRecord
                           {
                               PropA = PropA,
                               Subrecords = new List<IssuerRecord>(Subrecords.Select(r => r.GetMutableCopy()))
                           };
            return copy;
        }
    }

IssuerRecord here is much more descriptive and useful. When you pass it somewhere else you can easily create immutable version. Code that works on immutable should have read-only logic, so it should not really care if it is the same type as IssuerRecord. I create a copy of each field instead of just wrapping the object because it may still be changed somewhere else, but it may not be necessary especially for sequential sync calls. However it is safer to store full immutable copy to some collection "for later". It may be a wrapper though for applications when you want some code to prohibit modifications but still have ability to receive updates to the object state.

var record = new IssuerRecord { PropA = "aa" };
if(!Verify(new ImmutableIssuerRecord(record))) return false;

if you think in C++ terms, you can see ImmutableIssuerRecords as "IssuerRecord const". You have to take extracare though to protect objects that are owned by your immutable object that's why I suggest to create a copy for all children (Subrecords example).

ImmutableIssuerRecord.Subrecors is IEnumerable here and lacks Count and this[], but IReadOnlyList is coming in 4.5 and you can copy it from docs if wanted (and make it easy to migrate later).

there are other approaches as well, such as Freezable:

public class IssuerRecord
{
    private bool isFrozen = false;

    private string propA;
    public string PropA
    { 
        get { return propA; }
        set
        {
            if( isFrozen ) throw new NotSupportedOperationException();
            propA = value;
        }
    }

    public void Freeze() { isFrozen = true; }
}

which makes code less readable again, and does not provide compile-time protection. but you can create objects as normal and then freeze them after they are ready.

Builder pattern is also something to consider but it adds too much of "service" code from my point of view.

Upvotes: 0

Alexei Levenkov
Alexei Levenkov

Reputation: 100547

To decrease number of arguments you can group them into sensible sets, but to have truly immutable object you have to initialize it in constructor/factory method.

Some variation is to create "builder" class that you can configure with fluent interface and than request final object. This would make sense if you actually planning to create many of such objects in different places of the code, otherwise many arguments in one single place maybe acceptable tradeoff.

var immutable = new MyImmutableObjectBuilder()
  .SetProp1(1)
  .SetProp2(2)
  .Build();

Upvotes: 14

Reed Copsey
Reed Copsey

Reputation: 564413

Does this mean that the constructor on this class really needs to have 13 parameters in an immutable design?

In general, yes. An immutable type with 13 properties will require some means of initializing all of those values.

If they are not all used, or if some properties can be determined based on the other properties, then you can perhaps have one or more overloaded constructors with fewer parameters. However, a constructor (whether or not the type is immutable) really should fully initialize the data for the type in a way that the type is logically "correct" and "complete."

This class represents some on-disk format which really does have this many properties, so refactoring it into smaller bits is pretty much out of the question at this point.

If the "on-disk format" is something that's being determined at runtime, you could potentially have a factory method or constructor which takes the initialization data (ie: the filename? etc) and builds the fully-initialized type for you.

Upvotes: 13

Netfangled
Netfangled

Reputation: 2081

You could use a combination of named and optional arguments in your constructor. If the values are always different, then yes, you're stuck with an insane constructor.

Upvotes: 3

spender
spender

Reputation: 120450

Perhaps keep your current class as it is, providing sensible defaults if possible and rename to IssueRecordOptions. Use this as a single initializing parameter to your immutable IssueRecord.

Upvotes: 3

Carsen Daniel Yates
Carsen Daniel Yates

Reputation: 2032

You could make a struct, but then you would still have to declare the struct. But there are always arrays and such. If they are all the same data type you can group them in several ways, such as an array, list or string. It does appear that you are right though, all of your immutable types must go through the constructor in some way, weather through 13 parameters, or through a struct, array, list, etc...

Upvotes: 2

Related Questions