...an approach to improve software and source code, in terms of:
- General Quality - Reducing the number of software bugs and problems
- Making the source code comprehensible - the source code should be readable and understandable so it is approved in a code audit.
- Making the software behave in a predictable manner despite unexpected inputs or user actions
Automated code testing
If another developer doesn't understand your intent, they make make incorrect assumptions about the code and make inappropiate code changes.
Comprehensible code requires less time to change.
Build clean code
Refactor to make code easier to understand
Validation + Exception Handling
- Improves Comprehension
- Simplifies Maintenance
- Reduces Bugs
Bad code vs Bad Code
Write good code following clean coding techniques
Transform bad code to good code when possible through refactoring
Clean code is:
- Easy to read (for others and future you)
- Clear intent
- Simple - if it is too complex or too "elegant" can be hard to change
- Minimal - each part of the code does one thing and does that thing well, dependencies between code are kept to a minimal.
- Thoughtful - Code location is carefully considered.
- Improves Quality
- Confirms Maintenance
- Reduces Bugs
Not only unit tests, but also integration tests
Automated code testing makes it easier to test new code without having to run the whole application and all the steps to get there.
- Improves Predictability
- More consistent
- Reduces bugs
Anticipating failure.
Recover from failure
Should the code trust the data it's given?
Each non-private method that you write is a contract
Contract:
- Parameters
- Return Type
- Exceptions
Verify:
- Parameters
- Data
- Return Type
- Exceptions
Clear purpose - should do one thing and do it well, easier to understand, easier to debug and modify without introducing new bugs. Tests are easier to write because they are focused.
Good Name - should clearly identify the purpose, has clear intent on when and why it should be used
Focused Code - All of it should be focused on the single purpose, minimizing the need for extensive comments, should not have side effects such as modifying global data, modifying incoming parameters or calling unrelated methods.
Short Length - Code that is long suggest method does not have a single purpose
Testable - You should be able to call the method from an automated code test.
Predictable Result - The code should not react strangely when provided invalid information or if an unforeseen condition occurs.
A method should do what the name says, and nothing more.
Length matters because you should be able to tell at a glance what a method does.
"Code-behind" is really hard to test
A Class library is the best place to put non-UI logic, makes it reusable across different UIs, easier to test in automated tests.
The Repository Pattern includes a specific set of classes that handle the data access for the application
The first step in making a method predictable, is to check parameters and only allow valid input.
Guard clauses check the value of all incoming parameters, regardless of where the value comes from.
Minimize number of parameters
Consistent parameter ordering:
- From most important to least important.
- Flags are normally last
Fail Fast technique causes code to fail immediately when it receives something invalid
- Arrange
- Act
- Assert
Automated code testing must be:
- Structured
- Self-documented
- Automatic
- Repeatable
- Protects your code across time and space
Visual Studio (as of 2012) comes with MSTest, but also supports NUnit and other frameworks
Unit tests saves time, saves clicking through UI screens to get to code.
Lets you find bugs faster
Refactor/ Add Safely
Enhance your value
Minitmize Interruptions
Code first - write code based on the requirements and then write the tests
Test first - write tests based on the requirements and then write the code that makes the tests pass
Test for:
- Valid inputs
- Invalid inputs
- Each guard clause
- Assumptions
Everytime there's a bug that managed to get past unit tests, write a new unit test for it.
Garbage in / Garbage Out
Create OperationResult class for returning multiple values
Can return:
- Value
- Throw Exception
- Multiple value
- Null
Which one to choose depends on context
A method that returns a value type, could return a nullable type.
- Best Practices
- Clarity of Intent
- Predictability
Local variables should be declared closer to where they are needed.
Use clear naming
Where possible, initialize variables where they are declared.
var
keyword, use it:
- if the type is already in the right-hand side
- when dealing with complex return types
- Use Braces
- If not using braces, put the code in 1 line.
- Consider whether an else clause is needed.
- Think Positive - it's easier for readability to define the condition as a positive.
- Consider the order
- Always have a default case to make it more predictable
- The code in each statement should be a simple action
Enums should be used whenever you have a discrete set of values
Enums should not be nested within a class
Cast Carefully
Check value first before casting: use the as / if
operators
Assertions are warnings to the developer, a function that complains at runtime if an asserted assumption isn't true.
Debug.Assert()
If those conditions are not met, the program aborts and breaks into the debugger in that line of code.
Asserts are only executed in Debug Mode, not Release mode.
- Invalid User Entry - Use appropriate controls that limit input, use built in data validation, write a validation method, validate with guard clauses, proceed with a good default value
- Invalid or Missing Data - Validate incoming data, proceed without the value, display a message to the user.
- Code Construct Issues (eg switch statements without all cases) - Proceed with a default operation, ignore the issue, log it and display a message to the user.
- System Issues - Try again, proceed with an alternate operation, ignore the issue if it is not critical.
Notify the user only when necessary
Global exception in windows forms application:
// For UI thread exceptions
Application.ThreadException += new ThreadExceptionEventHandler(GlobalExceptionHandler);
//Force all Windows forms errors to go through our handler
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
//For non-UI thread exceptions
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(GlobalExceptionHandler);
// App run
static void GlobalExceptionHandler(object sender, EventArgs args)
{
MessageBox.Show("There was a problem with this application. Please contact support");
System.Windows.Forms.Application.Exit();
}
Do not use global exception handler for anticipated exceptions.
Handle locally in time (right after thrown) and DO SOMETHING (don't just catch and leave the block empty)
Catch Specific Exceptions
To propagate an exception, do not rethrow the caught exception, since the call stack will be lost.
try
{
//do something
}
catch (InvalidOperationException ex)
{
// throw ex; <- don't do this
throw; // <- do this
}