Ten principles of good programming language design

On how Dieter Rams would design C#

Posted on February 18, 2024

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.

Ten principles of good design

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 Brown appliances):

  1. is innovative
  2. makes a product useful
  3. is aesthetic
  4. makes a product understandable
  5. is unobtrusive
  6. is honest
  7. is long-lasting
  8. is thorough down to the last detail
  9. is environmentally friendly
  10. is as little design as possible

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.

C# properties, a case of good design

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?

  1. is innovative - Not really.
  2. makes a product useful - It certainly does.
  3. is aesthetic - It gets really ugly once there are a handful of these.
  4. makes a product understandable - Yes, this is code that everyone can understand.
  5. is unobtrusive - Not really: As a consumer of this class, you want to just access the person's name, but you have to go through a level of indirection.
  6. is honest - It certainly is.
  7. is long-lasting - It'll be useful and understandable in a millennium too, so yes.
  8. is thorough down to the last detail - Yes, it is.
  9. is environmentally friendly - Not too applicable here, but since it consumes too many resources (text and reading time) for what it does (which is little), it's perhaps a "no".
  10. is as little design as possible - Certainly not.

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?

  1. is innovative - Yes, we just invented a new language feature to get rid of boilerplate.
  2. makes a product useful - Still does.
  3. is aesthetic - More aesthetic than getter/setters, but I think there's still room for improvement.
  4. makes a product understandable - The code is still easy to understand, just from the English keywords you can get an idea of what's happening here.
  5. is unobtrusive - Yes.
  6. is honest - Kind of but not clearly: It now appears like a field, but it does more, like a method, without being either.
  7. is long-lasting - Looking back from today, we can say yes.
  8. is thorough down to the last detail - Yes, we have a clear picture.
  9. is environmentally friendly - More than setter/getters, but not quite there yet.
  10. is as little design as possible - Not yet, we still have plumbing to write.

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!

  1. is innovative - Yes.
  2. makes a product useful - Yes.
  3. is aesthetic - I'd say now indeed yes. You may dislike the curly braces and semicolons of C#, but if you prefer that style, it fits right in.
  4. makes a product understandable - Yes, and better than full properties.
  5. is unobtrusive - Yes.
  6. is honest - Less so than full properties: There is now an automatically generated invisible backing field. Conceptually, it's simple, but technically there's a lot more going on.
  7. is long-lasting - Yep.
  8. is thorough down to the last detail - I'd say so.
  9. is environmentally friendly - Now yes.
  10. is as little design as possible - I think so.

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.

C# primary constructors, a case of questionable design

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?

  1. is innovative - Nope.
  2. makes a product useful - Yes, Dependency Injection is useful, and we have no better way to do this.
  3. is aesthetic - Not really.
  4. makes a product understandable - While it's not rocket science, you still have to take your time to understand what happens in the constructor and why.
  5. is unobtrusive - No, we have to jump through hoops to get our dependency injection working.
  6. is honest - Certainly.
  7. is long-lasting - Yes, it builds on principles that are decades old.
  8. is thorough down to the last detail - Yes.
  9. is environmentally friendly - Not really, due to its verbosity.
  10. is as little design as possible - Certainly not.

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.

  1. is innovative - Yep.
  2. makes a product useful - Hmm, yes, but we didn't gain any more use than what we had before.
  3. is aesthetic - Looks better, though what's up with the parameter that's not really a parameter?
  4. makes a product understandable - No, it makes it harder to understand.
  5. is unobtrusive - I guess it is.
  6. is honest - No, some magic is going on.
  7. is long-lasting - Can't tell yet, it's a new and strange concept that borrows components from existing ones but uses them in different ways.
  8. is thorough down to the last detail - Wouldn't say so.
  9. is environmentally friendly - Better, but still looks like too much text.
  10. is as little design as possible - Better, but we still have the field.

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.

  1. is innovative - Yes, can't argue with that.
  2. makes a product useful - Yes, still the same.
  3. is aesthetic - I like it, at least.
  4. makes a product understandable - Hard no.
  5. is unobtrusive - No, it broke my naming conventions.
  6. is honest - No, it does something behind the scenes and tries to lead me on.
  7. is long-lasting - Judging from the criticism it got, I'd guess that it'll need a lot of fixes soon, so no.
  8. is thorough down to the last detail - No.
  9. is environmentally friendly - As far as the amount of text goes, yes. However, I need a lot of glucose for my brain to understand it, and that needs to come from some sustainable farming, or we're all doomed.
  10. is as little design as possible - I guess so, yes.

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.

Did we learn anything?

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!