1.- Make the classes that contain the logic of your application stateless.
A stateless class is a class which doesn’t have state, or which state is formed by injected dependencies used to delegate behaviour. Since these classes don’t have state, the outcome of their methods is only dependant on their input. Stateless classes have the following advantages.
- Thread safe friendly.
- Deterministic behaviour
- Easy to test.
2.- Use unmodifiable beans as much as you can.
Unmodifiable beans are classes which state are set on creation and from them are prevented to change; in java, using the modifier final in your member fields best approaches this.
Unmodifiable beans offer the following advantages.
- Thread safe friendly. Actually: stateless logic classes + unmodifiable beans = Thread safety!
- Robustness. Unmodifiable beans eliminate the risk of holding references to objects that keep changing.
3.- Try to have your classes matching one of the following categories.
There are two main concerns a class can deal with: logic and data modelling. Having classes that deal with logic or data that don’t belong to them make your code more confusing and cluttered. Based on these two concerns, there are mainly 5 types of classes that you should try to adhere to:
- Service. 100% logic, publishes the main public end points of your application, they usually delegate to managers through dependency injection.
- Bean. Mainly data model but also logic, used to carry data and perform operations, the operations the bean can perform should be self contained.
- DTO. 100% data model, is a particular type of bean used to transport data betweeen different mediums.
- Business logic manager. 100% logic, encapsulates the implementation of the business rules defined for your domain.
- Integration manager. 100% logic, glues together business logic manager + external frameworks used in your project + beans.
4.- Avoid static methods.
Static methods usually contain miscellaneous logic required in your application, because is difficult to find an already existing class in the code to host them, they sometimes are created as static methods in abstract classes. It is much better to encapsulate the static methods as instance methods in utility classes such as AppUtils… The disadvantages of using static methods are:
- They are more difficult to mock out in tests
- Their implementation can’t be hidden behind an interface
- They can’t be injected.
5.- Use composition over inheritance.
Inheritance heavily violates encapsulation, and easily generates obscure code. One clear example is the java io library which entirely designed with the decorator pattern. Composition allows you to have completely equivalent functionality, but without breaking encapsulation, it is also the core concept of the most important design pattern from all them all, the strategy pattern.
6.- Design in layers.
Almost every single business application has a few broad main areas of concern that are completely unrelated from each other. These areas of concern should be translated into layers in your design to allow for a cleaner code. The most common layers are:
7.- Avoid child to parent, or child to sibling relationships.
Design is the art of abstracting a problem into its different entities and their relationships. These relationships most of the time express some sort of parent-child relationship, as entity A contains entity B, or entity A manages entity B. Having these relationships implemented only from the parent to the child allows for clearer an less coupled code. One clear example of this would be the way Lists are implemented in Java.
8.- Keep the invariants to be maintained by the programmer to a minimum.
Invariants are conditions that need to be kept in order for an algorithm to work as expected. For instance, constants are usually defined with Simple data types as integers, strings… and if they are used anywhere in your code, is to be expected that you will receive not any value, but the ones specified by your constants. Other examples of invariants are not so obvious, imagine that there is a service with a method process, that retrieves an order, the only parameter that should be accepted is the order id, if the method requires something else, like the creation date of the order, we are adding a unnecessary invariant to be kept by our consumers.
9.- Avoid side effects in methods.
A method should only do one thing. Methods with side effects are dangerous, as the output of the method is never clear, you should always aim for deterministic methods with no side effects and which only read their input, never change them.
Test, test, test… In software development is more productive to produce 7/10 of a robust application than a completely flaky application. Based on the previous classification of classes I would recommend the following approach for testing.
- Don’t test your beans.
- Unit tests your business logic managers.
- Integration tests for services and integration managers.
- And finally… always spend some time doing manual end to end test.
10+1.- Your judgement must be above everything.
Software development is not an enginery; there isn’t a set of well-defined problems that have well-defined solutions. Is not like building a bridge, or a building… every development is unique, what applies to one project doesn’t necessarily apply to other. What this means, is that there isn’t absolutely any rule that has to always be applied, all are best practices, or advises, but is up to you to decide what approach is better suited for the problem you have to solve now.