0%

Good Code, Bad Code is a clear, practical introduction to writing code that’s a snap to read, apply, and remember. With dozens of instantly-useful techniques, you’ll find coding insights that normally take years of experience to master. In this fast-paced guide, Google software engineer Tom Long teaches you a host of rules to apply, along with advice on when to break them!

Table of Contents

  1. Good Code, Bad Code
  2. Copyright
  3. contents
  4. front matter
    1. preface
    2. acknowledgments
    3. about this book
    4. Who should read this book
    5. How this book is organized: A roadmap
    6. About the code
    7. liveBook discussion forum
    8. How to use the advice in this book
    9. Further reading
    10. about the author
    11. about the cover illustration
  5. Part 1 In theory
  6. 1 Code quality
    1. 1.1 How code becomes software
    2. 1.2 The goals of code quality
    3. 1.2.1 Code should work
    4. 1.2.2 Code should keep working
    5. 1.2.3 Code should be adaptable to changing requirements
    6. 1.2.4 Code should not reinvent the wheel
    7. 1.3 The pillars of code quality
    8. 1.3.1 Make code readable
    9. 1.3.2 Avoid surprises
    10. 1.3.3 Make code hard to misuse
    11. 1.3.4 Make code modular
    12. 1.3.5 Make code reusable and generalizable
    13. 1.3.6 Make code testable and test it properly
    14. 1.4 Does writing high-quality code slow us down?
    15. Summary
  7. 2 Layers of abstraction
    1. 2.1 Nulls and the pseudocode convention in this book
    2. 2.2 Why create layers of abstraction?
    3. 2.2.1 Layers of abstraction and the pillars of code quality
    4. 2.3 Layers of code
    5. 2.3.1 APIs and implementation details
    6. 2.3.2 Functions
    7. 2.3.3 Classes
    8. 2.3.4 Interfaces
    9. 2.3.5 When layers get too thin
    10. 2.4 What about microservices?
    11. Summary
  8. 3 Other engineers and code contracts
    1. 3.1 Your code and other engineers’ code
    2. 3.1.1 Things that are obvious to you are not obvious to others
    3. 3.1.2 Other engineers will inadvertently try to break your code
    4. 3.1.3 In time, you will forget about your own code
    5. 3.2 How will others figure out how to use your code?
    6. 3.2.1 Looking at the names of things
    7. 3.2.2 Looking at the data types of things
    8. 3.2.3 Reading documentation
    9. 3.2.4 Asking you in person
    10. 3.2.5 Looking at your code
    11. 3.3 Code contracts
    12. 3.3.1 Small print in contracts
    13. 3.3.2 Don’t rely too much on small print
    14. 3.4 Checks and assertions
    15. 3.4.1 Checks
    16. 3.4.2 Assertions
    17. Summary
  9. 4 Errors
    1. 4.1 Recoverability
    2. 4.1.1 Errors that can be recovered from
    3. 4.1.2 Errors that cannot be recovered from
    4. 4.1.3 Often only the caller knows if an error can be recovered from
    5. 4.1.4 Make callers aware of errors they might want to recover from
    6. 4.2 Robustness vs. failure
    7. 4.2.1 Fail fast
    8. 4.2.2 Fail loudly
    9. 4.2.3 Scope of recoverability
    10. 4.2.4 Don’t hide errors
    11. 4.3 Ways of signaling errors
    12. 4.3.1 Recap: Exceptions
    13. 4.3.2 Explicit: Checked exceptions
    14. 4.3.3 Implicit: Unchecked exceptions
    15. 4.3.4 Explicit: Nullable return type
    16. 4.3.5 Explicit: Result return type
    17. 4.3.6 Explicit: Outcome return type
    18. 4.3.7 Implicit: Promise or future
    19. 4.3.8 Implicit: Returning a magic value
    20. 4.4 Signaling errors that can’t be recovered from
    21. 4.5 Signaling errors that a caller might want to recover from
    22. 4.5.1 Arguments for using unchecked exceptions
    23. 4.5.2 Arguments for using explicit techniques
    24. 4.5.3 My opinion: Use an explicit technique
    25. 4.6 Don’t ignore compiler warnings
    26. Summary
  10. Part 2 In practice
  11. 5 Make code readable
    1. 5.1 Use descriptive names
    2. 5.1.1 Nondescriptive names make code hard to read
    3. 5.1.2 Comments are a poor substitute for descriptive names
    4. 5.1.3 Solution: Make names descriptive
    5. 5.2 Use comments appropriately
    6. 5.2.1 Redundant comments can be harmful
    7. 5.2.2 Comments are a poor substitute for readable code
    8. 5.2.3 Comments can be great for explaining why code exists
    9. 5.2.4 Comments can provide useful high-level summaries
    10. 5.3 Don’t fixate on number of lines of code
    11. 5.3.1 Avoid succinct but unreadable code
    12. 5.3.2 Solution: Make code readable, even if it requires more lines
    13. 5.4 Stick to a consistent coding style
    14. 5.4.1 An inconsistent coding style can cause confusion
    15. 5.4.2 Solution: Adopt and follow a style guide
    16. 5.5 Avoid deeply nesting code
    17. 5.5.1 Deeply nested code can be hard to read
    18. 5.5.2 Solution: Restructure to minimize nesting
    19. 5.5.3 Nesting is often a result of doing too much
    20. 5.5.4 Solution: Break code into smaller functions
    21. 5.6 Make function calls readable
    22. 5.6.1 Arguments can be hard to decipher
    23. 5.6.2 Solution: Use named arguments
    24. 5.6.3 Solution: Use descriptive types
    25. 5.6.4 Sometimes there’s no great solution
    26. 5.6.5 What about the IDE?
    27. 5.7 Avoid using unexplained values
    28. 5.7.1 Unexplained values can be confusing
    29. 5.7.2 Solution: Use a well-named constant
    30. 5.7.3 Solution: Use a well-named function
    31. 5.8 Use anonymous functions appropriately
    32. 5.8.1 Anonymous functions can be great for small things
    33. 5.8.2 Anonymous functions can be hard to read
    34. 5.8.3 Solution: Use named functions instead
    35. 5.8.4 Large anonymous functions can be problematic
    36. 5.8.5 Solution: Break large anonymous functions into named functions
    37. 5.9 Use shiny, new language features appropriately
    38. 5.9.1 New features can improve code
    39. 5.9.2 Obscure features can be confusing
    40. 5.9.3 Use the best tool for the job
    41. Summary
  12. 6 Avoid surprises
    1. 6.1 Avoid returning magic values
    2. 6.1.1 Magic values can lead to bugs
    3. 6.1.2 Solution: Return null, an optional, or an error
    4. 6.1.3 Sometimes magic values can happen accidentally
    5. 6.2 Use the null object pattern appropriately
    6. 6.2.1 Returning an empty collection can improve code
    7. 6.2.2 Returning an empty string can sometimes be problematic
    8. 6.2.3 More complicated null objects can cause surprises
    9. 6.2.4 A null object implementation can cause surprises
    10. 6.3 Avoid causing unexpected side effects
    11. 6.3.1 Side effects that are obvious and intentional are fine
    12. 6.3.2 Unexpected side effects can be problematic
    13. 6.3.3 Solution: Avoid a side effect or make it obvious
    14. 6.4 Beware of mutating input parameters
    15. 6.4.1 Mutating an input parameter can lead to bugs
    16. 6.4.2 Solution: Copy things before mutating them
    17. 6.5 Avoid writing misleading functions
    18. 6.5.1 Doing nothing when a critical input is missing can cause surprises
    19. 6.5.2 Solution: Make critical inputs required
    20. 6.6 Future-proof enum handling
    21. 6.6.1 Implicitly handling future enum values can be problematic
    22. 6.6.2 Solution: Use an exhaustive switch statement
    23. 6.6.3 Beware of the default case
    24. 6.6.4 Caveat: Relying on another project’s enum
    25. 6.7 Can’t we just solve all this with testing?
    26. Summary
  13. 7 Make code hard to misuse
    1. 7.1 Consider making things immutable
    2. 7.1.1 Mutable classes can be easy to misuse
    3. 7.1.2 Solution: Set values only at construction time
    4. 7.1.3 Solution: Use a design pattern for immutability
    5. 7.2 Consider making things deeply immutable
    6. 7.2.1 Deep mutability can lead to misuse
    7. 7.2.2 Solution: Defensively copy things
    8. 7.2.3 Solution: Use immutable data structures
    9. 7.3 Avoid overly general data types
    10. 7.3.1 Overly general types can be misused
    11. 7.3.2 Pair types are easy to misuse
    12. 7.3.3 Solution: Use a dedicated type
    13. 7.4 Dealing with time
    14. 7.4.1 Representing time with integers can be problematic
    15. 7.4.2 Solution: Use appropriate data structures for time
    16. 7.5 Have single sources of truth for data
    17. 7.5.1 Second sources of truth can lead to invalid states
    18. 7.5.2 Solution: Use primary data as the single source of truth
    19. 7.6 Have single sources of truth for logic
    20. 7.6.1 Multiple sources of truth for logic can lead to bugs
    21. 7.6.2 Solution: Have a single source of truth
    22. Summary
  14. 8 Make code modular
    1. 8.1 Consider using dependency injection
    2. 8.1.1 Hard-coded dependencies can be problematic
    3. 8.1.2 Solution: Use dependency injection
    4. 8.1.3 Design code with dependency injection in mind
    5. 8.2 Prefer depending on interfaces
    6. 8.2.1 Depending on concrete implementations limits adaptability
    7. 8.2.2 Solution: Depend on interfaces where possible
    8. 8.3 Beware of class inheritance
    9. 8.3.1 Class inheritance can be problematic
    10. 8.3.2 Solution: Use composition
    11. 8.3.3 What about genuine is-a relationships?
    12. 8.4 Classes should care about themselves
    13. 8.4.1 Caring too much about other classes can be problematic
    14. 8.4.2 Solution: Make classes care about themselves
    15. 8.5 Encapsulate related data together
    16. 8.5.1 Unencapsulated data can be difficult to handle
    17. 8.5.2 Solution: Group related data into objects or classes
    18. 8.6 Beware of leaking implementation details in return types
    19. 8.6.1 Leaking implementation details in a return type can be problematic
    20. 8.6.2 Solution: Return a type appropriate to the layer of abstraction
    21. 8.7 Beware of leaking implementation details in exceptions
    22. 8.7.1 Leaking implementation details in exceptions can be problematic
    23. 8.7.2 Solution: Make exceptions appropriate to the layer of abstraction
    24. Summary
  15. 9 Make code reusable and generalizable
    1. 9.1 Beware of assumptions
    2. 9.1.1 Assumptions can lead to bugs when code is reused
    3. 9.1.2 Solution: Avoid unnecessary assumptions
    4. 9.1.3 Solution: If an assumption is necessary, enforce it
    5. 9.2 Beware of global state
    6. 9.2.1 Global state can make reuse unsafe
    7. 9.2.2 Solution: Dependency-inject shared state
    8. 9.3 Use default return values appropriately
    9. 9.3.1 Default return values in low-level code can harm reusability
    10. 9.3.2 Solution: Provide defaults in higher level code
    11. 9.4 Keep function parameters focused
    12. 9.4.1 A function that takes more than it needs can be hard to reuse
    13. 9.4.2 Solution: Make functions take only what they need
    14. 9.5 Consider using generics
    15. 9.5.1 Depending on a specific type limits generalizability
    16. 9.5.2 Solution: Use generics
    17. Summary
  16. Part 3 Unit testing
  17. 10 Unit testing principles
    1. 10.1 Unit testing primer
    2. 10.2 What makes a good unit test?
    3. 10.2.1 Accurately detects breakages
    4. 10.2.2 Agnostic to implementation details
    5. 10.2.3 Well-explained failures
    6. 10.2.4 Understandable test code
    7. 10.2.5 Easy and quick to run
    8. 10.3 Focus on the public API but don’t ignore important behaviors
    9. 10.3.1 Important behaviors might be outside the public API
    10. 10.4 Test doubles
    11. 10.4.1 Reasons for using a test double
    12. 10.4.2 Mocks
    13. 10.4.3 Stubs
    14. 10.4.4 Mocks and stubs can be problematic
    15. 10.4.5 Fakes
    16. 10.4.6 Schools of thought on mocking
    17. 10.5 Pick and choose from testing philosophies
    18. Summary
  18. 11 Unit testing practices
    1. 11.1 Test behaviors not just functions
    2. 11.1.1 One test case per function is often inadequate
    3. 11.1.2 Solution: Concentrate on testing each behavior
    4. 11.2 Avoid making things visible just for testing
    5. 11.2.1 Testing private functions is often a bad idea
    6. 11.2.2 Solution: Prefer testing via the public API
    7. 11.2.3 Solution: Split the code into smaller units
    8. 11.3 Test one behavior at a time
    9. 11.3.1 Testing multiple behaviors at once can lead to poor tests
    10. 11.3.2 Solution: Test each behavior in its own test case
    11. 11.3.3 Parameterized tests
    12. 11.4 Use shared test setup appropriately
    13. 11.4.1 Shared state can be problematic
    14. 11.4.2 Solution: Avoid sharing state or reset it
    15. 11.4.3 Shared configuration can be problematic
    16. 11.4.4 Solution: Define important configuration within test cases
    17. 11.4.5 When shared configuration is appropriate
    18. 11.5 Use appropriate assertion matchers
    19. 11.5.1 Inappropriate matchers can lead to poorly explained failures
    20. 11.5.2 Solution: Use an appropriate matcher
    21. 11.6 Use dependency injection to aid testability
    22. 11.6.1 Hard-coded dependencies can make code impossible to test
    23. 11.6.2 Solution: Use dependency injection
    24. 11.7 Some final words on testing
    25. Summary
  19. Appendix A. Chocolate brownie recipe
  20. Appendix B. Null safety and optionals
    1. B.1 Using null safety
    2. B.1.1 Checking for nulls
    3. B.2 Using optional
  21. Appendix C. Extra code examples
    1. C.1 The builder pattern
  22. index
34.239.170.244