So Primitive
As it happens now, it's been a little while since my last post. I don't have as much to think through as I did the many prior years.
I'm pretty shitty at the "self-promotion" aspect that a prolific blogger or tech-coach will do. It's not me; and I accept that limitation. Though it's frustrating when I'd like to share more.
ANYWHO....
Let's talk some more about MtgDiscovery Repo!
(reviews the two entries) Apparently I've only written about MonoStates
The Topic
Let's talk about Primitives. This is a core aspect of how I write code - "No Primitives". Whenever I list the MicroObject practices, it's towards the bottom which may make it seem less important. It's not less important, it's one of the biggest; and if you follow the other core practices, primitives just ... go away. It's not as an important practice TO FOCUS ON because the other important practices make it stop happening.
It must BE a practice because it's so important to remember.
Let's look at how I handle "No Primitives".
A History
The start of No Primitives was training with Fred George. I don't recall it being a specific rule to not use primitives; but we did use "No Getters/Setters". This SERIOUSLY limits the amount of primitives moving around your system.
No Getters/Setters is the MOST important of the practices, IMO.
Recently I've started to use DataBags in for ingress and egress to my systems. I'm still feeling it out. Some exist in this repo. I'm trading a smell for obfuscation in some cases.
Yegor's book Elegant Objects is where the practice "No Primitives" got really well defined.
In the UWP app we did for Premera applying these MicroObject practices used something like Yegor does in the book and the code he has online. The "primitive" types; like Text; were a core of what we did for that project.
I created my entire FluentTypes around this concept. It was useful.
It felt wrong.
Moving On
A Text
is still a primitive. It's LESS god-class than a string
is. It's operations are constrained... which is FANTASTIC. I love it. And it's still... data.
Primitives aren't JUST the system types, to me. That might be how others consider it. I think they're wrong. Just like Getter/Setters are a thin wrapper around accessing private variables, the Text
object is a thin wrapper around string
. I could create a new TextOf(userName)
and pass it into void ValidatePassword(Text password)
.
It solves SOME of the issues with primitives. Good ones to solve. It was a fantastic first step and showed me the power and efficiency that the practices we used had.
And their shortcomings. Areas that I'd like to see more from them.
I had some workarounds, but they were stuck on; not smooth. I started working with teams on new code to try out new concepts.
Represent The Concept
Where I'm currently at is what I consider to be the driving principle for how I write code; and all of the MicroObject practices are in support of this.
Have a representation for every concept that exist in the code.
Instead of UserName
being a Text
it just implements a UserName
interface. That's it. What's WHAT IT IS.
The slight caveat to that is that these usually implement the ToSystemType<T>
class. This is for the implicit casting. We still need to be able to get the data out at the edges of the system, but it stands out.
I won't have Text
as parameters all over the place; it's IUsername
and IPassword
or IDcn
... because I dind't want the interface to be IDocumentControlNumber
everywhere.
Classes started to represent the domain we were working in. I feel a little guilty as I have read one of the defining books, "Domain Driven Design". Which I've heard talks about writing code along these lines.
I've gotten to a kinda similar place through application and experimentation. It works well; IMO.
What's primitive?
Now - We are to the point that we are representing the concept and I still need to talk about how I handle primitives. Because I do.
I've got a number of Primitives
folders in projects to encapsulate these very low level aspects to the system. These are what I want to talk about; within the notion that the concepts in the system should have a class for that concept.
It's one of the reasons I don't like the Factory pattern. A CLASSFactory isn't a concept of the system. I'll avoid it most of the time; but it's a useful thing in some instances. SOME!
Fscking Booleans
Booleans are the primitive I can't get rid of. I try; and it feels crappy. Many of the classes in these Primitives
namespaces are boolean centric. I still try to wrap it; so I can impart convience functionality and get some benefit from that... but it's FAR LESS than I would like... because it's still just a thin wrapper over bool
. Bools are also used in the code. We need the primitved value within the flow of code, not just at the edges of the system.
I say "need" even though I've experimented with having a If
class that takes in actions for either option. Then the primitive bool
would only ever be exposed in a single place. Which I've done for null in my ClassCacheAsync.
I, personally, like it a lot. I'm not going to force it on other developers I have to work with. Re-creating smalltalk in C# isn't going to go over well for other developers. I've abandoned it and accept that bools are going to exist... for now.
In the Primitives
folder there are 10 classes; 4 are for bools.
One's a GUID utility that I pulled in and was too lazy to incorporate into the classes that need it. So... shame on me for that.
There's an interface, ITokenPair
which isn't used. Whatever I created it for fell out because its was clearly a crappy way to do it.
There's ToSystemType
and a derivced class StringEqualityToSystemType
which handles ... string equality... for classes that would be ToSystemType<string>
. I found myself copy/pasting a lot of the comparrison code for strings; and that means I'm missing an abstraction - OH LOOK; An Abstraction!
The other two are around handling of Urls
in the system. Which is a low level type. It's a thin wrapper around Uri
using the ToSystemType
.
This exists less to wrap Uri
; as I'd rather create and object that represents the concept of the URl - which I have... but not in the repo's uploaded. It's in my less idealized utility projects.
Here's an example in the project that creates labels for me to put in the binder spines for my sets
public sealed class SetCodeLogoSNIPPEDUrl : Url
{
private readonly SetCode _setCode;
public SetCodeLogoSNIPPEDUrl(SetCode setCode) => _setCode = setCode;
public override Uri AsSystemType() => new($"https://static.SNIPPED.com/media/upload/about/{_setCode.AsSystemType().ToUpperInvariant()}.png");
}
public sealed class SetCodeLogoSNIPPED02Url : Url
{
private readonly SetCode _setCode;
public SetCodeLogoSNIPPED02Url(SetCode setCode) => _setCode = setCode;
public override Uri AsSystemType() => new($"https://static.SNIPPED.com/media/upload/about/{_setCode.AsSystemType().ToLowerInvariant()}logo.png");
}
public sealed class SetCodeLogoSNIPPED03Url : Url
{
private readonly SetName _setName;
public SetCodeLogoSNIPPED03Url(SetName setName) => _setName = setName;
public override Uri AsSystemType() => new($"https://static.SNIPPED.com/media/upload/about/{StripStuff(_setName.AsSystemType().ToLowerInvariant())}.png");
}
public sealed class SetCodeLogoSNIPPED05Url : Url
{
private readonly SetCode _setCode;
public SetCodeLogoSNIPPED05Url(SetCode setCode) => _setCode = setCode;
public override Uri AsSystemType() => new($"https://static.SNIPPED.com/media/upload/about/{_setCode.AsSystemType().ToLowerInvariant()}.png");
}
All of these URLs could be a SNIPPEDUrl
type which implements ToSystemType<Uri>
. Absolutely... I just hate having ToSystemType
as a parameter to methods if not required. Url
is a thin wrapper which makes my code neater. Easier to read. It allows instances of ToSystemType
and the AsSystemType
method to stand out in the code. I want that standout-ness. If it's everywhere... it stops indicating a use of a primitive values.
So... Out of 10 classes, 6 aren't booleans, 1's lazyness, 1 should be deleted, 2 are convertors for the edge of the system, and 2 are for Uris; which isn't even a primitve.
Boolean tends to be the only actual primitive I handle like this. TENDS to. The pattern with ToSystemType
isn't ToPrimitiveType
for a reason... it was originally. Though the use case for translating to ANY system type, like Uri
is a much better pattern; and hence the name.
Some Others
The other two 'primitive-ish' types I create wrappers for are DateTime
and TimeSpan
. Which... I want the name TimeSpan
for my object!!!! For these I use TimeInstant
and ... sigh... TimeInterval
.
The utility projects I have centered around maintaining and displaying my MtG collection have some of the more interesting bits; but I haven't uploaded those yet. If you want to see the initial versions; they're in my FluentTypes project here
public abstract class TimeInstant : ToSystemType<DateTime>
{
public BoolOp LessRecentThan(TimeInstant challenger) => new LessRecentThanTimeInstantBoolOp(this, challenger);
public BoolOp MoreRecentThan(TimeInstant challenger) => new MoreRecentThanTimeInstantBoolOp(this, challenger);
}
These follow very closely the implementation and usecase of the original Text
. I'm not a fan. I'm still looking for a clean way to accomplish this without making the system overly convoluted.
I could create a TimeInstant<T>
which would be like class SpecificThingy: TimeInstant<SpecificThingy>
so the inner methods could be restricted BoolOp LessRecentThan(TimeInstant<T> challenger)...
. but... ugg... so ugly and forced. It's not an elegant solution. It's not clean. It's forced in JUST BECAUSE. Aside from learning; "just because is a terrible reason.
There's not a lot of cases where there's a primitve that can't effectively be implemented as the concept in a class with an interface; and that's it (and the ToSystemType
; which is just an implicit cast abstraction).
Another 'primitive' I have for one of the utility projects is DisplayValue
. I needed a shared way to produce data that could be ... displayed... or written out for logging.
public abstract class DisplayValue : ToSystemType<string> { }
public sealed class StringDisplayValue : DisplayValue
{
private readonly string _origin;
public StringDisplayValue(string origin) => _origin = origin;
public override string AsSystemType() => _origin;
}
The idea is that I could also have an IntDisplayValue
or other types. Those haven't manifested in what I've implemented... yet?
Solution Primitives
I have a whole project dedicated to the EXTREMELY COMMON primitives Lib.MtgDiscovery.Primitives.core
These are the values that are shared across the uploaded projects as well as my ingestion and graphic generation projects. I got tired of having the duplicated code all over.
They aren't that interesting; just the concepts that exist in the code being represented.
The main web project doens't have much that's any fancier. I have a couple of casing specific classes that extend the core concept.
Some values are lowercased to simplify matching; but still are the represented type
public sealed class ToLowerInvariantArtistName : ArtistName
{
private readonly string _origin;
public ToLowerInvariantArtistName(string origin) => _origin = origin;
public override string AsSystemType() => _origin.ToLowerInvariant();
}
While not the TRUEST of decorators; they could easily be transitioned to a pure form
public sealed class ToLowerInvariantArtistName : ArtistName
{
private readonly ArtistName _origin;
public ToLowerInvariantArtistName(string origin) :this(new StringArtistName(origin)){}
private ToLowerInvariantArtistName(ArtistName origin) => _origin = origin;
public override string AsSystemType() => _origin;
}
AND BAM - pure decorator. Of course, that also then requires the creation of a StringArtistName
class. It wasn't worth the squeeze. While I do love my abstractions; I hate inheritance; and I'm very against early abstraction; even if I expect it'll be needed later. I've been wrong often enough about what will be needed... I let the code tell me.
This code isn't telling me I need more; so it doesn't get more.
A lot of what I'd consider primitives are hiding in the Abstractions
folder in the Lib.Cosmos
project here.
They aren't super impressive. The ones that I'm thinking of are Marker Classes. They are just the abstraction for the representation.
They get the sealed
class in Lib.MtgDiscovery.Cosmos.Core.
Summary
For all that I've blathered on about here... it really does boil down to
Don't Use Primitives
... except bool...
Everything else I do avoid primitives and raises things to have a representation of the concept. I really enjoy that I find that is in fact what I'm doing; despite planning on writing a bunch about how I've been handling primitives... I'm not.. TA-DA!