Chuck Conway is a software craftsman with nearly 30 years of experience. His expertise extends to Python, Generative AI, .Net, React JS, and CI/CD practices, emphasizing craftsmanship, architecture, and processes. Passionate about continuous improvement and automation, Chuck's career embodies a commitment to beautiful software and efficiency.
Chuck lives in Folsom, CA, with his wife Erin, daughter, and a cat.
Unit Tests – verify the behavior of a unit under tests in isolation
Integration Tests – verify the behavior of either a part of the system or a whole system. These tests are often brittle.
Acceptance Tests – verify the software from the user’s point of view.
Frameworks & Tools
SpecFlow – BDD
xUnit – unit testing
Moq – Mocking
NCrunch
Resharper
Notes
Triangulation
We generalize the implementation when we have two or more test cases. We add test cases until the right way of implementation starts to emerge.
Faking
Faking means returning constants and gradually replacing the value with variables and evolving the solution.
Obvious Implementation
Sometimes the implementation appears obvious, but as you start to poke at it with tests, it begin to crumble.
Grabbing the gold
You start from writing unit tests which cover the core functionality right away, and it fails with edge cases.
Suggestion: Write as many tests as possible before approaching the core functionality. Check for max values, empty strings, null values, min values… etc.
Uncle Bob’s Suggestions: Write tests exactly in the following order: exceptional, degenerate, and ancillary
Degenerative cases are those which cause the core functionality to do "nothing"
Ancillary behaviors are those which support the core functionality
A change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. – Martin Fowler
Technical debt is the sum of the following coding shortcuts:
Duplication
Excess Coupling
Quick Fixes
Hacks
Why Refactor?
Improves Design
Improves Readability
Reveals Defects
Helps You Program Faster
Other than when you are very close to a deadline, however, you should not put off refactoring because you haven’t got time. Experience with several projects has shown that a bout of refactoring results in increased productivity. Not having enough time usually is a sign that you need to do some refactoring. – Martin Fowler
Refactoring Principles
Keep it Simple
Keep it DRY
Make It Expressive
Reduce Overall Code
Separate Concerns
Appropriate Level of Abstraction
Refactor Tips
Keep Refactoring’s Small
One At a Time
Make a Checklist
Make a ‘later’ list
Check in Frequently
Add Test Cases
Review the Results
Premature optimization is the root of all software evil. – Donald Knuth
Coding Philosophy
Keep it simple.
Deliver value.
Avoid over-engineering.
Avoid gold plating your code.
Principle of Least Surprise
Do what users expect
Be Simple
Be Clear
Be Consistent
Design API to behave as programmers would expect.
Organizing Code Smells into 5 groups
The Bloaters
Do it all god objects…
Small lean code bases and changed quickly
Long Methods
Keep methods smaller
10 lines or fewer
Avoid regions
The Object-Orientation Abusers
The Change Preventers
The Dispensables
The Couplers
The obfuscators
Environment Smells
Test Smells
Architecture Smells
Refactoring Long Methods
Extract Method
Introduce Parameter Object
Replace Temp with Query
Replace Method with Method Object
Compose Method
Place bits of functionality into smaller methods.
The result is a method that has a number of method calls.
Replace Nested Conditional with Guard Clause
Replace Conditional Dispatcher(switch statement) with Command Pattern
Move Accumulation to Visitor ()
Replace Conditional Logic Strategy pattern
Refactor Large Class
Violating Single Responsibility Principle
Too many instance variables
Too many private methods (Iceberg class)
Lack of cohesion
Some methods work with some instance variables, others with others
Compartmentalized class
Refactoring Large Classes Strategy
Extract Method (and hopefully combine logic)
Extract class
Extract Subclass/Extract Interface
Replace Conditional Dispatch with Command
Replace State-Altering Conditionals with State Machine
Replace Implicit Language with Interpreter Pattern
Primitive Obsession
Over-use of primitives, instead of better abstractions, results in excess code to shoehorn the type. The primitives themselves can contain invalid states and therefore must be checked when the value is set or used.
Often Guard clauses and validation is used to everytime the value changes for the type.
Les Intention – Revealing code
Refactor away from Primitive Obsession
Replace Data value with Object
Replace Type Code with Class
Replace Type Code with Subclass
Extract Class
Introduce Parameter Object
Replace Array with Object
Replace State-Altering Conditionals with State
Replace Condition Logic with Strategy
Examples of a Primitive Data Obsession
ZIP / Postal Codes
Phone Numbers
Social Security Numbers
Telephone Numbers
Money
Age
Temperature
Address
Credit Card Information
Long Parameter List
May indicate procedural rather than OO programming style
Related Smells
Message Chains
MiddleMan
Rules of Thumb
No more than three arguments
No output arguments
Not intuitive
No flag arguments
means the method is doing more than one things
Selector arguments
MEans the method is doing more than one thing.
In general
Prefer more, smaller, well-named methods to fewer methods with behavior controlled by parameters
Always be measuring – don’t do it because you think it’s the right thing to do. It might not be.
Managed memory is cheap. That’s not true, allocation is cheap and garbage collection is expensive. Garbage collection stops the world while things are cleaned up and optimized. Sometimes memory cleanup can impact the performance of the application.
Performance Low Hanging Fruit
Object reuse
Object pooling. Pass in existing array rather than allocating a new one. Preallocate array length.
String concatenation.
Use StringBuilder, Preallocate length if possible.
In Code: MyParamsMethod("Hello", "World"); MyParamsMethod();
Complier: MyParamsMethod(new {"Hello", "World"}); MyParamsMethod(new [0]); // this creates a new array object in memory
Instead, do this in code: MyParamsMethod(new {"Hello", "World"}); MyParamsMethod(Array.Empty); // reuses an existing empty string array in memory. It's the same idea behind string.Empty
Suggestion: Introduce overloads with common number of arguments.
Boxing
Boxing creates a new object on the heap. Now you have two memory locations with the same value. Changing either value does not impact the other value.
Suggestion: Introduce generic overloads
Closures
The complier converts closures to classes. Captured values are passed in as constructor parameters. This class is then allocated to the heap.
Suggestion: Avoid critical paths. Pass state as argument to lambda. Investigate local functions. Since local functions lifetime is known, it can be allocated on the stack where allocation and cleanup are cheap.
LINQ
Lambda expressions are treated the same as Closures. They are allocated as a class to the heap. Because much of LINQ is based on static methods, additional allocations of Iterators and IEnumerable happen.
Suggestion: Avoid critical paths. Use good old foreach and if statements
Iterators
iterators are rewritten as a state machine, which means more allocations to the heap.
Suggestion: Return a collection. Be aware of the cost.
async/await
Async/await also, generate a state machine. Task and Task also trigger more allocations which then can’t be reused.