- Untrue assumptions are like landmines, and tend to blow up in our faces at inopportune times.
- Before changing code, one should have a good handle understanding of how the current implementation works.
- Accidents of Implementation: Relying on behavior that is inherently unreliable. EX: undocumented errors, boundary conditions, call order
- When accidents of implementation happen, we can end up with surprising errors in one end of the application when the other is fixed.
- These types of errors can be common in rails Models since we update their state throughout a request.
- There are many instances of
ActiveRecord::Base#reload
called in our application. When we have to explicitly reload a model in a procedure, it can create downstream dependencies where methods expect a fresh object, but due to a change upstream we now have stale data that could lead to errors. has_many
relations also face issues when we do not use the collection constructors.
- There are many instances of
# Good
# items now has all items for this model
my_model.items.create!(some: :attribute)
# Bad
# called up here so rails caches the results
my_model.items
new_item = Item.create(model_id: my_model.id, some: :new_attribute)
my_model.items.include? new_item # false
my_model.items(true).include? new_item # true
- Accidents of Context & Implicit Assumptions: Relying on assumptions that might not be true.
- Assumptions about the country where most of our customers live…
- Always be aware of what you are doing (try and only act with explicit intent).
- Attempting to build an application you don’t understand or to use a technology you are not familiar with is an invitation for trouble.
- Rely only on reliable things.
- Document your assumptions
- Prioritize your effort: spend time on the most important things first
- Don’t let existing code dictate future code. If we see something has been made obsolete or has not been updated to the new business policy, fix it.
- It is important to always strive for efficiency and try to design efficient algorithms
- Big O is a useful tool for expressing the worst-case runtime a given algorithm.
- Be aware of the shortcomings of Big O, since we only show the highest order terms in big O, then
O(n^2) != O(n^2)
, since low order terms still effect runtime.
- Be aware of the shortcomings of Big O, since we only show the highest order terms in big O, then
- Use common sense estimation to decide when it is right to spend time optimizing your algorithm
- Simple Loops: Okay when you know your input will not be too large for the application. These will typically be O(n).
- Nested Loops: Probably a sign that there is a better way, but can be useful. These will typically be O(n^2).
- Binary Chop: Halves the group in consideration through each iteration, performance is likely O(lg(n)).
- Divide and Conquer: Algorithms that partition work, and then work on the sets separately until it finds a result. These typically perform in O(n lg(n))
- Combinatoric: Looking at the permutations of things, usually performed in O(n!). Try to steer clear of these.
- Be certain when testing code to try it with variable input sizes.
- Use Profilers to figure out if its the right time to optimize your algorithm. No need to spend the effort if the gain is not noticeable.
- When code is wrong, fix it.
- When to refactor:
- Duplication: Promote Dry Design
- Nonorthogonal-Design (Orthogonal code is lovely coupled and does not have wide spread effects when changed): When coupling is forming
- Outdated Knowledge: New business requirements
- Performance: Try and keep site uptime at 99.999%
- Updating earlier decisions is an exercise in pain management.
- Do not try to add functionality while refactoring.
- Have confidence in your tests before making changes.
- Make small steps, but push them often and test after each one.
- Test your individual components so you can have confidence in the system
- Testability should be a consideration in your initial design
- Unit tests verify behavior and confirm the contract
- Confirm the contract: pass not only the best case scenario, but also completely unexpected scenarios and ensure your code errors. Confirm the contract is as expected.
- Where should tests live:
- In Rails, we like to keep everything in a top level
test
/spec
directory - Other projects will keep tests closer to application code so usage examples never stray far.
- In Rails, we like to keep everything in a top level
- Build a test window:
- Ensure you have a way to confirm behavior in the field.
- Many services have an
/is_alive
to check simple things like the git SHA of the running code and a bool representing a database connection. - Use logs effectively to document input/output
- Ensure we promote a culture of testing.
- If we do not test our code, our users will.
- When we use static code generators, we must retain the knowledge of what they are providing for us.
- In rails, our generators are fairly simple and its clear the benefits of using them.
- Wizards that write code for you can save you the time of churning out boilerplate.
- Wizards should not allow you to “program by coincidence”, you should still fully understand how your code is running.