Problem-solving techniques in software

Problem-solving techniques in software

Listen to your code: Problem-solving techniques in programming

As developers, we depend on feedback to produce great software in reasonable time. When we are facing a bug or trying to implement the next awesome feature we tend to fiddle with the program. We add, remove, comment and uncomment lines of code. We play with undo/redo, toggle switches or use Git version control to navigate between different versions of the software.

To determine the next step to take, it’s?crucial to reliably and quickly get feedback?from the previous experiment. Even if we have a Big Plan to move forward it’s good to know that we are still on the right track.

These experiments are mental tools to triangulate the problem: we use various techniques to get a better understanding of the issue at hand as well as the current state of the software. We are trying to know more facts and narrow down uncertain elements.

There are?numerous ways?to get closer to the solution, like the classical debugging, experimenting in the REPL, or simply restarting the application after each change to name a few.

They can even be?combined. For example, it’s quite common in the JS world to start a debug session and invoke functions from the REPL.

One recurring problem I often see is?sticking to a single strategy when it’s not working. Although it’s also a matter of preference, there is?no single “right” way?of doing things, each strategy has its advantages and disadvantages.

Even if you work on a single project, usually you can’t stick to a single strategy the whole time. Various parts of a codebase have different constraints regarding how you can interact with them, requiring constantly adapting mindset when it comes to solving problems.

Make sure you use the right tool for the job by?continuously monitoring your workflow and choosing your approach consciously. It’s not easy to take a step back and recognize that the problem requires a different method.

In the following posts, I am planning to dive in and focus on the individual options a programmer can choose from when he faces a problem, for example:

  • Manual verification through the UI
  • Log internal state
  • Debugging with breakpoints, conditional breakpoints, and tracepoints
  • Read-Eval-Print-Loop (REPL)
  • Tests and TDD

End-to-end manual verification

One of the most intuitive ways to work with software is to make a change and inspect the modifications in its user interface. While it seems easy and intuitive, I think it deserves a post on its own because it so essential and can be easily misused.

Its only requirement is that you have to be able to build and start the application on your machine. Usually, this is easy, but there are exceptions. If the system is too complex or has many dependencies, it might be challenging and time-consuming to start the whole thing locally. In this case, it might be handy to have access to a staging environment where you can deploy your changes easily.?Docker?might also be useful in this situation as it allows to package the application with its dependencies, so you don’t have to manually install everything on your computer.


Pro

Because it’s usually?cheap and requires very little effort to set up, it’s a great way to?get involved?in a new project. It’s also a good starting point if you work with an application that has?poor infrastructure or tooling?to support another kind of verification strategies.


It’s feasible to use manual testing to?verify things that can’t be reliably or cost-effectively tested otherwise. For example, when developing the layout of the UI of your application, chances are that the best thing you can do is to manually refresh the browser from time to time and check the results.

While it’s not directly an argument?for?manual verification, it’s worth checking out David Heinemeier Hansson’s post about?Test-induced design damage, and the?discussion?it sparked with Kent Beck and Martin Fowler.

Based on this discussion I think it’s safe to say that relying on manual verification in certain parts of the application can?lead to a cleaner design, as the original vision doesn’t have to be distorted just for the sake of testing or other verification strategies.


Con

Usually, this technique only supports?long feedback loops. Because it requires the whole application to be up and running, there is significant time between implementing a small change and seeing it in action.

The delay usually consists of two things:

  • redeploying and restarting the application
  • reproducing the state necessary to test the changes (for example, signing in and wading through a bunch of menus)

Depending on your stack, you might get away with some of these annoyances with hot code deployment. If you rely heavily on this strategy make sure to constantly seek opportunities for shortening this time.

However, using this technique excessively can be time-consuming for other reasons as well.

No alt text provided for this image

Because you have to manually test everything after each change, this technique?scales poorly?with the number of cases you have to check. As a consequence, it’s hard to implement changes in complex modules solely by utilizing this strategy. Not to mention that it can quickly become really boring to recheck the same thing multiple times.

Also, it only makes possible to?observe the externally visible behavior. It doesn’t matter if you know the exact place of a bug in the source code: you have to fight your way down to the code in question from the user interface. You have to carefully craft input data, to get the desired conditions for a function where you suspect a defect. Needless to say that you also have to deduce the internal state from the visible output.


Note to maintainers

In the end, most tasks involve some amount of manual testing. It doesn’t matter how you created the changes and what automation you have to verify correctness, you should check that the feature you’ve implemented integrates nicely with the overall user experience of the product and works well with other parts of the software.

Because it’s so essential, anybody who is responsible for maintaining a software (or acts as a lead in a team) should aim for making it more accessible for newcomers.

  • Make the application easy to build and run. Avoid having too many external dependencies. If you happen to have them, try Docker. Write a?clear?guide on how to install the required dependencies and build the application.
  • Provide mocks for external services where possible to reduce external dependencies required for development.
  • Provide sensible initial data, so one can experiment with the application right from the start.
  • Make it easy to develop with various IDEs and workflows. Everyone has a preference. Don’t lose developers because you only support?that other?tool.
  • Seek to ease typical development tasks. Say you develop a UI heavy web app for a client who is really picky on user interface design details. Is it possible to edit the UI related code without restarting the application every time?

Making your application more accessible to developers is beneficial for everybody, as it will make deployment and maintenance easier as well.


Summary

It’s essential for every developer in a team to be able to start the application and see it’s working as a whole in some way. It can affect delivered quality, sense of ownership, and without it, you can’t make a full, business-impacting change on your own.

Because it can be time-consuming, it’s important to find the balance, and recognize the limits of this technique. As a rule of thumb, don’t try to check every little change through its interface, but make sure to test the most critical user scenarios at least at the end.

Effective print debugging

"The most effective debugging tool is still careful thought, coupled with judiciously placed print statements." Brian Kernighan, "Unix for Beginners" (1979)


Need to know the value of a variable? No problem, just add?System.out.println("Something=" + something);?to the next line and you are all set. It’s easy, and chances are that you are modifying that piece of code anyway. There is no need to worry about logging frameworks, test suites, or debuggers; it’s tempting to quickly add a println to check something. And as we programmers are so lazy, we do this from time to time.

This technique share similarities with manual end-to-end verification discussed in the?previous post, but this time the code is augmented with additional print (or maybe log) statements to provide insight about what’s going on in the bowels of the application. The additional requirement is to have access to the generated output.


Adding print statements shouldn’t be hard even without an IDE, but it’s worth checking out?Komodo’s new?Print Debugging?feature in action. It looks really awesome as it generates print statements for any given variable with a single click.

No alt text provided for this image

IntelliJ IDEA?has a?Live template?shortcut built in to add print statements easily. Just type?soutv?and press?TAB?to print the name of the variable and its value.


No alt text provided for this image

Eclipse?has a similar feature as well. If you type?sysout?and press?CTRL+SPACE, it will expand to?System.out.println. By default, Eclipse doesn’t provide a template to print a specific variable like IDEA do, but a?custom template?can provide this feature.

Under the?Window > Preferences > Java > Editor > Templates?click?New?and add the following for?sysoutv:

No alt text provided for this image

Pro

Besides its main selling point that its a?no-brainer, print debugging has some other advantages. The generated log is a?timeline of events?with the snapshots of the internal state.

Authorizing: person='john'
Querying DB for 'john'
Record found: Person[id=42, username='john', firstName='John', lastName='Doe', age=20]
Checking age limit, ageLimit=18, user=Person[id=42, username='john', firstName='John', lastName='Doe', age=20]
Authorized        

If you use logging libraries instead of simple print statements, you get timestamps and line numbers for free as well.

It can be great to?catch sporadic events?or to?show trends and outliers?of certain properties. The collected data can be?easily visualized and processed?with Excel or similar tools.

If you record the input parameters and return value of a function while you use the system, the log can be a?great starting point to create automated unit tests. This can be handy if you have to modify code that lacks decent test coverage. This works best if your objects are nicely printable. In case of Java, it?enforces toString-friendly classes.

The end of the bughunt can be a good time for some retrospective. The places where you had to inject extra print statements might?pinpoint locations where the application’s logging could be improved. The ad-hoc?System.out.println?calls can be promoted to actual log statements that adhere to the project’s logging standards. By doing so, this technique can be used to catch bugs in the QA environment as well.


Con

The?long feedback loop?is true for this strategy as much as it is true for manual end-to-end verification. The application has to be redeployed after you add or remove the print statements, and the results have to be examined manually each time. The excessive use of logging can introduce problems on its own:?storing, displaying and interpreting large volumes of text can be challenging.

Besides, it requires?cluttering the codebase with print statements. Make sure not to commit them accidentally by?reviewing?the modified files.

This technique is a?poor choice if you don’t know exactly what you are looking for. Logging a few variables is one thing, but if you have to check multiple data structures and results from various method calls it can quickly get out of hands. It’s even worse if you have to experiment with APIs in the middle of the code base and try to figure them out solely by printing the result of various method calls.

Using print debugging excessively can lead to?boiling frog syndrome. Because it’s easy, it’s tempting to stick with it and add more and more print statements to the code, even when they clearly not get you closer to success. Try different approaches like debugging or unit testing when you feel overwhelmed by this method.


Summary

Simply adding println statements to the code can be one of the fastest ways of debugging. It builds atop of the same workflow used to build and run the application, and it’s a great way to catch bugs that don’t manifest in a typical development or debugging scenario. But it shouldn’t be the only tool in the toolbox, as in many cases – like exploring APIs or complex state – simple debugging techniques are more efficient.

Effective debugging with breakpoints

Know how to use the developer's Swiss Army knife


This process introduces an additional dependency to the development flow compared to the simple compile-run-manual check cycles, as it requires a debug connection to the application. While remote debugging is possible in most stacks, the typical use case is to do it locally with an IDE.


In order to use it, you have to:

  • deploy breakpoints to stop program execution at specific lines of code
  • and investigate the internal state of the program when it is paused.

Each of these steps can be done in multiple ways depending on your concrete use-case, so let’s dive a little bit deeper into the details.


Stop the world with Breakpoints

The breakpoints can be deployed anytime, even in the middle of an already started debugging session. You can start the program before or after you deploy the breakpoints, and you can even add more breakpoints while investigating an already paused program.

There are multiple types of breakpoints you can use. The most commonly used?unconditional breakpoint?always stops the execution when it’s hit. This breakpoint can be deployed in a single click on the sidebar. It’s frequently used, as it’s simple and typically provide everything you need to zoom into any line of code.

While it’s mostly fine, sometimes it can be limiting that it stops the execution every time. This is OK if the method in question is called only once, or you don’t care about which execution you inspect.


Conditional breakpoints

Consider the following program that calculates the?geometric mean?of scores for the given movies.

double geometricMeanOfRatings(List<Movie> movies) {
	double product = 1;
	for (Movie movie : movies) {
		product *= movie.getRating().score;
	}
	return Math.pow(product, 1D / movies.size());
}        

Usually, it works well, but imagine one day it starts to behave oddly: it returns 0 all the time. Placing a simple breakpoint at the return statement reveals that it returns zero because the value of the?product?variable is 0.0. It’s possible if the rating is 0 for at least one movie. But which movie is it? Placing an unconditional breakpoint inside the for loop would needlessly make the program stop for many times. (Consider hundreds of movies.)

The solution is to specify a?movie.getRating().score == 0?condition for the breakpoint, that stops right at the bogus movie.

No alt text provided for this image

Conditional breakpoints?are breakpoints parameterized to trigger only when certain criteria are met. The condition can be specified in the host language, making it really flexible.


Tracepoints

The breakpoint conditions don’t have to be simple statements like?x == 42?all the time. They can have many lines of code that will be executed as if they were part of the program.?Tracepoints?are one example of using “clever” conditions, where the condition is to execute a log statement and return false. They never suspend the running program, just print a debug message to the standard output.

Many IDEs have support for Tracepoints, making them easy to deploy. In Eclipse, you can toggle them with?Alt+t. The Tracepoints are generated to?System.out.println?the name of the method they are placed in, but this can be configured.

No alt text provided for this image

You can think of Tracepoints as some sort of ephemeral log statements, with a lifetime limited to the debugging session.

It’s worth checking out the details in the breakpoint properties window because there are multiple ways to fine-tune breakpoint conditions, for example:

  • you can pause at breakpoints only after another specific breakpoint was reached
  • you can automatically disable a breakpoint after it’s been hit for?n?times


Event-based breakpoints

Certain breakpoints are tied to events, rather than deployed to a specific line of code. For example, you can pause the execution when an exception is thrown anywhere in the application with?Exception Breakpoints. In the movies example, it can be handy if the?geometricMeanOfRatingsmethod starts to throw?NullPointerExceptions, and you are keen to find the Movie object for which the?getRating?returns null instead of a real value.

No alt text provided for this image

Examine the application when it’s stopped

A breakpoint can stop the current thread or the whole virtual machine depending on its configuration. Usually, this isn’t something that you should keep an eye on, but it can make a difference in multithreaded environments, such as web applications. Or, when you remote-debug an application used by other developers. In case of Java, by default Eclipse suspends the thread that hit the breakpoint while IntelliJ IDEA suspends the whole JVM.

The values of variables and fields from the current and parent scopes when the program is paused are accessible. Usually, there is a dedicated Variables view that summarizes them, but you can check the value of each one by simply hovering over it with the mouse.

You can see them change if you continue the execution step-by-step, one line at a time.

No alt text provided for this image

Or, you can continue the whole program, to run until it hits the next breakpoint when you can inspect the variables again. I do this quite often when I need to investigate values on two distinct parts of the execution. It’s well worth to memorize the keyboard shortcuts for the most commonly used debugging actions, so you can pay attention to the code you debug rather than the quirks of your IDE.


String representation and internal structure

It’s important to note, that generally the values are rendered by the object’s?toString?method, so it’s always a good practice to define them carefully.

If you use IntelliJ (or Chrome if you debug Javascript) you can see the values of the variables printed for each line as comments. While it does not print a lot of potentially interesting data, usually it’s more than enough to see the result of an assignment.

No alt text provided for this image

Besides the?toString?version of the object, you can also check it’s internal structure. This is (by definition) tightly coupled with internal details of the implementation, so it’s not always the most convenient way to get information. For example, the next image illustrates how the data in a?HashMap?looks like:

No alt text provided for this image

Luckily, there is a neat feature in Eclipse called?Show logical structure?that allows to view objects in another, more meaningful structure. It can be configured manually, but it also comes with handy defaults for commonly used data types. Let’s see how the?HashMap?is doing with?show logical structure?enabled:

No alt text provided for this image

The difference in information density is pretty clear. If you find yourself clicking too much in the variable view looking for something in the nested structures, consider defining a saner logical structure presentation for them.


Changing the values of variables and fields

IDEs allow not only to inspect but to change variables and fields on the fly. This can be handy for a number of reasons. Imagine you’ve posted a web form, and now inspecting the processing logic on the server application. A few if-statements deep in the code you realize that you forgot to set a field to the desired value. Now a different code path is going to be executed. In this case, changing the value of the field while debugging can save you some time.

No alt text provided for this image

On top of that, you can execute arbitrary code at any given point in time. This is really cool because you can query APIs and experiment with the results. If the method you experiment with has no side effects, then it’s even better, as you can try it as many times as you’d like, and it does not corrupt the following executions. I really like how easy it is to debug functional style code.

You can run these experiments, in multiple ways, depending on your needs.

  • The simplest thing you can do is simply select the piece of code you’d like to run, right click, then?Inspect. It will display the result of the evaluation in a popup.
  • If you try to run a piece of code very often, you are better off with watch expressions. This is a persistent list of expressions that are evaluated whenever the debugger stops the program.
  • If you need to draft some code and run some part of it, there are sketch boards in most IDEs. For example, in Eclipse the?Display view?serves this purpose.


Step filters

While stepping through the codebase I often find myself wandering in the bowels of frameworks and libraries that my application depends on. (This is quite frequent if I keep pressing?Step into.) I am usually not that interested in their internals - first I want to debug the code that I wrote.

Luckily there is an option to use?Step Filtering?in Eclipse. (I did not find a similar feature in IntelliJ for Java.) With this feature, one can configure which packages to skip when stepping through the code. The list of packages can be configured in the?Java > Debug > Step filtering?menu. The filtering behavior can be toggled with the?Use Step Filters?(Shift+F5) button.


Drop to frame

Another interesting feature I’ve found both in Eclipse and IntelliJ is?Drop to frame. This command allows re-entering the selected stack frame. It essentially enables to replay every instruction from a given point. This can be helpful to go back to the start of a method if you missed something you’d like to see.

No alt text provided for this image

This is not time travelling in the sense that the previous changes to the global state will not disappear. So keep in mind that the execution might take a different path on the second try.

As you can see, debugging is a very flexible tool, there are many ways to stop and inspect an application. In the next sections, I try to collect some ideas about when it’s a good idea to use it.


Pro

Debugging is the Swiss Army knife of a developer. Breakpoints are excellent tools to query internal state or to call APIs at a given point of the code. With dynamic code evaluation, a method or function can be easily called with the relevant input data to investigate its results.

You don’t have to restart the application to toggle breakpoints or to do new kinds of investigations, you can also change your debugging strategy while in a middle of a debugging session.

Also, if you don’t exactly know what you are looking for, you can easily inspect all variables and methods available from the given line of code. It’s really useful when working with new code or large codebases.

Tracepoints provide the benefits of logging, but it?does not require to restart the application after adding a log statement. Toggling tracepoints in your IDE is all you need to adjust what is visible on the console. This also means that?you don’t have to clutter your code with print statements?using this technique.


Con

Typically it’s most feasible to use breakpoints in?local debugging sessions only, and it requires?manual intervention?to interrogate the system under debugging to get the relevant data each time the program stops at a breakpoint. This two makes really hard to catch sporadic events happening over a long time.

Because the system has to be manually poked each time, it does not scale well. If you debug something today, then tomorrow you can’t depend on this knowledge as the system might behave completely differently. If you have to modify a complex application with a lot of desired properties and internal invariants, you might quickly get overwhelmed by the many little details you have to keep an eye on every time you modify something. In this case writing some tests might be a better alternative, as they can be executed repeatedly, without much effort, and they are much better at locking down desired behaviors of the software.

Debugging is a flexible tool. Because you can debug pretty much everything,?relying excessively on this technique does not reward carefully planned design. It’s so powerful that it allows to succeed with very bad designs for a while, but it?limits the ability to involve new developers. In the end, it might result in harder to maintain systems that only those can touch effectively who knows all their internal details.

I am not saying debugging is evil, but don’t overuse it. Try to split the complex logic into isolated units, write unit tests for them, and avoid global state if you can.

Robert C. Martin shares similar views about debuggers. The following quote is from his post?Debuggers are a wasteful Timesink?from 2003:

I consider debuggers to be a drug – an addiction. Programmers can get into the horrible habit of depending on the debugger instead of on their brain. IMHO a debugger is a tool of last resort. Once you have exhausted every other avenue of diagnosis, and have given very careful thought to just rewriting the offending code,?then?you may need a debugger.

I know this comment is a bit old, but I hear similar arguments often, and I think it has truth in it.


Summary

Debugging is a powerful tool with a lot of capabilities. I think it’s essential for any developer to learn to use it effectively. With the growing number of frameworks and libraries, it’s essential to be able to take a peek under the hood. However, try not to debug too much, and don’t rely solely on the ability that you can step through the whole codebase once you get familiar with it.

Thanks To : Dávid Csákvári

Yeah this is really educational, if you are equipped and know how to read it !

要查看或添加评论,请登录

社区洞察

其他会员也浏览了