Conventional wisdom says that once a project is in the coding phase, the work is mostly mechanical… We think this attitude is the single biggest reason that many programs are ugly, inefficient, poorly structured, unmaintainable, and just plain wrong.
- Andy Hunt and David Thomas, The Pragmatic Programmer
When I first expressed a serious interest in coding, I remember sharing my aspiration with friends and acquaintances. We were at a get-together in Williamsburg, along the East River. There was some general oohs and aahs followed by this very blunt statement: “Well, you know, coding is a dying industry. I heard that the future of code will be replaced by massive amounts of pre-generated lines of code.”
Needless to say, I was very perturbed. And the conversation did not last very long after that. And the believers of this mentality were very, very wrong. (I was too timid to call them out at the time, but hopefully this blog post is my way of spotlighting the errors of their ways).
As software is eating the world, it seems that writing custom and complex code (not merely assembling them) is more important than ever. Sure, there will be GUI platforms that enable non-developers to compose applications or websites (think Squarespace). But there are a remarkable number of sites, systems, and companies that require true programming. By 2024, there will be 1.3 million software engineering jobs. Employment of software developers is projected to grow 17 percent from 2014 to 2024, which is much faster than the average for all occupation. Why? Because of the high demand of software.
Not only was person-from-Williamsburg-party wrong, but The Pragmatic Programmer also talks about wizards, which generate massive amounts of skeleton code. While prevalent, the use of wizards as central to your application is detrimental. In a nutshell, the authors urge programmers to not use code that you don’t understand (it will become much more unwieldy and difficult to debug over time). Here are other key takeaways in one of my favorite chapters thus far, “While You Are Coding.”
Don’t Be a Zombie
When Hunt and Thomas use the term, “programming by coincidence,” I pictured a zombie at the keyboard. Zoning out and mindlessly banging at a keyboard, not really testing or thinking about the code. I think it’s easy to not be thinking critically about your work because of how much brain power coding requires, which can also be physically exhausting. But just imagine! The feeling of a good day’s work or the nonexistent dread of having to clean up the program in the future. By programming deliberately, you are constantly being aware of what you are doing. You’re working with a plan and testing your code and assumptions. Don’t allow existing code to inhibit or deteriorate the quality of future code. (For me, I have found that the added benefit of actively coding is not needing caffeine and sitting with a good posture since I’m so alert).
In my previous post, I shared my love for the term “fearless competence” which was coined by Uncle Bob. In this book, I appreciate the analogy between software development and gardening. After all, software is in flux and organic; things are constantly being moved as plants (or methods, in our case) grow and need more room or there are other certain types of environmental or interaction-based factors.
Refactoring is a continual process that should happen at any given moment. I used to believe that refactoring should happen after a sufficient number of lines or methods have been introduced to the code base. But, it can really happen when there is 1) duplication in the code, 2) nonorthogonal design, 3) outdated knowledge based on changes to the code or requirements, and 4) performance issues.
To better demonstrate refactoring, behold this ugly code. Admittedly, I wrote this method. Its purpose is to determine the winner of the tic-tac-toe game. But if you look at it closely, you can see that it is doing too much. First, it requires two arguments and there is a lot of repetition in the code as the method attempts to separately and sequentially determine if the `current_marker` or `opponent_marker` is the winning marker on the board. Toward the end, the method is also checking to see if the grid is filled and if so, it will return a tie. This method is no longer about checking for a winner.
def winner(current_marker, opponent_marker)
@board.size.times do |n|
if @board.is_row_filled?(n, current_marker) ||
elsif @board.is_row_filled?(n, opponent_marker) ||
return “tie” if @board.is_grid_filled?
Looking at it was tough, so I decided that it was best to refactor it. I also changed the output of the code to return a boolean. Notice how my method name changes as a result of that (it becomes a question, `is_winner`?). Here, I have DRY’d up the redundant code and am taking it a single marker; this can be the current player’s marker, the opponent player’s marker, the potential third or fourth player’s marker, etc. This method is officially reusable. I also do not test for whether a draw and this method will be called only if a win has already been confirmed.
@board.size.times do |n|
if @board.is_row_filled?(n, marker) ||
This is a new term for me, and I will do my best to explain it. After reading more about it, test harnessing seems more like the coordination of different aspects of testing. It is not a single software, but a conceptual collection.
What we have already established is that testing and TDD are core to active coding. Many a-great software crafters believe in building testability from the very beginning. One of my most cherished quotes from this chapter asserts that “testing is more cultural than technical; we can instill this testing culture in a project regardless of the language being used” and points to Perl’s commitment to unit and regression testing.
Test harnessing enables you to execute tests and generate reports while using a test library. There are various conditions and the program is monitored for its behavior and outputs. When a test harness is in place, and data is also prepared, tests should be executed with a single command (or even with the click of a button). Since tests are composable (made up of subtests of subcomponents), there should be flexibility in testing either parts of the system or the entire system. A test harness has the capability of standardizing setup and cleanup; selecting individual tests or all of the tests; analyzing output for results; and standardizing failure reporting.
It’s interesting to note that there is a difference between a test harness (e.g. JUnit for Java) and test framework (e.g. RSpec for Ruby). The former is made up of drivers and stubs, which are dummy programs; categorized into automation and integration testing; and lastly, does not allow “record and playback” scripting. (Have you ever looked up terms only to look up even more terms? I would compare it to reading the Game of Thrones wiki). Anyway, “record and playback” was a central feature of old-school, first generation test automation tools and used for regression testing. Every test case is a series of actions with test data. However, scripts were unstable and data errors or changes disrupted automation; it ended up being that minor changes to the application had to be made to the test as well.
…and there you go! The more you know, the better you will code. Peace out.