ToSystem - My Primitive Protection

I'd written up an almost complete post about my ToSystem abstraction... and it got deleted... About 4 months later, I'm re-writing it. Mostly because I have NO idea whast I wrote before. :)

This ToSystem type had an evolution with my understanding of "objects" and "types" while developing systems.

It started with my initial misunderstanding... That's not fair. It wasn't a misunderstanding... just a poor understanding. I had an IText interface instead of a string type to pass through the system.

This interface was

public interface IText
{
    string String();
}

Super simple, and just had a way to get the string value out.

It was absolutely better than string. Just not as good as I've found other things to be. It forced certain behaviors into the code that I wanted to see.

Clearly as this is a story about how it's evolved, this isn't the final form.

I knew it wasn't "the right thing", but it was the best I understood.
Towards the end, it was clear that an abstract class would have worked better.

Not everthing that was text was a IText. That's something I wanted to avoid as best I could.
We have an IPassword and IUsername to control those distinctly. They... had a way to convert to an IText which then could be converted to string.

public class Username : IUsername
{
    private readonly IText _username;

    public Username(string username) : this(new TextOf(username)) { }

    public Username(IText username) => _username = username;

    public IText Value() => new TextOf(_username);

    public bool IsBlank() => _username.IsBlank();
}

public interface IUsername : IScalar<IText>
{
    bool IsBlank();
}

public interface IScalar<out T>
{
    T Value();
}

The IUsername only exposes the behavior we want. Which is the goal of objects! This is the HUGE WIN over using string. Hopefully you understand that already. :)

The IScalar concept is something pulled directly from Elegant Objects. I tried to use it a little more widespread... felt REALLY wrong.

It's something I don't use anymore. Served a great purpose here... but then... No more.

One thing that REALLY felt wrong was the duplication of code. Which was easy to see because I followed the principle of "Make similar things more similar".

There were two types of implementation of IText. One that took a string

public class CLASS_NAME_Text : IText
{
    private readonly IText _origin;
    /* [other fields] */

    public CLASS_NAME_Text(IText origin /* [other fields] */)
    {
        _origin = origin;
        /* [other fields] */
    }

    //Brevity
}

and one that took an IText

public class LowerText : IText
{
    private readonly IText _origin;

    public LowerText(IText origin) => _origin = origin;

    public string String() => _origin.String().ToLowerInvariant();
}

All of the ones that took a string had and stored the value in a field called _origin...
The only difference for the types that take an IText is that they store a different type.

So damn repetative.

Using the IText also forced classes that did data manipulation to be ... verbose... Now - I didn't like the verbosity that I'm about to show you; but it was isolated; and that was a win.

public class PrefixEditorText : IText
{
    public string String() => (ShouldRemove() ? SubText() : _origin).String();

    private IText SubText() => new SubText(_origin, _remove.String().Length);

    private bool ShouldRemove() => _origin.String().StartsWith(_remove.String());
}

The IText#String method is invoked 4 times in this class.

It's a common sight - Get the string out to do something with it.

public class LowerText : IText
{
    private readonly IText _origin;

    public LowerText(IText origin) => _origin = origin;

    public string String() => _origin.String().ToLowerInvariant();
}

Ugggg... ugly. Isolated - so better. I'm good with better. I recognize that we need to reduce the code smell, not try to eliminate it. We eliminate over time.

This is where it started.

By the end of the project we knew that the interface was a terrible way to do it. We wanted to remove some of the smells we saw with this.

We had the opportunity on a new project to spin up the new way we thought would be great; an abstract class!

This is represented in my FluentTypes repo.

I have a few variations and improvements on this style over time; but it's definitely the core of it.
Let's look at the Text class.

public abstract class Text
{
    public static readonly Text NullObject = new NullText();

    public static implicit operator string(Text origin) => origin.Value();

    protected abstract string Value();
}

This doesn't store anything for us; we don't always know what we want to store. What we know is that we have to turn our Text into a string. I wanted to get rid of the .String() call. We've added the implicit operator string for the base type. Can't do that in an interface. Instead of the String method, each derived class just has to implement a method converting to string, and implicitness is used.

The simplest example is the TextOf

public sealed class TextOf : Text
{
    private readonly string _value;

    public TextOf(string value) => _value = value;

    protected override string Value() => _value;
}

It just holds the string for us.

Another simple example that allows us to not have to force conversions is appending

internal sealed class AppendText : Text
{
    private readonly Text _origin;
    private readonly Text _suffix;

    public AppendText(Text origin, Text suffix)
    {
        _origin = origin;
        _suffix = suffix;
    }

    protected override string Value() => _origin + _suffix;
}

The compiler does inference and determines the implicit casting _origin and _suffix need.

The result of this is that Text is passed through the system. Even when it's not /really/ text.

The next project I was on, I got to experiment with removing this smell. We had a DocumentControlNumber.
This was a string of characters and numbers... a string... some Text... but I didn't want the sytem to see Text... It was a DocumentControlNumber. That's what I wanted the rest of the code to see.

So...

//from memory
public abstract class DCN
{
    public static implicit operator string(DocumentControlNumber origin) => origin.Value();

    protected abstract string Value();
}

public sealed class GeneratedDocumentControlNumber : DCN{
    //Generates the DCN somehow
}

Now; the system would only have the DCN. You can't get the data out without a hard cast (string)dcn which is gonna look REALLY off in a code base that doesn't use string.

But - passing it to some 3rd party code that expected a string; works perfect.

Now... Every type in the system has to have some form of

public static implicit operator SSYTEM_TYPE(CUSTOM_TYPE origin) => origin.Value();

Which... sure... Better. It's better than what I had before.

But it's repetition. It's duplication. Duplication means missing an abstraction.

Which lead to the ToSystemType. It's still getting some slight tweaks as I find little things... but much less need to adjust it than I've felt in the previous ways.

At the base - Just replaces the duplication

[DebuggerDisplay("{AsSystemType()}")]
internal abstract class ToSystemType<TSystemType>
{
    public static implicit operator TSystemType(ToSystemType<TSystemType> origin) => origin.AsSystemType();

    public abstract TSystemType AsSystemType();
}

and that's it.

What I could do for my custom types is

internal sealed class GeneratedDocumentControlNumber : ToSystemType<string>{
    //Stuff
}

But... I've not been a fan of this. I'd rather use Marker Classes (much like marker interfaces and do something like

internal abstract class TextResult: ToSystemType<string> { }

internal sealed class GeneratedDocumentControlNumber : TextResult{
    
    public override string AsSystemType() => //returns something
}

This is a bit on the preference side... but it's what I prefer.

Now... We don't have to have our Domain Classes know how to convert to system types. Just know the type they want to convert to.

ToSystemType being an abstract class makes it the only class you can inherit from. It hasn't seriously impacted me because I don't do a lot of inheritance. We also can't use this pattern to convert to multiple types.
I don't think there's a lot of use cases for the SAME type being represented to external systems by multiple different types. If so, I have a 'converter'

internal sealed class SomeTypeConvertedToDouble : ToSystemType<double>{
    ctor(SomeType tr) => _tr = tr;

    public override double AsSystemType() => //do something to return a double
}

I like this way. It forces us to separate different behaviors that are required by external systems. There's ways to put it all in a single class... but it breaks patterns... I like things the same.

The ToSystemType I showed above; works great... in non-async flows. I've had plenty of cases where the data came from external sources...
I don't like putting sync over async... so I had to create an async version of this.

I've created a few... some were a little wild. As long as an object has the correct methods, it's awaitable. You can await an object.

await _foo;

In the scenario I was playing with this... it was an async call returning a task... Or something... It's been a while... I just remember having to write

await await _foo

... and at this point I decided that awaiting an instance variable... twice... was not something I was keen on seeing in the code. It breaks WAY too many norms for C#. I'll flex norms all day every day... and break those I think need breaking... but to break it that far... sorry... I'll take easier to understand verbose code over convoluted concise code. It made things super smooth for me... just... not what I'd want to force any other developer into. I had to understand other ways.

What I've ended up with is something like

//Async Version
[DebuggerDisplay("{AsSystemType().Result}")]
public abstract class ToSystemType<TSystemType>
{
    public static implicit operator TSystemType(ToSystemType<TSystemType> origin)
    {
        Task<TSystemType> systemType = origin.AsSystemType();
        Task.WaitAll(systemType);
        return systemType.Result;
    }

    public abstract Task<TSystemType> AsSystemType();

    public TaskAwaiter<TSystemType> GetAwaiter() => Value().GetAwaiter();
}

Again - these shift around a little as I muck about; bit not significantly.

And example of the extension of the async ToSystemType class

internal abstract class Text : ToSystemType<string>{}
internal sealed class ReplaceText : Text
{
    private readonly Text _origin;
    private readonly Text _token;
    private readonly Text _value;

    public ReplaceText(Text origin, Text token, Text value)
    {
        _origin = origin;
        _token = token;
        _value = value;
    }
    protected override async Task<string> Value() => ( await _origin).Replace(await _token, await _value);
}

The use of this is just

db.SaveUsername(usernameInstance);
db.SaveUsername(await usernameInstance.ToSystemType());

We may have noticed in ToSystemType; the implicit operator is returning the raw type. No await needed. We could make it a task, but then we have to wait on an object... and that's breaking a pattern I don't want to.

So; we force sync over async with the use of Task.WaitAll. There's probably better ways here - it was an idea that didn't seem terrible. :)

And there we have it... the long journey for my discovery of abstracting out implicit casts in C# so I can use domain specific types... the start of a Domain Specific Langauge... to convey the intent and meaning of the system being built.

Show Comments