By the end of the course, you will be expected to have a strong grasp of all of the design principles listed below, and be able to discuss and apply them in your own work. A few notes: First, many of them overlap with each other. For example, several are simply more specific reinforcements or examples of a broader principle. Second, some of them may contradict each other. With design, there is often no one right answer, no silver bullet, but a matter of balancing tradeoffs and meeting the needs of the specific situation you are designing for. Third, many of them are prescriptive: things you should always or never do; others are guidelines: things to aspire to but that cannot always be adhered to. Nearly all of these guidelines are linked to topics in Effective Java, Second Edition (and most of this advice is unchanged in the third edition, though the item numbers may shift around slightly), for further reading.
Write Javadoc comments for all public classes and methods, however small and trivial. The comment for a class should be at least two sentences, and provide information not already clear from its definition. A person reading your comments should understand the purpose, details of inputs and outputs without actually reading its code! (Effective Java, item 44)
Use interface types over concrete classes wherever possible. *(Effective Java, items 18 and 52). For example, instead of using ArrayList as a type, use List.
Fields and methods should have as restrictive access modifiers as possible, unless they hinder using them as they should. Fields must always be private, unless there is a class at present that extends it and needs to access to that field. Methods and classes should be as private as possible. (Effective Java, items 13, 14 and 15)
Do not make any changes in your design with the sole purpose of testing. This includes making fields and methods public.
Any method in an interface must belong there (i.e. must be relevant to all implementations of that interface).
Always remember that there may be future implementations of an interface that exists now, and all present and future implementations are beholden to what the interface specifies.
Classes should not have public methods that are not in the interface (aside from constructor).
Catch and handle/report errors as early as possible, but only where they can be addressed. If you can prevent an error from happening (i.e. making that error creates a compiler error, do it! (Effective Java, items 60 and 65)
Consider using composition instead of inheritance. (Effective Java, item 16)
Use class types over Strings. (Effective Java, item 50)
Use exceptions only for exceptional situations -- not for flow control. (Effective Java, item 57) For example, do not throw and catch exceptions in the same method. Do not catch exceptions when you cannot do anything meaningful to address its underlying cause.
Checked vs unchecked exceptions: A checked exception indicates a reasonable expectation that the program can recover. Unchecked exceptions indicate programmer error (may still be recoverable). (Effective Java, items 58 and 59)
Don't leave things in an inconsistent state for any substantive length of time. (Effective Java, items 1, 4, 6, 15 and 38)
Beware of references, copies, and mutation. Do not return references to private mutable fields. (Effective Java, item 39)
Separate responsibilities: one class, one responsibility.
If your code needs to explicitly know the type of an object in order to do its job: stop. Use class hierarchies and dynamic dispatch. (Effective Java, item 20)
Remove code duplication. Use private helpers, abstractions, etc.
Open for extension, closed for modification: make changes without modifying existing code; write code to support later changes without modification.
Extensibility: design to make likely later changes easier.
Write tests first, cover the range of situations, edge cases. Write code to be testable (avoid System.out).
One test should have one purpose. Testing this purpose may require one or more assert statements: this is OK. If a test is too long even if it serves one purpose, divide it into multiple tests with more specific purposes.
Loose coupling over tight coupling (e.g. avoid hardcoding a reference to System.out). Write reusable components when possible.
You cannot change an interface once it's published.
If you override equals(), override hashCode(), and vice-versa. (Effective Java, items 8 and 9)
Reuse existing exceptions, classes, libraries, and designs. (Effective Java, item 47)