This answer is more philosophical than it is technical, as your question revolves around intent and communication, not technical implementation or syntax (directly). You have some expectations, but aren't quite taking them to their logical conclusion.
If you're just interested in the code, read the last section. The rest is explanation and justification as to why this is a better approach.
Your intentions are unclear
I want the user of the class to know that setting the Slug property to null is supported
... it requires the user of the Post class to know that a null value can be safely passed into its Slug property -- something I wish to avoid"
That's a contradiction.
The reason I'm bringing this up is because this question revolves around communication between developer and consumer, not technical implementation (directly). But if you want your developer and consumer to be on the same page, you need to first know what that page is supposed to be, and that contradiction isn't helping you.
The typical property
Generally, you expect a gettable and settable property to retain the state you set.
var myClass = new MyClass();
myClass.MyProperty = "hello";
Assert.AreEqual(myClass.MyProperty, "hello"); // Should pass!
That is the default expectation on how a property behaves. Is that behavior set in stone? No. Clearly, the language/compiler allows you to implement your properties completely differently.
But doing so means you go against the consumer's natural intuition on how a property behaves.
The conclusion I'm trying to get to here is that you need a valid reason to deviate from the default property behavior, if you want to justify the cognitive cost to your consumer, who now has to know about this atypical behavior in your implementation.
Sometimes, this can already be contextually understood, e.g. if your setter cleans up an understandable-but-abnormal value (e.g. a file path with consecutive separators). Sometimes, this requires documentation to explain to the consumer.
I have no idea what a Slug
is, so I can't judge this part. Based on the root your question, it would seem that this needs to be explicitly communicated to the consumer.
The null
crutch
Historically, null
is a controversial topic. Some developers hate it, some use it religiously. I'm not making any definitive statements here. Let's avoid a holy war and try to find middle ground: null
can have its purposes, but null
can also be abused.
One clear abuse of null
is when developers use it as an additional value which they don't bother to further define. The clearest example here is turning a bool
into a bool?
when a third state needs to be added. It should've been an enum with 3 members, but null
is being introduced as the third value instead, as a shortcut.
What you're doing here is similar. Your Slug
doesn't have "the chosen value or null
", your Slug
has "the chosen value or a default value". You've just chosen to represent that choice of setting the default value using null
(and then subsequently translate a null
into the actual default value you want), which in my book counts as the same null
abuse I just described.
Solution: the named default
We've addressed several issues:
- The way you're suggesting to use your property is atypical and would require the consumer to learn the specifics of your implementation
- The way you're suggesting
null
should be used to set a (non-null) default value is atypical and would require the consumer to learn the specifics of your implementation.
This can't live in the same world as:
- You want the code to be self-documenting towards the consumer.
If you stray from the beaten path, then people won't find their way on their own.
To solve the above issues, we should make the "default" value an explicit part of the contract of your Post
class. This way, your consumer can figure out that there is a default value, and how to set it, without needing to read additional documentation.
The simplest solution here is to stick with a non-null property, and add a secondary route to set that property value.
public class Post
{
public string Title { get; }
public string Slug { get; set; }
public Post(string title)
{
Title = title;
SetDefaultSlug();
}
public void SetDefaultSlug()
{
Slug = title.Slugify();
}
}
The main difference between this answer and the already given answer by dumetrulo is that this version can revert back to the default, whereas the other answer's default, once removed, cannot be retrieved (since your consumer doesn't know how you calculate the default value.
Additionally, you can argue that you should still use the default value when the custom value doesn't pass a certain validation (e.g. no empty strings).
I can see arguments pro and con, depending on whether you consider this a valid responsibility of your class. That's a contextual consideration which I cannot conclusively judge. Should you judge it to be relevant, you can change your Slug
property to:
private string _slug;
public string Slug
{
get
{
return ValidateSlug()
? _slug
: title.Slugify();
}
set { _slug = value; }
}
private bool ValidateSlug()
{
return !String.IsNullOrWhitespace(_slug);
}
A null or empty check is just a basic example. This could be based on length, profanity filter, ... Your business validation is your own decision.
However, do keep in mind that we're getting into atypical property behavior again. If your Post
's responsibility includes sanitizing the slug (which would be the basis for adding the above code), then that's a valid justification for changing the property's default behavior.
But that depends on the notion that the consumer inherently understands that the Post
class will sanitize the slug as it sees fit.
Slug
like this:set => slug = !string.IsNullOrEmpty(value)? value : fallbackValue;
\$\endgroup\$