You can have design just for design's sake, solely to bring visual, auditory, spiritual, intellectual, or other pleasure to the consumer and its creator. This is art.
Then you can have design for things people interact with to achieve a goal, for something that people use. These objects are merely tools; while welcome to be delightful, they should serve the singular purpose of helping people achieve said goals. Cleverness, some mistaken "elegance", or the mental fulfillment of the creator is thus secondary. Good design, by its very nature, induces low cognitive load: If your users have to think too much, you're doing things wrong.
This letter will use examples from the programming language C#. These will be easy to understand for anyone who can code in any language, but perhaps less for others.
Dieter Rams, a living legend of German industrial design, formulated ten principles that, according to him, "good design" should follow (here in video form if you want to feast your eyes on some classic Braun appliances):
Now, these might not be God-given indisputable facts, but for us mere mortals they're good enough guiding principles for now. Let's see a teachable example from C# where the designers hit these accurately with a feature whose design I'd consider "good", properties, and one less of a success, primary constructors.
In ancient times, classes had to have getter/setter methods to offer controlled access to data that they wanted to expose (note that for historical accuracy I'm using C# as it was at the time, without expression-bodied members here):
Simple, gets the job done. However, it's also verbose boilerplate that the programmer has to write over and over again. And since it's a convention, not a language feature, it's up to the developer to figure this out and diligently follow. How does it fare with the ten principles above?
So, perhaps 5 out of 10. Can we make it better? Yes, with properties:
Properties are setter/getter methods implemented as a language feature, a mix between fields and methods. Is this any better?
Let's say, 6/10 at least, perhaps 7 if we want to be charitable. A clear improvement, but not perfect yet. Let's introduce auto-implemented properties:
Now we're getting there!
We now achieved 9/10. This is clearly good design. But what makes it great design, eternal design is not when you see its chronological evolution, but when you consider how you'd design properties today, after more than a decade of experience with C#'s implementation, looking back: I dare you to come up with a better approach (I'd argue other languages couldn't). The automagically generated backing fields are something to wrap your head around, but you don't have to: If you don't know about that, you can still use auto-properties and nothing bad will happen.
Utilizing Inversion of Control containers and Dependency Injection, we frequently write code like this (the point is in the constructor and field):
I know, very useful class, let's move on. What's up with our ten principles?
4/10. You can of course understand why everything is like it is here, you need all the pieces. Still, do we want to write such constructor injection code, with the field and constructor parameter and assignment all the time? We could do a similar feat as we did with properties and turn this pattern into a language feature. Let's call that primary constructor!
Hmm, does my class now have parameters? Interesting. Does this mean clock
is available in the whole class? Nope, just for the field initializer. It looks nicer but I'm confused.
Still about 4/10, I'd say. The C# design team wasn't sure about this either, because they introduced an alternative way of using primary constructors at the same time, without fields:
This looks neat, but what is going on? The clock
parameter is now somehow also available in the method. Is it then a field, just a magic one? But how can it be a field and a parameter at the same time? Is this the quantum computing already that they talk about? Is it a closed variable? Also, I like underscores to prefix class-level fields, it confuses me that now clock
and days
both look like local variables. But if I name the parameter _clock
then it'll go against the naming conventions of parameters. Well, can I write this.clock
, then? No, for some reason.
Maybe 5/10? Better where we started, but only marginally. This is something I'd expect from a new design pattern, perhaps, but not from a fundamentally new language feature. How to fix it? I have no idea; criticizing is easier than coming up with a solution, and I'm no language designer.
To be fair, primary constructors have other uses, and reinventing constructor injection was just one of its goals.
I think you, as a designer (and if you invent any experience that people interact with, even an API or CLI, you're a designer), should approach your work foremost with humility. It's not about you, it's about your users.
This is a hard job, and sometimes goes against your professional desire (and your fun!) to create something impressive just for its own sake that you're proud of. You have to take a step back, back to the drawing board, and produce "good design". This sometimes means that you have to say "no" and admit that whatever you can think of right now wouldn't be up to your standards.
C# is my favorite programming language; I've been working with it since I was 15. Most of the code I've written in my life, and close to all my open-source code is in C#. However, I feel that in recent years, C# has become bloated. While before, every minor change came after meticulous consideration, now we see half a dozen new language features every year. I welcome progress, but when you're building something fundamental like a programming language that millions use, everything will have an outsized impact.
You could say that well, you don't have to use primary constructors, what's the harm in having it there? It still adds to the cognitive load of using the language. Unless some higher power tells you that you musn't use primary constructors and that's it, you must now consider one more language feature to use when approaching a problem. Code fixes and analyzers will nudge you towards it. You need to debate it for a day with your team whether you want to use it. You will migrate half of your code base in your initial enthusiasm before realizing that no matter how many band-aids you put on it, it just won't work for you (ask me how I know!). Arguably, my team would be better off without primary constructors ever landing in C#. And boy, are there many more such features!