Cleaner code - how clean is too clean?!

Interesting (?) article here on clean code and SOLID: https://discussions.unity.com/t/tips-for-writing-cleaner-code-that-scales-in-unity-a-five-part-series

it also mentions design patterns.

The thing is all of these things work well for very specific examples and use cases but are often fairly ignored in real-world coding (speaking from personal experience and some spoken experience of others).
Also clean code (from the original books) has, I believe, been show to ultimately break up the code too much with the single dependency principle so you end up with thousands of classes and interfaces, making a project actually slower to manage and develop.
You can use many of the principles of course, but I think there is a balance. Just do not try to shoe-horn in SOLID and design patterns everywhere just because someone taught them to you.

So I wonder if anyone has gone this in-depth with a full project down the clean/SOLID route?

1 Like

That’s how you know when you’ve gone too far. :slightly_smiling_face:

4 Likes

Nobody suggests that there are no cases where dialing back or trying another scheme isn’t warranted and there is no solution that has followed every design principal. First, they can’t all be unambiguously listed and second, they can’t all apply in an absolute sense in every case. Which method of “dependency injection” is the right one?

That said, it must be realized that “outcomes” can’t be predicted based upon past events. One could have saved maintenance hours (maybe) having done something differently but development could take longer. Could the bug that caused an outage for 50% of the customer base been avoided? Impossible to say in the abstract case, depends upon the bug. If yes, then the effort would seem to have been worthwhile. If not, then it seems reasonable to believe whatever time was available should have been spent elsewhere. It isn’t the chain that breaks, it is the weak link in the chain that breaks.

Code is part of the solution but code is not “the” solution.

2 Likes

I always loop back to this classic:

“The purpose of all programs, and all parts of those programs, is to transform data from one form to another.” - Mike Acton

If you aren’t transforming data with your code, why are you writing code?

I haven’t worked on a game that really focused on SOLID. I did work at a non-game related company that used it. I really disliked it at first, you had to create multiple classes / interfaces and update database records just to add a new button click action. Another problem is that people that didn’t understand the system would occasionally create really bad workaround hacks. After I got used to it, it wasn’t too bad. Some things I noticed include:

  • About 25-50% of a programmer’s time is normally debugging. Unit testing helps reduce this, especially as the project scales up. SOLID helps make classes testable.
  • The smaller classes are much easier to work with in a team than large monolithic classes. So entry level programs could learn the parts of the system they need to work on relatively quickly.
  • It’s kind of like working on an assembly line where complex tasks are broken into much smaller/simpler tasks. If you’re on holiday or quit then the company can easily replace you.
  • Smaller classes are much easier to track in source control. A 100 line class might only have about 10 commits while a large 10,000 line class can have thousands of commits so the commit history isn’t as useful.

I like using SOLID in my own game projects but mainly for non-gameplay related things like UI or services. It’s nice to know that it’s working as intended with Unit tests and that any future changes won’t break existing code. It also makes it easier to reuse the code in other projects. That said, I don’t use it while prototyping as it’s kind of a waste of time for something that I intend to throw away.

3 Likes

#1 A little hack to the same end - use small projects.

Break off separate systems into different Unity projects. Design it so you can import it as a package into the full project. This forces you to not couple systems together, and helps you create good modular components or interfaces.

By “separate system”, a recent example for me was a dialogue system. This covered the UI, some scriptable objects, and components to pop dialogue. Not large.

This is something I also do because a larger project in Unity can begin to slow down, so it also has a nice benefit to speeding up development in addition to preventing lazy coupling. It’s semi-SOLID for a Unity environment.

#2 Group #regions in your code. Probably each should be a separate class from a SOLID perspective but it really depends on what it is whether that’s worthwhile. The region reminds you that if it grows any larger then maybe it’s time to separate.

I’m not a fan of designing code based on any “principles”, such as SOLID. I think you can end up introducing a lot of unnecessary complexity to the codebase, if you just blindly apply something like the dependency inversion principle to every single dependency, even when there’s no practical reason for it.

I think SOLID is best viewed as just some tools in your toolbox - you can apply some them when they make sense, and not apply them when they’re not useful. But thinking that you need to refactor your code only because it “violates” some principle, isn’t a very good approach to design in my view.

As for Clean Code, I find most of the tips in the book agreeable - but sometimes it gets too dogmatic for my tastes.

For example, I totally agree that writing easy-to-understand, self-documenting code is a really good idea. But I also think that adding literal comments into your code sometimes can make a lot of sense as well. I don’t think that comments are inherently a code smell.

I also don’t think that always trying to make all your classes and methods extremely short is a good idea. If the implementation of some complex class naturally takes 1000 lines of code, or the functionality of some complex method naturally takes 20 lines of code, I don’t think there’s anything wrong with letting them take that much space. I don’t agree that refactoring those into dozens of smaller modules will automatically improve readability and maintainability on the whole.

For example, I think it’s quite nice that I can just open up the one Dictionary.cs file to see the whole implementation of the Dictionary class, instead of having to open Dictionary.cs, DictionaryBuckets.cs, DictionaryVersionInfo.cs, DictionaryFactory.cs, DictionaryToArrayConverter.cs, DictionarySerializer.cs, DictionaryResizer.cs and so on.

Is that one CopyTo method over 50 lines long?

Yes.

Would it improve readability if it was split into 20 smaller methods, each one only a couple of lines long?

I very much doubt that.

3 Likes

Principles are not rigid rules, they cannot be applied universally in every situation. This is evident in the fact that some principles can contradict one another.

The significance of each principle lies not in the principle itself but in the problem it seeks to address. That’s why it is imperative when learning a principle to understand why this principle was made, the problem the principle solves is more important than the principle.

By learning various principles, we learn to identify potential problematic areas in our code. This understanding allows us to make informed decisions: Do we believe this issue will negatively impact our codebase in the future? If the answer is yes, we already have a relevant solution in mind, along with its pros and cons, enabling us to decide whether to implement that solution.

Think of your code as a recipe with two distinct audiences. One audience is the compiler, which transforms your code into a binary that fulfills the required functionality. The other audience consists of people, yourself or other programmers, who will interact with the code you write. Just as code solves problems for the end user, it should also solve problems for developers. Specifically, your code should be easy to test, debug, modify, and work on collaboratively.

In summary, learning programming principles helps us recognize potential issues in our codebase. Once we identify these issues, we need to determine whether they will significantly impact us. If they do, we can then weigh the pros and cons of applying the relevant principle.

So, too clean code is the code that we write that doesn’t solve any of the problems mentioned.

The same is true for the programming patterns, for the performance and in general with everything related to coding. Our job is not merely to write code, it is to solve problems by writing code. Code that doesn’t solve real, existing problems is a liability.

1 Like

Well this is the thing if it slows you down it costs time and so it costs money. On the other hand if you rush too much and ignore architecture early on then down the line things will slow to a crawl. But I don’t much understand the need for interfaces most of the time, if you only ever will have one version of a class anyway as then adding an interface wastes time. You can add one later if required.

Reality means speed of dev often comes first and the time to write the cleanest most testable code is not available anyway.

Ultimately games are the most complex of things to architect. On class size there are times when big classes may make sense. If you have a complex world with deep nesting of classes holding basic data about it then it is very tempting to have a huge world class with loads of functions to get specific info about your world or edit it in a level editor. It’s hard to break it down without making it worse to use in some way.

I would disagree a bit… I can’t think of a single good reason for a big class much like I can’t think of a good reason for a manila folder designed to hold 1000 sheets of paper. There is no point when folders can contain other folders.

If one has properties (or methods) that define things like targetDistance, targetHeight (and other target-related values) then surely they can be grouped into an inner class named Target with just properties named Distance and Height. You group them for organizational purposes.

As one notices the same prefix used on properties and methods group them and even the task of naming all these properties becomes easier.

In my workspace the people who talk about all this stuff (SOLID etc.) are the the older people, who like to talk more about theory than actual coding, they tell you that you have to follow all these rules to write “good” and “clean” code.

They claim that their old legacy code is bad, because they didn’t follow these rules back then. As a beginner you belive this and it all sounds nice, but once you got more experience you start to realize that their old code is not bad, because they didn’t follow these rules, it’s bad because of much bigger issues.

And no amount of SOLID (or any other guide) will convert a bad developer into a good one, if they don’t understand the actual issue and blindly follow rules.

A good developer will realize on it’s own what is clean and what not, without reading any guides. You shouldn’t need someone to tell you that a 500 line method or 30 if nestings are not readable.

1 Like

We should gather in an auditorium and ask for a show of hands, “how many here are good developers”?

4 Likes

While readability can be an important factor, it’s not the only metric that applies. There are always several goals and some may contradict each other. So there’s no one-size-fits-all solution. One of my own examples is the image cropping code I wrote over here. In my answer I left the “readable” version that uses 4 Lerps for the bilinear filtering which makes it easy to understand what it does. On my linked dropbox I have optimised the code so it ran about 8 times faster. As a result the code is more cryptic and even longer, but runs faster. In the end you always have to find a compromise.

The same is true for things like database normalization. Almost no database is in the highest normal form because it can be a pain to work with such a database. It’s a guideline with good intentions and good reasons why to do it. However there can always be other aspects which may justify to diverge from the gold standard.

2 Likes

I used readability as one of many examples, I didn’t want to write a whole list.

My argument was “A good developer will realize on it’s own what is clean and what not, without reading any guides.” (In hindsight I should have worded it a bit differently, maybe “without strictly following any guides”)

Your comment about “In the end you always have to find a compromise” is actually similar to my thoughts. Once you are experienced enough, you make your own desicision and don’t blindly follow some guru rules or guides, just because someone said you have to do it this way otherwise the code isn’t “clean” anymore.

1 Like

These applications were written by professionals and reviewed by many.