3
\$\begingroup\$

Trying to combine functional style (immutable objects) and flexibility of property setters. For the sake of example, let’s say we have a soil types table with two attributes: Color and Name. I am looking for a way to alternate Names, but not Color. Here is how I solved it:

        // retrieving: all objects are immutable
        SoilTypes types = SoilTypes.Default;
        ISoilType clay1 = types.Clay;
        ISoilType clay2 = types[3];

        // derive an alternated immutable copy
        SoilTypes altTypes = types
            .With(tt =>
            {
                // tt.SensitiveFines.Color is still read only
                tt.SensitiveFines.Name = "Very sensitive fines!";
                tt[2].Name = "Purely Organic soil!";
            });

        // retrieving: everything is immutable
        ISoilType sensitiveFines = altTypes.SensitiveFines;

Where this interface is immutable:

public interface ISoilType
{
    Color Color { get; }
    string Name { get; }
}

And this class is mutable:

public class SoilType : ISoilType
{
    public static implicit operator SoilType((Color Color, string Name) tuple) =>
         new SoilType(tuple.Color, tuple.Name);

    internal SoilType(ISoilType source)
        : this(source.Color, source.Name)
    {
    }

    internal SoilType(Color color, string name)
    {
        Color = color;
        Name = name;
    }

    public Color Color { get; }
    public string Name { get; set; }
}

And this non-generic class is immutable:

public class SoilTypes : SoilTypes<ISoilType>
{
    public static SoilTypes Default = new SoilTypes(
        (White, "Undefined"),
        (Red, "Sensitive Fines"),
        (Green, "Organic Soil"),
        (Blue, "Clay"),
        (Orange, "Silty Clay"));

    public SoilTypes(params SoilType[] types)
        : base(types)
    {
    }

    public SoilTypes With(Action<SoilTypes<SoilType>> update)
    {
        var copy = this
            .Select(t => new SoilType(t))
            .ToArray();

        update(new SoilTypes<SoilType>(copy));
        return new SoilTypes(copy);
    }
}

while this generic base used in both situations:

public class SoilTypes<TType> : ReadOnlyCollection<TType>
    where TType : ISoilType
{
    internal SoilTypes(TType[] types)
        : base(types)
    {
    }

    public TType Undefined => this[0];
    public TType SensitiveFines => this[1];
    public TType OrganicSoil => this[2];
    public TType Clay => this[3];
    public TType SiltyClay => this[4];
}
\$\endgroup\$
7
  • \$\begingroup\$ // tt.SensitiveFines.Color is still read only -- could you elaborate what you mean/wht is Color "read only"? I don't see how the Color attribute is different from the Name in the code provided. \$\endgroup\$ Apr 22, 2017 at 17:50
  • \$\begingroup\$ @IgorSoloydenko SoilType class does not have a setter for Color. \$\endgroup\$ Apr 22, 2017 at 17:53
  • \$\begingroup\$ Oh, right. Hard to notice a three-letter long missing keyword. :) \$\endgroup\$ Apr 22, 2017 at 17:57
  • 1
    \$\begingroup\$ I'm trying very hard but I still have no clue what this is about ;-( what advantages this have over a builder pattern? Why do you want to change the Name in a semi-mutable-immutable fashion? Btw. the SoilTypes should be SoilTypeCollection. Collections do not have plural names ;-] \$\endgroup\$
    – t3chb0t
    Apr 23, 2017 at 8:07
  • \$\begingroup\$ @t3chb0t Compiler will not allow you to write SoilTypes.Default.Clay.Name = "Dirty thing" but do allow to write SoilTypes.Default.With(alt => alt.Clay.Name = "Dirty thing") to derive and override SoilTypes content, so we combine immutability with the syntactical efficiency of property assignments. My API is way wider then 5x2 table, so I do need it. P.S. I feel guilty about SoilTypes name, but it is how domain experts reference it - it is not just a technical artifact (container) - it is actually a business object. \$\endgroup\$ Apr 23, 2017 at 17:25

3 Answers 3

4
\$\begingroup\$

I'm afraid this is not fully immutable becasue I am able to change the Name with a simple cast:

altTypes.Dump();
((SoilType)altTypes.SensitiveFines).Name = "foo";
altTypes.Dump();

The underlying data type is still SoilType so the interface does not protect the data from being overriden.

Consider a user that writes a function like this one because he doesn't like interfaces :-)

public static void foo(SoilType bar)
{
    bar.Name = "new name";
}

and calls it

foo((SoilType)altTypes.SensitiveFines);
altTypes.Dump();

Name changed. Unfortunatelly I have no idea how to prevent it yet.

\$\endgroup\$
7
  • \$\begingroup\$ I would say that it is the same trick like using IReadOnlyList<T> as a field type for a mutable collection. It is not bullet proof, but good enough to deliver the point of immutability. Good business logic code has no casting operators at all :) \$\endgroup\$ Apr 23, 2017 at 17:53
  • \$\begingroup\$ @DmitryNogin true, that's why it's better to call AsReadOnly and not just rely on the interface. It's hard to prevent the data from people who doesn't know this rule and try to hack into everything :-] \$\endgroup\$
    – t3chb0t
    Apr 23, 2017 at 18:03
  • \$\begingroup\$ Frankly speaking, I just usually need a tip for myself to understand from the API shape what a hell it was about many years ago... :) Agree, you are right - some kind of mutable AltSoilType (inherited from SoilType) builds a more understandable dichotomy. Thanks! \$\endgroup\$ Apr 23, 2017 at 18:19
  • \$\begingroup\$ C# scares me out on everyday basis - one can not extend a get-only property with a setter after inheriting. Common, it is a total shame. \$\endgroup\$ Apr 23, 2017 at 18:53
  • 1
    \$\begingroup\$ @DmitryNogin This might have something to do with the fact that a getter-only property gets backing field that cannot be accessed from code, see Is it possible to access backing fields behind auto-implemented properties? \$\endgroup\$
    – t3chb0t
    Apr 23, 2017 at 19:02
2
\$\begingroup\$

Guess, I must start with a disclaimer again -- after multiple rereadings, still unsure what exactly the code tries to achieve.

The big confusion (of mine)

There's one thing I really don't like about SoilTypes<TType>, namely the ad hoc-ish mapping to collection entries by index. The consumer of the class will have to know that implementation detail, right?

Don't have a C# compiler in front of me at the moment so I could play with things.

Is there a way to keep public static SoilTypes Default = ... and the public TType Undefined => this[0]; as close together as possible (meaning, in the same class)? Not sure if it is achievable.

On naming the lambda parameters

As a minor thing, I'd note that tt is a bit confusing. Bet, you know we can write (@type => ....

Readability

I know that .With(...) fluent syntax is very well known, I haven't really seen that working with a collection (non-scalar) object, though. In other words, while you're not inventing anything new with this idiom, it's still a bit unintuitive to me. Please disregard this comment if you find it a subjective thing. :)

P.S. Good question, just like many others that you post on CR!

\$\endgroup\$
2
  • \$\begingroup\$ 1) Yep, you will need a VS2017 for this :) It is just to ensure that property assignments operations are syntactically correct in a special context only + implementing prototype inheritance between immutable objects, which are safe to share and reuse. 2) My business logic requires access to records by name and index. 3) Nope, there is no way to put population and shortcuts at the same class. 4) Lambda parameter represents temporary mutable container, so it could probably be @types or alt. \$\endgroup\$ Apr 22, 2017 at 18:32
  • \$\begingroup\$ @DmitryNogin I wish my comment was more helpful \$\endgroup\$ Apr 22, 2017 at 18:34
2
\$\begingroup\$

I have defined mutable/immutable dichotomy in a cleaner way according @t3chb0t answer:

public class SoilType 
{
    internal SoilType(Color color, string name)
    {
        Color = color;
        Name = name;
    }

    public Color Color { get; }
    public string Name { get; protected set; }

    internal AltSoilType Mutable =>
        new AltSoilType(this);
}

And

public class AltSoilType : SoilType
{
    internal AltSoilType(SoilType source)
        : base(source.Color, source.Name)  
    {
    }

    public new string Name 
    {
        get { return base.Name; }
        set { base.Name = value; }
    }

    internal SoilType Immutable => 
        new SoilType(Color, Name);
}
\$\endgroup\$
1
  • \$\begingroup\$ This should be bullet-proof and I share your C# feeling sometimes ;-P like here where it's possible to make the setter protected but not virtual (I tried this previously while experimenting with it) \$\endgroup\$
    – t3chb0t
    Apr 23, 2017 at 20:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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