Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

How to Write Code to Solve a Problem, A Beginner's Guide

5.00/5 (35 votes)
6 Oct 2020CPOL7 min read 63.2K  
First steps in development: break it, break it again!
By the time you've read this, you will be an UberKoderz, just like me! No. No, you won't. But ... you will have one of the really important tools available to you to start writing code: Breaking it into smaller bits. And no, I'm not an UberKoderz either.

Introduction

All the code here is in C# - but it's all pretty simple stuff, and the process is the same in any language. Just "bleep" over the brackets and semicolons if you don't understand them!

Recently, there was a question which involved a chunk of homework: write a method that reads a file and returns all the lines which are followed by a line containing three asterisks. And the code was terrible, even for a beginner - so bad I won't embarrass anyone by linking to it.

Quote:

Good day everyone, I don't know if there is an option after the return method to continue or if I should otherwise construct a code. I would like to after the return I continue to the end of the txt file and let the return list more values.

C#
public static string FindLineAboveAsterisks(TextReader reader)
        {
            StringBuilder sbBuilder = new StringBuilder();
            string result = reader.ReadLine();
            string line = String.Empty;          

            while  (result is object && (line = reader.ReadLine()) is object)
        {
                int startIndex = 21;
                int length = 9;

            if (line.Contains("***"))
            {
                sbBuilder.AppendLine(result);
                return result;
                    
                }
                {
                    result = line.Substring(startIndex, length);
                }
                
            }
            return string.Empty;

You look at that code and you just start to wonder ... Why? What is that indentation? What is that there for? Why do this? How you do expect that to work?

And of course, it doesn't. It can't work - and the reason why is the author just threw it together without thinking about the task at all.

My Answer (Expanded a Little)

That looks like it was thrown together without any thought about what you are trying to do!

Blunt, I know. But I wanted to get his attention.

Throw it away, and think about your task: Read a file, find all the lines which are above lines with an asterisk, and return them.

Still blunt, but let's think about it.

So let's take it from the top: You need to return more than one line - so the obvious thing to do is to return a collection of strings instead of a single string. Because although you can return them as a single string, it makes life a lot harder for the code that calls your method - it has to "break it back up again" in order to use the information.
Let's change that:

C#
public static List<string> FindLineAboveAsterisks(TextReader reader)

Now, it returns a collection of strings so the outside world can work with it.

Think about what you are trying to get the method to do: don't complicate things for the calling code - because you are going to call it one or more times, and you will write it once. If you make the outside world work harder, then you are just adding work you have to do every time you use the method.
So if you need a collection of items, return a collection - don't bodge round so that the outside world has to do more processing each time it calls you!

But ... why are you passing it a TextReader? That means each time you call it, the outside world has to do the work of creating, opening, passing, and closing the reader - which is silly. Pass the path instead, and let the method do what it wants with it:

C#
public static List<string> FindLineAboveAsterisks(string filePath)

Now, the caller looks easier to work with.

Again, make life easier for yourself: you want to read a file? Pass the path and let the method decide what to do with it. If you pass a TextReader, or a Stream, then you are limiting what the outside world can do, and forcing a "shape" on code that may not be the easiest or most efficient for the job it has to do.
The more "generic" you make parameters, then more flexible your code can be - and that means it can be reused - which saves you writing another, similar method to do much the same thing.

Let's start filling in the method: We need a List to return, and to process every line in a file. If we want to use every line, then let's just get them all and let the system handle it! That's pretty easy:

C#
public static List<string> FindLineAboveAsterisks(string filePath)
    {
    List<string> lines = new List<string>();
    foreach (string line in File.ReadLines(filePath))
        {
        // ...
        }
    return lines;
    }
What could be simpler? We know there are two things we must do: return a collection of lines, and process all the lines in a file. So create the collection at the top of the method; return it at the end. Add a simple loop to give us each line at a time. Result: the code is simple, and easy to write. And if it's easy to write, it's probably going to work ...

Now, what do we have to do with the lines?
Simple; we need to collect all the lines where the next line contains three asterisks.
So we need to know what the last line was.

Think about it for a moment: inside the loop, how can we tell what the next line contains? Practically speaking, we can't (unless we complicate the code and use a different looping construct, but that's messy). What we do know though is what the previous line was - because we have already processed it and can keep a copy for next time.
So turn the problem on its head and think of it as "find all the lines which contain three asterisks, and return the previous line for each". A moments thinking tells you that gives the same result, and means we can work with "historical data" which we have already looked at instead of "future data" that we haven't.

Let's add that:

C#
public static List<string> FindLineAboveAsterisks(string filePath)
    {
    List<string> lines = new List<string>();
    string lastLine = "";
    foreach (string line in File.ReadLines(filePath))
        {
        // ...
        lastLine = line;
        }
    return lines;
    }
Each time, we are adding a tiny amount of simple code - nothing complicated, so there is less to go wrong.

We need to check if the current line contains "***". If it does, add the last one to the collection. That's easy too - a quick if test will do it:

C#
public static List<string> FindLineAboveAsterisks(string filePath)
    {
    List<string> lines = new List<string>();
    string lastLine = "";
    foreach (string line in File.ReadLines(filePath))
        {
        if (line.Contains("***"))
            {
            lines.Add(lastLine);
            }
        lastLine = line;
        }
    return lines;
    }

Hang on ... it's finished, isn't it?

All we have to do now is call it and test it:

C#
string path = @"D:\Test Data\List of hats.txt";
foreach (string line in FindLineAboveAsterisks(path))
    {
    Console.WriteLine(line);
    }
I could have shown you the original code for that ... but you might have just finished eating ...

Oh look - it works!

So What Did We Do?

Basically, all we did was take a whole task and break it into littler ones:

Quote:

Write a method that reads a file and returns all the lines which are followed by a line containing three asterisks.

  1. Decide what it needs to return
  2. Decide what it needs as parameters
  3. Create the returnable value, and set us up to return it.
  4. Add a loop to look at each line.
  5. Save the current line for next time at the end of the loop, when we are finished looking at it.
  6. Check if the line has asterisks.
  7. If so, add the line we saved last time round the loop to our output collection.

None of those tasks are difficult: they are a line or two of code and it's pretty simple code as well.

And that's the secret: big tasks are made up of smaller ones, and those are made of even smaller ones.

You are used to that: you use it every day!

Task: "Have breakfast."

Smaller tasks:

  1. Go to kitchen.
  2. Decide what to eat for breakfast.
  3. Prepare it.
  4. Eat it.
  5. Wash up after yourself.

Each of those tasks may be quite complicated:

Subtask: "Go to kitchen"

  1. Work out where you are.
  2. Work out how to get from here to the kitchen.
  3. Move there.

Those may have sub-sub-sub tasks:

Sub-sub-task: "Work out where you are"

  1. Wake up.
  2. Open eyes.
  3. Look around: where am I? Do I recognise this room? What the hell did I do last night?
  4. ...

Summary

The point is that every task can be broken down into smaller pieces until you reach a task you either can do, or know how to find out how to do. If you wake up in a strange room, then you need to check for other people, and maybe ask them where the kitchen is - and so on.

Software tasks are the same; refine the task into smaller bits and some - probably all - can be done easily, and build up to completing bigger, more complex tasks that sound impossible.

Just start by thinking instead of jumping into code: five minutes planning can save you hours of work!

History

  • 6th October, 2020: Original version
  • 6th October, 2020: Typos. Always typos ...
  • 8th October, 2020: A couple of less than and greater than symbols were fixed: they were showing as the HTML equivalent: "&lt;" and "&gt;"

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)