DEV Community

loading...

The Pragmatic Programmer highlighted points: Ch7. While We're Coding

Albert Mulia Shintra
Web Developer with the passion of solving problem and clean solution.
・7 min read

Disclaimer

This article is just highlighting the tip of the iceberg from the original book. Please read the book for better detail and examples given by the author.

1. Listen to Your Lizard Brain

Our instincts, aka the lizard brain, are simply a response to patterns packed into our nonconscious brain. Whatever their source, instincts share one thing: they have no words.

The trick is first to notice it is happening, and then to work out why.

When you are coding, the code is trying to tell you something. It's saying that this is harder than it should be. Whatever the reason, your lizard brain is sensing feedback from the code, and it's desperately trying to get you to listen.

Listen to Your Inner Lizard

First, stop what you're doing. Give yourself a little time and space to let your brain organize itself. Eventually, they may bubble up to the conscious level, and you have one of those a ha! moments.

If that's not working, try to externalizing the issue by writing a doodle of your code, or talking to your coworker or the rubber duck. You may have moments when you're explaining the problem and suddenly realized something.

If it's still not coming, then it's time for action by creating a prototype. If you're working on existing code and it's pushing back, then stash it away somewhere and prototype up something similar instead. While prototyping, if that nagging doubt suddenly crystallizes into a solid concern, then address it.

Not just our code, but we also deal with existing code, often written by other people. If you can see what drove them to write code that way, you may find the job of understanding it becomes a lot easier.

2. Programming by Coincidence

We should avoid programming by coincidence, relying on luck and accidental successes, in favor of programming deliberately.

If we don't know why the code is failing because we didn't know why it worked in the first place.

If it worked, why should we take the risk of messing with something that's working? We can think of several reasons:

  • It may not be really working, it might just look like it is
  • The boundary condition you rely on maybe just an accident, as it might behave differently in different circumstances
  • Undocumented behavior may change with the next release
  • Additional and unnecessary calls make your code slower
  • Additional calls increase the risk of introducing new bugs

Human beings are designed to see patterns and causes, even when it's just a coincidence. Don't assume it, prove it.

Finding an answer that happens to fit is not the same as the right answer. Assumptions that aren't based on well-established facts are the bane of all projects.

3. Algorithm Speed

Pragmatic Programmers estimate their resources that algorithms use such as time, processor, memory, and so on.

Normally, the size of the input will affect the algorithm: the larger the input, the longer the running time or the more memory used.

We find that whenever we write anything containing loops or recursive calls, we subconsciously check the runtime and memory requirements.

The Big-O notation, written O(), is a mathematical way of dealing with approximations.

Big-O is never going to give you actual numbers for time or memory or whatever: it simply tells you how these values will change as the input changes.

Address the potential problems and try to find a better approach to improve the algorithm speed. Then, test your estimates to see if it improve.

You also need to be pragmatic about choosing appropriate algorithms, the fastest one is not always the best for the job. It's always a good idea to make sure an algorithm really is a bottleneck before investing your precious time trying to improve it.

4. Refactoring

Code needs to evolve; it's not a static thing. Rather than construction, the software is more like gardening, it is more organic than concrete.

Refactoring is defined by Martin Fowler as a:

disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior

The critical part of this definition is:

  1. The activity is disciplined, not a free-for-all
  2. External behavior doesn't change

In order to guarantee that the external behavior hasn't changed, you need good, automated unit testing that validates the behavior of the code.

You refactor when you've learned something when you understand something better than you did last year, yesterday, or even just ten minutes ago.

Refactor early, refactor often

Refactoring, as with most things, is easier to do while the issues are small, as an ongoing activity while coding. It shouldn't take a week to refactor, that's a full-on rewrite.

Martin Fowler offers simple tips to refactor without doing more harm than good:

  1. Don't try to refactor and add functionality at the same time
  2. Make sure you have good tests before you begin refactoring
  3. Take short, deliberate steps, and test after each step to avoid prolonged debugging

5. Test to Code

Testing is Not About Finding Bugs

We believe that the major benefits of testing happens when you think about and write the tests, not when you run them.

Thinking about testing made us reduce coupling in our code and increase flexibility. Thinking about writing a test for our method made us look at it from the outside as if we're a client of the code, not the author.

We think this is probably the biggest benefit offered by testing: testing is vital feedback that guides your coding.

The basic cycle of TDD (Test Driven Development) is:

  1. Decide on a small piece of functionality you want to add
  2. Write a test that will pass once that functionality is implemented
  3. Run all tests. Verify that the only failure is the one you just wrote
  4. Write the smallest amount of code needed to get the test to pass, and verify it
  5. Refactor your code: see if there's a way to improve on what you just wrote, and make sure the test is still passed when you're done

If you follow the TDD workflow, you'll guarantee that you always have tests for your code.

We like to think of unit testing as testing against contract. We want to avoid creating a "time bomb", something that sits around unnoticed and blows up at an awkward moment later in the project. By emphasizing testing against contract, we can try to avoid as many of those downstream disasters as possible.

Even the best sets of tests are unlikely to find all the bugs. This means you'll often need to test a piece of software once it has been deployed, with real-world data flowing through its veins.

Log files containing trace messages are one such mechanism. Another mechanism for getting inside running code is the "hot-key" sequence or magic URL, to show a debugging message on the fly. This isn't something you normally would reveal to end-users, but it can be very handy for the help desk.

Treat test code with the same care as any production code. Keep it decoupled, clean, and robust.

Testing, design, coding - it's all programming

6. Property-Based Testing

There could be incorrect assumptions while writing unit tests. The code passes the tests because it does what it is supposed to do, based on your understanding.

Once we work out our contracts and invariants, we can use them to automate our testing.

Use Property-Based Testing to Validate Your Assumptions

At this point, the book gives more concrete examples that a test may only cover commonly known scenarios and doesn't cater to bad assumptions. Thus we can improve the test by adding more property to test those assumptions.

One suggestion is that when a property-based test fails, find out what parameters it was passing to the test function, and then use those values to create a separate, regular, unit test.

We believe that property-based testing is complementary to unit testing: they address different concerns, and each brings its own benefits.

7. Stay Safe Out There

As a matter of fact, you do need to be paranoid as spies or dissidents, every day.

There are cases where hundreds of records stolen at once, billions of dollars in losses. It's not because the attackers were terribly clever or competent, it's because the developers were careless.

The next thing you have to do is analyze the code for ways it can go wrong and add those to your test suite.

The survival time of an unpatched, outdated system on the open net is measured in minutes, or even less.

Pragmatic Programmers have a healthy amount of paranoia. There are a handful of basic principles that you should always bear in mind:

a. Minimize Attack Surface Area

Simple, smaller code is better. Less code means fewer bugs, fewer opportunities for a crippling security hole.

Never trust data from an external entity, always sanitize it before passing it on to a database, view rendering, or other processing.

By nature, any user anywhere in the world can call unauthenticated services, so limit the opportunity for a DoS (Denial of Service) attack at the very least.

If an account with deployment credentials is compromised, your entire product is compromised.

Don't give away information that may reveal credential knowledge, for example: "Password is used by another user".

Make sure any testing window and runtime exception reporting is protected from spying eyes.

b. Principle of Least Privilege

Don't automatically grab the highest permission level, such as root or Administrator. If needed, then take it, do the minimum amount of work, and relinquish your permission quickly to reduce the risk.

c. Secure Defaults

The default settings on your app, or for your users on your site, should be the most secure values.

d. Encrypt Sensitive Data

Don't leave personally identifiable information, financial data, passwords, or other credentials in plain text.

e. Maintain Security Updates

You need that security patch, but as a side effect, it breaks some portion of your application. Don't decide to wait, apply security patches quickly.

8. Naming Things

Your brain treats written words as something to be respected. We need to make sure the names we use to live up to this.

There are only two hard things in computer science: cache invalidation and naming things.

It's important the everyone on the team knows what the common names are being used and that they use them consistently.

When you see a name that no longer expresses the intent, or is misleading or confusing, then fix it.

If for some reason you can't change the now-wrong name, then you've got a bigger problem: an ETC (Easy to Change) violation. Fix that first, then change the offending name.

Make renaming easy, and do it often.


This chapter is quite long but they are very insightful for us to be aware of while we're coding. What's the struggle you face while coding? Please share it in the comment section and I'll be happy to learn from you!

Discussion (0)

Forem Open with the Forem app