Making objects elegant in C#

What makes object-oriented programming what it's supposed to be?

There are many great books on the topic, and last year I was very impressed by two of them, the "Elegant Objects" volumes 1-2 by Yegor Bugayenko.

His message resonates with me. Tired of the procedural mess, with its mutating state, NULLs, God classes, and other artifacts of mis-understood OOP in practice, I've been looking for guidance on how to write object-oriented code that best approximates the original idea.

How I understand the differences between procedural, object-oriented, and functional approaches

Before going any further, I'll summarize my understanding of the different programming paradigms. This will set the context for what comes next. Any mistakes I make here will propagate downstream.

By way of example, let's compare a common scenario of capturing user data from an incoming web request and storing them in a database.

1. Procedural

I tell the computer what to do, step-by-step. Data is just data, dumb as a Dodo.

int SaveUser(RequestData data) // assume RequestData being some hashmap
if (data == null) throw new ArgumentNullException(nameof(data));
var person = new Person() { Name = data["Name"], Age = data["Age"] };
person.setAddress(new Address() { City = data["City"], Zip = data["Zip"], street = data["Street"]});
if (person.Age < 18)
{
person.IsUnderage = true;
}
var id = MyDatabase.save(person);
return id;
}

My concerns:

2. Functional

Data is just data, with no behavior, and preferrably is immutable. I write and compose functions that operate on it. The functions are supposed to not have any side effects, always return the same output for the same input, are composable and such.

C# is not a functional language but we can approximate the style:

Maybe<Person> fromRequestData(RequestData data) {
return new Maybe<RequestData>(data).Map<Person>(stub => new Person(stub["Name"], stub["Age"], new Address(stub["City"], stub["Zip"], stub["street"])));
}

// "pure" - I get the exact-same looking Person for the same RequestData
var person = fromRequestData(myRequestData);
// IO - never pure, dealing with the nasty outside world
person.ifSome(p => MyDatabase.save(p));

I like: pure functions that map one value to another, predictably and without throwing Exceptions.

Not so sure about: it isn't idiomatic C#. The Maybe monads are a foreign concept. If you want to do it functionally, F# does this natively and you don't need 3rd party libraries like the amazing Language.Ext.

3. Object-oriented

Objects encapsulate certain data and expose behavior via their methods.

class RequestData
{
public RequestData(Request request)
{
_name = request.Body().get("Name"); // yes, we still have a dict here, I know
//...
}

private string _name;
private int _age;
// etc.
private bool _haveCompleteInfo;

public Person ToPerson()
{
return haveCompleteInfo
? new Person(_name, _age, new Address(_city, _zip, _street)))
: Person.Empty();
}
}
var person = myRequestData.ToPerson();
person.PersistTo(MyDatabase.Adapter());

A few differences:

Just like with the functional example, I can map one object to another. The object itself, however, is trusted to do the mapping.

Exploring truly object-oriented mindset in a toy project

I set out to figure out whether I can write truly object-oriented code in the style of Elegant Objects, that is by delegating functionality to objects and respecting their boundaries.

The project is Blogroll, a webapp I use personally on my tango blog. Its main use case is to store links to my favorite blogs and expose them via a REST API.

The domain model is rather simple: we have a Link, which encapsulates a Web URL, a Blogroll, which is a list of Links, and a few helper classes.

The solution contains the core logic in one project (Blogroll.Common), two persistent storage projects (Blogroll.SQLite and Blogroll.LiteDB), and an ASP.NET Core frontend and API server in a single project (Blogroll.Web).

The web project is where I had the most work waiting for me.

Printers instead of getters

At the end of the day, you need to expose data on the UI layer.

By default, most templating engines expect you to arrive with a procedural mindset.

You are supposed to use properties and make them directly accessible to your template, such as in this Handlebars snippet:

<div>
<span class="title">{{ person.title }}</span>
<span>{{ person.firstName }} {{ person.lastName }}</span>
</div>

That's just as true for Razor, the default templating engine for ASP.NET.

I refused to do so.

Elegant Objects encourage the use of so-called "printers", to make objects capable of "printing" themselves to some media.

To do that, I found no other way than to turn the default way upside down.

The template would have no way to access object's properties, mainly because the object would have no public properties whatsoever!

public interface ILink
{
bool CanRead();

Task<Snippet> LatestContent();
}

A quick look at an implementation of ILink reveals that it does, indeed, encapsulate some data. It just isn't willing to give it back.

public sealed class Link : ILink
{
// ...
private string Name { get; }

private string Url { get; }

private string FeedUrl { get; }
// ...

public bool CanRead() => !string.IsNullOrEmpty(FeedUrl);
// ...
}

Notice that whether or not the Link can be "read" is left up to the Link instance itself. No more of this:

var link = new Link(...);
if (link.FeedUrl != null)
{
// ...
}

What do I do when I want the Link to print itself to some template, then? After all, this is an object that is surfacing on the UI.

Following the cues from Elegant Objects Vol. 2, chapter 5.3 "Printers instead of getters", I set up the following interfaces.

First, the "media" into which the Link will print itself.

public interface IMedia
{
IMedia With(string key, string value);
IMedia With(string key, double value);
IMedia With(string key, object value);
byte[] Bytes();
}

The assumption is that most values as they hit the template will be strings or numbers. And if they must be Objects, they better have a sensible ToString() override.

Next, an interface for objects that wish to print themselves.

public interface IPrintable
{
/// <summary>
/// Returns an IMedia instance into which the object has printed itself.
/// </summary>
IMedia PrePrinted(IMedia media);

/// <summary>
/// Returns the rendered string of an IMedia instance into which the object has printed itself.
/// </summary>
string PrintedTo(IMedia media);
}

The PrePrinted method is used when chaining the printing of nested objects. The PrintedTo method will give you the final rendered template as string.

An IPrintable object will work with any type of media as long as it implements the IMedia interface - the output can be text, HTML, JSON, or whatever else that looks like a string in the end.

This is how Link does it:

public IMedia PrePrinted(IMedia media) => media.With(nameof(Url), Url)
.With(nameof(Name), Name)
.With(nameof(FeedUrl), FeedUrl);

public string PrintedTo(IMedia media) =>
PrePrinted(media).ToString();

The simplest possible media is a TextMedia implementation that works with plain text.

public sealed class TextMedia: IMedia
{
public TextMedia(string template) : this(template, new Dictionary<string, string>()) {}

public TextMedia(string template, IDictionary<string, string> terms)
{
_template = template;
_terms = terms;
}

private readonly IDictionary<string, string> _terms;
private readonly string _template;

public IMedia With(string key, string value)
{
_terms[key] = value;
return new TextMedia(_template, _terms);
}
// more methods

private string TemplateKey(string key) => new StringBuilder(").Append(key).Append(").ToString();

public override string ToString() =>
_terms.Aggregate(_template, (current, kvp) => current.Replace(TemplateKey(kvp.Key), kvp.Value));
}

Its template can look like this:

{{Name}} - {{Url}}

What you see are keys that will be substituted for values, as you would expect. The difference is that the object itself does the printing, not the templating engine. In fact, the IMedia implementation is the templating engine of sorts but it is told to print whatever the object is willing to provide by the object itself.

Wiring it up

This is then wired up with the Razor template like this:

<div class="col-md-6">
<form method="post" asp-controller="Home" asp-action="Add">
@Html.Raw(Model.Form("Views/Manage/_form.hbs"))
<button type="submit" class="btn btn-primary">Add blog</button>
</form>
</div>

The Model referenced above is a viewmodel that provides a method Form that takes a Handlebars partial as an argument.

public string Form(string templatePath) => new FormViewModel($"{_contentRoot}/{templatePath}", _link).ToString();

The FormViewModel instance does this:

public override string ToString() => _link.PrintedTo(new HtmlMedia(File.ReadAllText(_templatePath)));

The Link instance (_link) is given an IMedia instance that prints into this Handlebars template:

Views/Manage/_form.hbs

<div class="form-group">
<label for="Url">Blog address</label>
<input type="text" name="Url" id="Url" class="form-control" required placeholder="https://blog.example.com"
value="{{Url}}"/>

</div>
<div class="form-group">
<label for="Name">Blog name</label>
<input type="text" name="Name" id="Name" class="form-control"
value="{{Name}}"/>

</div>

You will notice that I am mixing Razor and Handlebars. The reason is that I did not figure out how to use Razor in an analogous way I am using Handlebars here. For the purpose of this demo, this is not important at all.

The downsides

Type safety

One thing is lost now: type safety on the UI layer.

Using Razor, I have type-safe templating. Given a @Model declaration, I can only access those properties and methods that my (view)model exposes.

In my approach, keys used in templates are just strings. If I make a typo, nothing gets printed out. I don't get a compile-time warning or any other hint.

That is a definite step back.

Contract between the designer and programmer

When objects are opaque and do not provide any public properties, how does the template designer know how to name the keys/placeholders in the template?

That information can now be provided only in the comments or API docs. Just like with the previous caveat, much is lost. Opportunities for errors await.

These are grave trade-offs that prevented me from declaring victory outright.

On the one hand, I was able to give my objects the respect they deserve, and let them do their work for me without interfering with their decisions, so to speak.

As a result, I lost type safety on the UI layer and left the designer guessing.

I don't yet know how to resolve this trade-off to be more acceptable to me.

Where to go next

I want to avoid writing procedural code as much as I can.

To that end, I favor using the object-oriented approach, and in this project I've explored how far I can go.

It felt like going uphill most of the time. Major OOP frameworks still have a lot of procedural baggage and will fight you if you don't accept it.

I don't. And I am willing to fight. It will just take some more learning for me to be better equipped for this fight.