Skip to content

Problem Solving

Dandielo edited this page Dec 10, 2021 · 1 revision

Looking around the world wide web, we can find lots of things, however I'm still missing a good guide on how to tackle problem solving.

I'm not aiming to create the best guide but want to explain what seems to be the best approach working for me.

Introduction

To start this topic, lets define what can be a problem, and the easy answer is: everything.

Starting with things like doing groceries or getting a coffee, ending on designing IK animation system, etc.

Of course, this guide is not here, to solve your coffee addiction, but to help with addressing problems in your projects. This best way to solve anything is to have a proper understanding of it in the first place.

First: Categorize your problem

To solve a problem successfully and quickly we need first to know what we are dealing with:

  • Is this a feature or bug?
  • Is it clear what the result should be?
  • Do we know why the problem appeared?
  • and so on...

Unless you know what you need to solve, how can you try to solve it? If you get a crash because a nullptr was dereferenced, the "obvious" way is to check if it's != nullptr, but is it? Why did we get a nullptr value in the first place where the dereferencing happens?

  • Maybe we did run out of available memory from a predefined pool?
  • Maybe there was a typo in a file name, and the FS returned no file found?
  • Maybe the documentation is missing, outdated or not clear enough?
  • Did we allow the user to misuse the API and he was able to just explicitly provide a nullptr?

Example: API allows to provide a pointer but never accepts nullptr's

Let's say the user was allowed to pass a nullptr so this is generally an API misuse that we can fix requiring the user to provide a reference instead of a pointer. Now if we get a nullptr dereference, the user was doing naught things.

Of course, we could just add a check and introduce a workaround or special case for nullptr values, but now we probably made a simple function overly complicated.

If we want to allow a nullptr case, then we can implement it, but it should not be the 'goto' solution.

Second: Decide and plan how you want to address the issue

Once we know what the problem is we need to pick a way to solve it:

  • Update the documentation?
  • Update the API making it impossible for the user to misuse it?
  • Fix the bug in the code?
  • Make a grocery list before going shopping?
  • or any other idea...

Because possibilities are endless, it's up to the project contributors to pick the proper solution. However, the picked solution should always improve the project, and not introduce any new bigger problems. A few things to keep in mind:

  • We prevent the problem from re-appearing with the provided solution.
  • We addressed the problem and can agree on the results.
  • It does not introduce new issues.
    • ex.: We don't make private members public and say a feature was added without proper checks in place.
    • ex2.: Another user is not be negatively affected by the solution, by just updating to a newer version.
  • It does not result in new implicit dependencies.
    • ex.: Added implicit use of a File System in the command line API, to handle response files. What if we don't have one in our system?

This is not easy, but the more you try to provide the best solution the less problems will you encounter in the future. Sometimes it's also good to have a list of pros and cons before making a final pick.

You should also not limit yourself to picking a single 'way' of solving a problem:

  • Introducing a new API consider using multiple design approaches to achieve a good solution.
    • ex.: Data Oriented Design + Object Oriented Programming can go together a long way
  • Misusing an API can be done by changing it and updating documentation.

Third: Try to define some constraints

It helps a lot when you also thing about things that you don't want to be part of the solution:

  • When making a grocery list I don't want to use paper as I do have a smartphone.
  • Changes to the API don't introduce any new dependencies.
    • ex.: instead of using a FileSystem in CommandLine system maybe we can provide the contents of the response-file directly?
  • The updated documentation doesn't describe special corner cases.
  • Existing valid code is working properly after the change.

Fourth: Don't allow the user to make mistakes

Always try to make your solution complete.

  • If an API takes a pointer, allow it to fail when a nullptr value is provided.
  • If an object requires complex setup, create a factory instead of making it multistate.

For example, it's much easier to spot an invalid use if a factory returns a nullptr instead when one of multiple initialization steps of an object fail.

The EngineRunner object, the factory will return either a fully functional object you are allowed to use right away or returns a nullptr when something goes wrong. In this case the user is not able to misuse the runner. If it exists it's ready to be used.

Really bad approaches are multistage initialization objects. This is quite common, where types have interfaces containing multiple Init, Load, Setup methods that when not called keep the object in an invalid or undefined state.

Even worse when you provide dependencies in those methods, you now need to keep track of them in the object.

A solved problem should make the use case less error prone and/or easier to use. The less documentation is required to explain an API the better.

Finally: Look for opinions and ideas.

You are not all knowing, mistakes are natural and cannot be avoided. Try to always discuss your ideas and solutions with others, you might find out that there is a solution that no-one has ever thought of.

Summary

**Remember to "solve the problem" and not just hide the symptoms.