There is a lot more to good software development than just knowing the syntax of a programming language. This is my attempt to compile a list of best practices covering all aspects of software development. (My background is in enterprise application development so the list is oriented towards that and some of it is less applicable for solution development or web development.)
If you know any useful tips or best practices that I haven’t listed, please comment!
Different kinds of software development
There are different sorts of software development and the optimal approach is slightly different for each one.
|Kind of development||What is it?||Important characteristics|
|Application||Developing an application for multiple users. Could be a “boxed” software product or software as a service.||Lots of code, often millions of lines. Design is highly complex and time consuming, as code will be used by multiple users with varying requirements, so needs to be highly flexible. Application will be composed of large scale components. Long application lifetime, e.g. 15 years. Hence large amount of effort required to maintain and update existing code, as opposed to developing new functionality from scratch. Effort/reward ratio for automated testing is high. Documentation, both in code and external is very important.|
|Solution||Developing bespoke solutions for individual clients.||Design is typically easier than application development, as code can be bespoke to specific customer. Testing and documentation approach may depend on whether your business is maintaining the solution after the initial delivery. Automated testing may be more useful for the initial development of functionality as opposed to for regression testing.|
Architecture (large scale)
- An enterprise application should always be split into components, which should be loosely coupled, and can be built and/or tested independently. For a typical app, you should expect to have 5-15 components, with each one between 10-100kloc. This doesn’t mean you need a microservice! Imo a microservice is only needed if at least one of two things is true: one – you need to scale the service independently, two – you need to invoke the service from different applications. If these things aren’t true, you simply need a component, not a microservice. i.e. a module, with a public API and tests inside the module.
- You should have a repeatable, incremental build. i.e. if you have 50 modules and you only change code in the very last module, only that module should rebuild and only the tests for that module should run. Maven does not support this, but Gradle is a great tool for this. By repeatable, I mean checking out the same commit anywhere and running the build should produce the same result. e.g. don’t have a step on your CI server which mutates the source files to set a version number
- Many applications have several ways they can be accessed. e.g. web browser, desktop app, web services, file exchange. In this scenario, all of your useful code should be in the core app (e.g. Spring beans) and each API should just be a thin layer.
- Whenever you integrate a third party library or application, always write an abstraction layer over it. Otherwise you’ll kick yourself two years later when you need to replace the third party code and discover you have references to it in 500 classes.
- For logging, use SLF4J as a facade so that you and your customers can plug in a logging framework as needed.
Coding (small scale)
- Code to interfaces, not concrete classes. You make it much harder to update your code if you don’t use interfaces. My default approach is: an interface to define the contract, an abstract class to contain functionality that I can already see is reusable, then the concrete classes. When you need to add new functionality in the future, if it is close to what you already have, you can write a new class that extends your abstract class, but if it is noticeably different, you can write an entirely new class without breaking the contract with the calling code.
- Favour composition over inheritance. Inheritance fundamentally breaks the concept of a public API. e.g. you override several methods in a class. each one invokes the superclass method to perform some of its processing. Sounds okay right? But…for one of the methods, the super class method internally invokes a different method, which you’ve overridden. The process of dynamic dispatch means that it invokes your new version of the method, and the code falls over…
- Think twice about coding helper classes. An OO approach suggests that you might need to reconsider your object hierarchy and push the functionality down into the correct classes. (Thanks to: http://www.carlopescio.com/2011/04/your-coding-conventions-are-hurting-you.html)
- A method should only do one thing. When designing an algorithm, it should be broken down into small steps, each of which becomes a separate method. A top level method can then call each of these methods in turn to perform the required task.
- Methods should be short. As a rule of thumb, most methods should be fewer than 20 lines. If you are writing a method and it gets longer than this, it indicates that you’re trying to do too much in one method. Split it into multiple methods.
- Don’t use instance variables to share state between methods unless you really have to. By doing this you are giving all methods in the class access to it, but it might only be required by some of them. Generally, try to use the return value of the method to return data required by the calling code.
- All or most data required by a method should be passed in as parameters, rather than being accessed within the method. This is for two reasons:
- It makes it easier for the caller to see what data the method requires.
- It makes the method easier to unit test.
- If you need to provide several methods or constructors that do similar things but take different parameters, put the implementation in one method and have the other methods call it, rather than copying the implementation code into several methods. (Method chaining.)
- Methods shouldn’t have side effects. i.e. they should do what the method signature implies they should do, no more, no less.
- Don’t design or write code that includes “special cases”. There are two related reasons for this: Firstly, the special case code is usually hidden within a method, so other developers can’t tell that it exists and won’t understand how the code works. Secondly, once you have introduced the special case in one part of your code, it will have to be added to other sections of code as the application grows. This is a bug waiting to happen as (because of point one) other developers won’t know about your special case so won’t account for it.The correct thing to do is to push the special case functionality down into the class that requires it, so that it happens automatically when the calling code calls the appropriate method on the object.
I think its hard to generalise about how to write performant code, but here are some pointers:
- For code that is querying a database and processing the result of the query, you need to maximise the amount of useful data you get in the initial query, minimise the amount of unnecessary data and minimise the additional processing that you need to do on the query results to get what you need. There is no need to instantiate large numbers of objects and perform processing on them if you can actually get the end result simply by writing a better database query.
- Any code that accesses large numbers of database records should retrieve them in batches to keep the memory footprint low.
- Use database indexes to reduce lookup times for fields that are commonly used for retrieval.
- Does the code you’re writing actually need to run in real time? Or could it be done asynchronously? e.g. either the user initiates it and gets the result later, or the action is performed periodically and the user just views the results of the latest run
- For web apps, Apache Bench is great for simple performance testing.
- For all new functionality, the specification should be written based on a specification template. I’d recommend making the template as detailed as possible. If sections don’t apply, the developer can mark them as such, but if they miss something crucial you’ll be in trouble later. As well as the obvious requirements, design, code changes etc, you might like to include: assumptions and limitations, security, performance, automated testing, configuration parameters, database changes and interaction with third party systems.
- Use an issue review process and/or issue template. It means that even issues which don’t fall into the category of requiring a specification (like bug fixes) are still checked.
- Use automated testing to test all of your code. I recommend using Test Driven Development (TDD) as it helps developers to tie down how the code should behave, explore all of the variations of input parameters and means that code can be tested as soon as it is written. My recommended approach is:
- Write your unit tests, integration tests and empty application classes.
- Run all of your tests and confirm they fail.
- Start writing your application code from the inside out – first write the methods that can be unit tested and rerun the unit tests. Then, when you get to writing the code that can only be tested by running your integration tests, if the tests fail, you know the error can only be in the portions of code that aren’t covered by your unit tests.
- Understand how to perform data driven testing, where you can list off different permutations of input data. Available in both TestNG and JUnit. For the TestNG docs, see https://testng.org/doc/documentation-main.html
- If your app has a web API, you’ll probably need web testing as well as code level testing. I recommend Selenium.
- You want to be able to run your tests quickly and easily, so as many of them as possible should be unit tests that only need the compiled class under test, rather than system tests, that need your entire app running. Remember you can use mocks, stubs and in-memory databases to make more of your tests into unit tests. The TestNG book has lots of good info on this.
- Use a code coverage tool like Cobertura or Jacoco. I am pragmatic about code coverage – although it does help to see what areas you are lacking in tests, remember that normally all you are seeing is if a line is executed by a test, you aren’t seeing if the result of that line is checked by an assertion, and unless you explicitly configure it, you won’t see branch coverage. i.e. if a code has two if statements and each one has two branches, you actually have 2 x 2 = 4 routes through the code. You could have 100% line coverage but only test 2 out of those 4 routes. If you want to use Jacoco with Gradle, I’ve documented my set up here: Jacoco and Gradle.
- Use static analysis testing as well as runtime testing. It is fast to run and has the advantage that once you have configured the rules you are going to apply to your code, they will run against new code automatically. I recommend PMD and SpotBugs. I tend not to use CheckStyle as it is mostly stylistic, rather than finding real bugs. PMD is good because its easy to write your own rules so if you can identify conditions that must be true across your app, you can enforce them easily. See, for example, Enforcing Spring role based security with PMD.
- Learn a scripting language. They are great for automating small tasks. My preferred scripting language is Groovy, but Ruby and Python are also good languages. Perl and PHP aren’t as powerful.
- Learn the keyboard shortcuts for all apps you use.
- Learn regular expressions.
- Install Cygwin so you can use grep on Windows.
- Keep up to date with current technology. An easy way to do this is to subscribe to a few mailing lists. I get:
- Java Developer Journal
- IBM Developerworks
- Java Code Geeks.
I use IntelliJ. I think it is an amazingly powerful IDE. See my hints and tips here: IntelliJ Hints and Tips
I use Atom for some text editing tasks, but most of the time I just stick with IntelliJ because it is so good.
PuTTY – for SSH connections.
PuTTY Connection Manager or PuTTY tray – both allow you to save PuTTY login details and login at the click of a button.
FileZilla – for ftp and sftp.
Cygwin – for grep and other unix commands on Windows.
WinMerge – visual diff. (Although for a three way diff you might want to try the Perforce Merge Tool.)
Outlook and e-mails
- For every e-mail, you should do one of three things:
- Bin it.
- Archive it.
- Flag it for action.
- Use rules to automatically process e-mails when you can.
- Use categories to make it easier work on your task list.
- Use the Outlook pst backup plugin. (Thanks to Dom Day for this.)
8 Responses to Software development best practices