DEV Community

Cover image for # Pragmatic Programming: How to develop mental models and good practices to be a successful software developer?
Gharsa Amin
Gharsa Amin

Posted on

# Pragmatic Programming: How to develop mental models and good practices to be a successful software developer?

Introduction

In the fast-paced world of software development, staying adaptable and continuously learning is essential. This article synthesizes key principles of pragmatic programming that can help developers write better code, build more maintainable systems, and adapt to the constantly changing tech landscape. We'll explore how to develop stronger mental models to think and act pragmatically, whether as individual software developers or as part of a team.

Table of Contents

Learning and Growth

Continuous Learning

"The more different softwares, technologies, languages you know, the more valuable you are."

In our industry, what's hot today might be obsolete tomorrow. To remain relevant and valuable:

  • Learn at least one new language every year - Different languages solve problems in different ways, broadening your thinking
  • Talk tech with people outside your immediate circle - Network within your company or at local meetups; engage with the tech community, especially if you are underrepresented, to increase visibility and presence
  • Review and rebalance your skills regularly - Evaluate which technologies you need to brush up on and which might be time to let go

Pro tip: Maintain an engineering daybook where you record what you learn, ideas, debugging notes, best practices, and mistakes. This serves as an external memory, idea repository, and reflection tool all in one that you can revisit when needed.

Code Organization and Design

Embrace Orthogonality

Two or more things are orthogonal if changes in one do not affect any of the others. In a well-designed system, components are independent and decoupled.

"In a well-designed system, the database code will be orthogonal to the user interface: you can change the interface without affecting the database, and swap databases without changing the interface."

When systems are orthogonal:

  • Changes remain localized
  • Development and testing time are reduced
  • Code reuse is promoted
  • Systems are easier to reconfigure

Consider a layered architecture approach:

┌─────────────────────┐
│   User Interface    │
├─────────────────────┤
│   Business Logic    │
├─────────────────────┤
│   Data Access       │
├─────────────────────┤
│   Database          │
└─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

It's easier to write relatively small, self-contained components than a single large block of code. Simple components can be designed, coded, tested, and then forgotten—there's no need to keep changing existing code as you add new code.

Guidelines for Better Code Organization

  1. Avoid global data - Every time your code references global data, it ties itself into other components that share that data
  2. Avoid similar functions - Duplicate code indicates structural problems
  3. If it's important enough to be global, wrap it in an API
  4. Design shy code - Components should only deal with things they directly know about
  5. Follow the DRY principle - Coupled with decoupling and external configuration, this helps avoid critical, irreversible decisions
  6. Consider real-world coupling - Don't rely on external identifiers you can't control (phone numbers, postal codes, SSNs, email addresses)

Use Prototypes and Estimation

  • Build software prototypes to analyze and expose risk at reduced cost, especially for anything unproven, experimental, or critical
  • Develop estimation skills to intuitively understand the feasibility of proposed solutions

Defensive Programming

Trust No One, Not Even Yourself

"Pragmatic Programmers don't trust themselves either. Knowing no one writes perfect code, they build defenses against their own mistakes."

Key practices:

  • Use assertions to prevent the impossible - When you think "that could never happen," add code to check it
  • Design by contract - Clearly define what each component promises to deliver and what it expects in return
  • Manage resources consistently - Develop a plan for resource allocation and deallocation
  • Externalize configuration - Keep values that may change after deployment outside your application
def transfer_money(source_account, target_account, amount):
    # Assert our preconditions
    assert amount > 0, "Transfer amount must be positive"
    assert source_account.balance >= amount, "Insufficient funds"

    # Perform the transfer
    source_account.withdraw(amount)
    target_account.deposit(amount)

    # Assert our postconditions
    assert source_account.original_balance == source_account.balance + amount, "Source balance inconsistent"
    assert target_account.original_balance + amount == target_account.balance, "Target balance inconsistent"
Enter fullscreen mode Exit fullscreen mode

Transformational Thinking

Programs Are About Data

"Programming is about code, but programs are about data."

Focus on transformations - convert inputs to outputs:

  1. Start with requirements to define the overall program function
  2. Find steps that lead from input to output
  3. Build your program as a series of transformations

This approach leads to:

  • Cleaner code
  • Shorter functions
  • Flatter designs

Testing and Debugging

Testing Is Not About Finding Bugs

Testing provides feedback on your code's design, API, coupling, and more. The major benefits come from thinking about and writing tests, not just running them.

"A Test Is the First User of Your Code"

Test-Driven Development Cycle:

  1. Decide on a small piece of functionality
  2. Write a test that will pass once implemented
  3. Run tests to verify only your new test fails
  4. Write minimal code to make the test pass
  5. Refactor and ensure tests still pass

Debugging Mindset

Remember that debugging is problem-solving. When facing issues:

  1. Read the error message carefully
  2. Explain the bug to a colleague
  3. Understand the root cause rather than just the symptom
  4. Localize the bug
  5. Own your mistakes rather than blaming external libraries
  6. Consider if similar conditions exist elsewhere in the system

Feedback Loops

Always take small, deliberate steps, checking for feedback and adjusting before proceeding. Consider that the rate of feedback is your speed limit. Never take on a step or task that's "too big."

Embracing Change

Make Reversible Decisions

"Coupling is the enemy of change."

Strategies to stay flexible:

  • Write less code - Every line introduces potential for bugs
  • Avoid tight coupling - Individual components should be connected to as few other components as possible
  • Put configuration data outside your application
  • Embrace event-driven architectures:
    1. Finite State Machines
    2. Observer Pattern
    3. Publish/Subscribe
    4. Reactive Programming

Refactoring

Refactoring keeps code adaptable through ongoing small improvements rather than massive rewrites.

When to refactor:

  • When you've found duplication (DRY violation)
  • When design isn't orthogonal
  • When knowledge or requirements change
  • When usage patterns reveal new priorities
  • When performance needs improvement
  • When tests pass

Practical Wisdom

Don't Program by Coincidence

Program deliberately by:

  • Being aware of what you're doing
  • Understanding your code well enough to explain it
  • Proceeding from a plan
  • Relying only on reliable things
  • Documenting and testing your assumptions
  • Prioritizing effort on important aspects

Listen to Your Instincts

When something feels wrong in your code or design, stop and analyze those feelings. Your subconscious might be detecting patterns your conscious mind hasn't yet recognized.

Think About Algorithm Performance

Even if you rarely write sorting algorithms from scratch, understanding computational complexity helps you anticipate performance issues:

  • Simple loops: O(n)
  • Nested loops: O(n²)
  • Consider how large n can become in your specific context

Remember that the fastest algorithm isn't always best - for small datasets, simpler solutions often work fine.

Agile Is an Adjective, Not a Noun

"Agile is how you do something. You can be an agile developer. You can be on a team that adopts agile practices, a team that responds to change and setbacks with agility."

Agility in software development is about responding to change and the unknowns you encounter after setting out. The Agile Manifesto values:

  • Individuals and interactions over processes and tools
  • Working software over comprehensive documentation
  • Customer collaboration over contract negotiation
  • Responding to change over following a plan

Conclusion

The essence of pragmatic programming is combining technical excellence with adaptability. By embracing orthogonal design, defensive programming techniques, transformational thinking, and continuous feedback, you can write better software and respond more effectively to change.

Take small, deliberate steps, listen to your instincts, and remember that programming is a journey of constant learning and adaptation.


What principles from this article do you already practice? Which ones would you like to incorporate into your workflow? Share your thoughts in the comments!

Top comments (0)