Learn when to tell your manager little white lies.
Well, they are not really lies, but rather this is about interpreting questions the right way.
Managers need to know how long it will take to implement a new feature or fix a bug. Often they are under pressure, but even when that’s not the case, they still need that information for prioritizing and scheduling. Getting this information from a developer is where some miscommunication can occur in my experience.
“How long will it take to implement this feature?” Often there is some explicit or implied time pressure in the question. “You know, I’m really in the hot seat on this one with our biggest customer.”
Once upon a time, I interpreted this question at face value: If I were to work on this feature starting now, what is the minimum time it would take me to have something that met the requirements? I answered that question, but I ignored other relevant considerations:
- What kind of tests should I add?
- Is there some existing code that can be reused?
- Is there some refactoring that should be done to implement this feature in a way that it can be maintained and enhanced in the future?
Ignoring these questions and getting a feature working quickly makes lots of people happy in the short term, but all of these aspects affect the long term quality and maintainability of the software. When they are ignored it leads to in the code base.
Technical debt makes it ever more difficult to add new features and produce high-quality software. In my experience, if one doesn’t address testing, refactoring and code reuse during the implementation, they are never addressed. There is always another problem to solve around the corner. Each time we ignore them, we take one more step towards the inevitable “sea of complexity”. Each step makes the next enhancement or feature more difficult. When you finally reach the “sea of complexity” you realize you can no longer enhance or support the software using the existing code base and the only solution is to throw it all away and start over.
So when I hear the question “How long will it take to implement this feature?” Before I answer, I translate it in my head into the question “How long will it take to implement this feature with high quality, including adding unit tests, refactoring existing code, and integrating it into our existing code base in such a way that can be maintained and enhanced?”
- Most of the time, using inheritance is a bad object oriented design in the long run. It reduces reusability and testability of code. Consider using composition and interfaces instead. See . Related;
- Avoid introducing an interface until you are comfortable in your domain. “Premature interfacing” can also lead to design issues down the road.
- Deep nested code (both intra-function and inter-function) is 1) harder to maintain, 2) more prone to bugs and 3) harder to reuse. Shallow code hierarchies generally makes a better foundation for reuse and testing. See note about inheritance above.
- Estimating time is hard. One reason why Scrum and sprints are used in many places.
- Proper encryption is hard. Don’t invent it yourself unless you have a good reason to.
- Side-effect free logic is nice. It makes it easier to reason about state (see below) and generally simplifies automated testing.
- Learn to reason around state and lifecycles. See .
- Concurrency can be hard without the right primitives. Threadpools, queues, observables, immutability and actors can sometimes help a lot.
- Premature optimization is the root of all evil. A good general development process is: 1) Get it to work. 2) Make the code beautiful. 3) Optimize.
- Know your basic data structures and understand time complexity. It’s an effective way of making your code much faster without adding complexity.
- Practise back-of-the-envelope calculations. How many items will a piece of code generally hold in memory? Related;
- An application will eventually break; Bad deploy, unintended behaviour, unintended input or unintended external load. Plan for that. This includes making sure you log uncaught exceptions, test a deploy works after it’s out (and potentially roll back), should run tests continously, but also should make sure to set (sane!) limits on all in-memory queues and thread pools. Related;
- If you monitor the size of a queue, it’s generally always full or empty. Plan for that. Related;
- Networks and external services should always be expected to be flaky. Always set socket timeouts on your sockets and read/connect timeouts on HTTP calls. Consider wrapping external network calls in a retrying/circuit breaker library (see & ).
- Write code as you want to read it. Add comments where you think you will not understand your code in a year’s time. You will need the comment in a month. Somewhat related;
- Setup you build tooling around a project so that it’s easy to get started. Document the (few) commands needed to build, run, test and package in a README file.
- Making sure your projects can build from command line makes things so much easier down the road.
- Handling 3rd party dependencies in many languages can be a real mess (looking at you Java and Python). Specifically when two different libraries depend on different versions. Some key things to take away from this: 1) Constantly question your dependencies. 2) Automated tests can help against this. 3) Always fixate which version of a 3rd party dependency you should use.
- Popular Open Source projects are a great way to learn about good maintainable code and software development process.
- Every single line you add to an application adds complexity and makes it more likely to have bugs. Removing code is a great way to remove bugs. Related;
- Every piece of infrastructure (databases, caches, message queues etc.) your application is a source of bugs, requires maintenance & new knowledge. Not to mention that such dependencies might slow down productivity. Weigh new infrastructure against productivity carefully. Can you replace an old piece of infrastructure with your new?
- Code paths that handles failures are rarely tested/executed (for a reason). This makes them a good candidate for bugs.
- Input validation is not just useful for security reasons. It helps you catch bugs early.
- Somewhat related to above: State validation and output validation can be equally useful as input validation, both in terms of discovering inherent bugs, but also for security sensitive code.
- Code reviews are a great way to improve as a programmer. You will get critique on your code, and you will learn to describe in words why someone else’s code is good or bad. It also trains you to discover common mistakes.
- Learning a new programming language is a great way to learn about new paradigms and question old habits.
- Always specify encoding when converting text to and from bytes; be it when reading/writing to network, file or for encryption purposes. If you rely on your locale’s character set you are bound to run into data corruption eventually. Use a UTF character set if you can get to choose yourself.
- Know your tools; That includes your editor, the terminal, version control system (such as git) and build tooling.
- Learn to use your tools without a mouse. Learn as many keyboard shortcuts as possible. It will make you more efficient and is generally more ergonomic.
- Reusing code is not an end goal and will not make your code more maintainable per se. Reuse complicated code but be aware that reusing code between two different domains might make them depend on each other more than necessary.
- Sitting for long time by the computer can break your body. 1) Listen to what your body has to say. Think extra about your back, neck and wrists. Take breaks if your body starts to hurt. Creating a pause habit (making tea, grabing coffee) can be surprisingly good for your body and mind. 2) Rest your eyes from time to time by looking away from your screen. 3) Get a good keyboard without awkward wrist movements.
- Automated testing, and in particular unit tests, are not just testing that your code does was it should. They also 1) document how the code is supposed to be used and 2) also helps you put yourself in the shoes of someone who will be using the code. The latter is why some claim test-first approach to development can yield cleaner APIs.
- Test what needs to be tested. Undertesting can slow you down because of bug hunting. Overtesting can slow you down because every change requires updating too many tests.
- Test what (outcome) is being done in an implementation, now how it’s being done. In other words, your tests should not depend on the inner nitty-gritty details of a class. A different way of looking at it is that a rewrite of how a class does something shouldn’t require changing any of the tests as long as the outcome is the same. This will simplify refactoring a lot easier.
- Dynamic languages generally need more testing to assert they work properly than compiled languages. (Offline code analysis tools can also help.)
- Race conditions are surprisingly more common than one generally thinks. This is because a computer generally has more TPS than we are used to.
- Understanding the relationship between throughput and latency ( ) can be very useful when your systems are being optimized. Related;
- Many times high throughput can be achieved by introducing smart batching.
- Commit your code in small, working, chunks and write a helpful commit message that summarizes what you did and why you did it. Working commits are a prerequisite for bisecting bugs ( ).
- Keep your version control system’s branches short-lived. My experience is that risk of failures increases exponentially the longer a branch lives. Avoid working on a branch for more than two weeks. For large features, break them into multiple refactoring branches to make the feature easier to implement in a few commits.
- Know your production environment and think about deployment strategies for your change as early as possible.
- Surprisingly, shipping code more frequently tends to reduce risk, not increase it.
- Learning an object oriented language is easy. Mastering good object oriented design is hard. Knowing about and object-oriented will improve your understanding of OO design.
- It’s possible to write crappy code in a well architected system. However, in a well architected system you know that the crap is isolated and easily replaceable. Focus on a sane decoupled architecture first. The rest can be cleaned up later if on a tight schedule.
- can be a serious risk to your team. Be a team player: Most of your code you write will be read or modified by someone else. This includes the code you write early in a project! Document (as appropriate) and write solid commit messages from the start. Also, code reviews and scripts can help a lot in knowledge sharing. Last, but not least, do make sure you aren’t the only one sitting on secret passwords etc.
1. Never reveal all that you know.
OK, seriously this time. I think there are really a few things that distinguish great programmers.
- Know the concepts. Solving a problem via memory or pattern recognition is much faster than solving it by reason alone. If you’ve solved a similar problem before, you’ll be able to recall that solution intuitively. Failing that, if you at least keep up with current research and projects related to your own you’ll have a much better idea where to turn for inspiration. Solving a problem “automatically” might seem like magic to others, but it’s really an application of “practice practice practice” as suggests.
- Know the tools. This is not an end in itself, but a way to maintain “flow” while programming. Every time you have to think about how to make your editor or version-control system or debugger do what you want, it bumps you out of your higher-level thought process. These “micro-interruptions” are small, but they add up quickly. People who learn their tools, practice using their tools, and automate things that the tools can’t do by themselves can easily be several times as productive as those who do none of those things.
- Manage time. Again it comes back to flow. If you want to write code, write code. If you want to review a bunch of patches, review a bunch of patches. If you want to brainstorm on new algorithms . . . you get the idea. Don’t try to do all three together, and certainly don’t interrupt yourself with email or IRC or Twitter or Quora. 😉 Get your mind set to do one thing, then do that thing for a good block of time before you switch to doing something else.
- Prioritize. This is the area where I constantly see people fail. Every problem worth tackling has many facets. Often, solving one part of the problem will make solving the others easier. Therefore, getting the order right really matters. I’m afraid there’s no simple answer for how to recognize that order, but as you gain more experience within a problem domain – practice again – you’ll develop a set of heuristics that will guide you.
- Reuse everything. Reuse ideas. Reuse code. Every time you turn a new problem into a problem you already know how to solve – and computing is full of such opportunities – you can save time. Don’t worry if the transformed solution isn’t absolutely perfect for the current problem. You can refine later if you really need to, and most often you’ll find that you’re better off moving on to the next problem.
A lot of these really come down to efficiency. As you move through more problems per day, you’ll gain more experience per day, which will let you move through more problems per day, and so on. It’s a feedback loop; once you get on its good side, your effectiveness (and value) will increase drastically.