Pregunta

Earlier today I was working on a simple program from a textbook.

Although I solved the question with confidence and a clear robust solution, the solution wasn't perfect as it had steps which were redundant.

The more I optimized and tried to make it perfect:

The code became more:
- Unreadable
- Buggy

I became:
- Less confident of the solution
- More frustrated

Is there a proper way to deal with optimizations so you don't lose your mind and time, or should one stop optimizing if the solution is unclean.

¿Fue útil?

Solución

Don't try for perfection. Recognize that there's a balance that must be struck between "good" code and "perfect" code.

Let's say the code works well, is readable, maintainable, easy to work with, but has a small handful of things that might not be 100% the way you'd like. If you try to refactor those few things but end up digging yourself into a rabbit hole, then the code would have been better off if you had just left it in it's "good" working state, right?

Actually, such cases are usually a sign that a better architecture at the outset would have been better, but at this point it may not make much sense to go back and "fix it all up."

For every single project you work on, you'll walk away with a few "I could have done X better" thoughts - and that's good. You learn from them and apply what you've learned in your next project. It's a never-ending cycle of programming. No project is ever "perfect", and recognizing that "hitting the balance" is what you really need to be doing is the key to your sanity. Learn from it, be happy with a job well done (even if not perfect), and try again next time.

For this particular project, it sounds like it might be good to take a step back and say "if I had to do it all again, what might have I done differently, specifically to prevent this problem area from being more buggy and contributing to less readable code when I try to fix it?" If you can figure that out, it sure sounds like it'd be the good takeaway.

The problem comes when people fail to be retrospective about these things. Good experience is built like this, but if a person isn't willing to reflect on what could have been done better, they're never going to remember that and better their skills for next time.

Otros consejos

The only best practice that I know of for reducing bugs while optimizing code (or for any kind of refactoring, for that matter) is proper testing. (Which includes extensive testing.)

So, first you get your code to work, then you have all your tests in place which prove that it works correctly, (or first you do your tests, then you write your code, if you are of the TDD persuation,) and then you start tweaking things. And with every tweak, you re-run your tests. This way, if your tweak breaks something, you catch it.

Of course, as you are tweaking things, you might get ideas about more tests to specifically test for things that your tweak may break. When such an idea comes to mind, stop what you are doing, shelve your tweaking changes, add that test, make sure everything works, and then unshelve your tweaking changes and proceed.

Avoid Pre-optimization

Pre-optimization occurs when you assume a piece of code is inefficient on resources, and thus focus on fixing it, without even checking if it's a problem.

You can waste quite a bit of time on this, and it could potentially harm your design decisions. If you have any worries, profile your application.

Avoid Mico-optimization

Mico-optimization is an attempt to save resources by trying trying to strip things down to their bare bones.

An example would be declaring local variables. If we had a collision detection method, it may look something like:

boolean isCollidingWith(Entity entity) {
    return this.x < entity.x && this.x + this.width > entity.x || this.x > entity.x && entity.x + entity.width > this.x || ...;
}

A developer may want to clean this up by declaring some local boolean variables:

boolean isCollidingWith(Entity entity) {
    boolean collideRight = this.x < entity.x && this.x + this.width > entity.x;
    boolean collideLeft = this.x > entity.x && entity.x + entity.width > this.x;
    
    return collideRight || collideLeft || ...;
}

If one were to say "you don't need those variables, they just waste space", that would be considered mico-optimization. Although a little space may be saved, it's negligable, and attempting to save that space may harm readability.

Suit your program's needs/requirements

You are required to write a program that calculates PI. Whoever writes the most powerful PI calculator wins a prize.

In a scenario like this, you may prefer resource efficiency over readability, conserving as much as possible. You plan on deleting this program afterwards, so you won't need to read this code ever again.

Are you going to invest hours into making this project scalable? Probably not.

You own a popular social networking site.

The rules have changed. You have millions of users constantly requesting content, while also attempting to fix any bugs currently being exploited. On top of this, you have ideas of your own you want to implement.

Performance is important, but there are many other, potentially more important, factors at hand. Readability is a must, due to your need for a team and the need to revisit code.

Make sure you understand the needs of your situation.

Learn how to properly test

Testing can be a long and painful process if you don't know how to do it right. You could spend hours fighting with tests, just to realize you didn't test a unit properly.

Unit testing is a mainstream form of testing. It focuses on checking the integrity of a single unit of code. Integration testing would be the next step: testing as a whole.

Mocking allows you to recreate situations that don't usually occur on command, such as seeing if a robot wakes up when an alarm goes off (you could mock the alarm and give it a specific "current time", rather than waiting).

Without exposing yourself to these concepts, you'll have a hard time ensuring your software's integrity.

It's a trade-off. You should optimize when there is a likely or clear potential benefit from optimizing the code, and stop optimizing when you no longer estimate that benefit to be worth your time.

The cost, as you mentioned, can be frustration, more time finding and fixing obscure bugs, creating more thorough tests, clearly documenting obscure refactorings, giving up and retreating to a previous working version, and etc.

The benefits can be a little as just from learning how to write (or not write!) "tight" efficient code for later situations where it is required (tiny embedded IoT, or zillion of costly AWS instances, or deep space network uploads, mobile user's battery life, and etc.)

In other situations, removing duplicated code might even cause disadvantages (tutorial code readability, weird processor cache/pipeline hazards, and etc.)

Licenciado bajo: CC-BY-SA con atribución
scroll top