Skip to content

Instantly share code, notes, and snippets.

@benj2240
Created April 16, 2021 18:57
Show Gist options
  • Select an option

  • Save benj2240/4d25bc469418d278fd86604de0589bac to your computer and use it in GitHub Desktop.

Select an option

Save benj2240/4d25bc469418d278fd86604de0589bac to your computer and use it in GitHub Desktop.

Don't write reusable code

Disclaimer: If you are the author of a language, or a library with thousands of happy developers using your code to build cool stuff, then this advice does not apply to you. If you are a line-of business programmer, working at a company whose primary product is not its own technology, then it definitely does.

Why not write reusable code?

Why shouldn't you write reusable code? Because you're not good at it. Or, more accurately, it's an insurmountably difficult problem within your domain. The promise of reusable code is this:

Take a little extra effort to write a general solution once, and from then on it will be easier to write solutions for specific problems.

That promise is never delivered on - or if it is, it happens so rarely that it's not gambling that "little" extra effort.

Case study: Command Line

Let's say that I have a specific problem: I need to write a console app that reads IDs from a file, then takes some action for each ID. Like this:

public static void main() {
    var ids = File.ReadAllLines("ids.txt");
    for (var id in ids) {
        Process(id);
    }
}

Now I'm done, right? Well, maybe I want to do a test run with a subset of IDs. That's not something I should bake into the app; it would be better for me to create an input file with that subset, and tell the app which file to read:

public static void main(string[] args) {
    if (args.length < 1) {
        Console.Error.WriteLine("Please provide the name of an ID file");
        return;
    }

    var ids = File.ReadAllLines(args[0]);
    for (var id in ids) {
        Process(id);
    }
}

Perfect, we're all done. Oh, but this app takes a long time to run. Wouldn't it be nice if it printed its progress as it goes? But we don't want to print progress every time we run the app... Let's add a --verbose parameter.

public static void main(string[] args) {
    if (args.length < 1) {
        Console.Error.WriteLine("Please provide the name of an ID file");
        return;
    }

    var verbose = false;
    if (args.length > 1 && args[1] == "--verbose") {
        verbose = true;
    }

    var ids = File.ReadAllLines(args[0]);
    var i = 0;
    for (var id in ids) {
        if (verbose) {
            i += 1;
            Console.WriteLine($"Processing record {i} of {ids.Count}");
        }
        Process(id);
    }
}

Hmm... This will work okay, but what if someone tries to call the app like this?

> .\Processor.exe --verbose MyIds.txt

As a human I can tell what they mean, but my app will try to open a file named "--verbose". Let me fix that...

public static void main(string[] args) {
    var processor = new Processor();
    var validArgs = processor.Parse(args, out var message);
    if (!validArgs) {
        Console.Error.WriteLine(message);
        return;
    }
    processor.Process();
}

public class Processor {
    private string FileName = null;
    private bool VerboseMode = false;

    /// <summary>
    /// Parse the arguments from the command line
    /// </summary>
    /// <param name="message">
    /// A short description of the problems with the arguments, if any
    /// </param>
    /// <returns>
    /// True if the arguments are valid, False otherwise
    /// </returns>
    public bool Parse(string[] args) {
        foreach (var arg in args) {
            if (arg == "--verbose") {
                VerboseMode = true;
            }
            else if (arg.StartsWith("--filename=")) {
                FileName = arg.SubString(11);
            }
            else {
                message = $"Unrecognized argument '{arg}'"
                return false;
            }
        }

        if (string.IsNullOrEmpty(FileName)) {
            message = "Provide the name of an ID file with '--fileName='";
            return false;
        }
        else {
            message = null;
            return true;
        }
    }

    public void Process() {
        var ids = File.ReadAllLines(FileName)
        // ...
    }
}

Okay that will work... but now there's a requirement for the format of the filename argument, that you can only know by trying a wrong format first (or by reading my source code). I should probably provide a "--help" option that describes the program's requirements.

You can see that this is getting out of hand. At this point I have options:

  1. I could stop where I am. This is a good option if I know this app has limited use, I will be the app's only user, I will be in close contact with all of the app's potential users, all users will have access to the source code (and the expertise to make sense of it), or some combination of these.

  2. I could use an argument-parsing library. CommandLine is a good option for C#. More recently, Microsoft released System.CommandLine. For NodeJS apps, the de facto standard is Yargs.

  3. I could try to Write Reusable Code. What would that look like?

Well, I could write a base class that looks like this:

public abstract class ConsoleApplicationBase {
    public void Run(string[] args) {
        if (!Parse(args)) {
            ShowInstructions();
            return;
        }

        Run();
    }

    private bool Parse(string[] args)
    {
        foreach (string arg in args)
        {
            if (arg == "--help")
                return false;

            if (!Parse(arg))
                return false;
        }
    }

    public abstract void Run();

    protected abstract bool Parse(string arg);

    protected abstract void ShowInstructions();
}

And then write a child class that looks like this:

public class Program {
    public static void main(string[] args) {
        var application = new MyApplication();
        application.Run(args);
    }
}

public class MyApplication : ConsoleApplicationBase {
    private string FileName = null;
    private bool Verbose = false;

    public override void Run() {
        var ids = File.ReadAllLines(FileName);
        // ...
    }

    protected override bool Parse(string arg) {
        if (arg == "--verbose") {
            VerboseMode = true;
            return true;
        }
        
        if (arg.StartsWith("--filename=")) {
            FileName = arg.SubString(11);
            return true;
        }

        return false;
    }

    protected override void ShowInstructions() {
        Console.WriteLine("This application processes IDs from a file.");
        Console.WriteLine("It recognizes three arguments: --help, --verbose, and --filename");
        // ...
    }
}

Why have I gone into such painstaking detail in this example? Because I want to drive home this emphatic point: OPTION 3 IS THE WORST. It's the worst by FAR. The version of my application which uses ConsoleApplicationBase is objectively more complicated than any of the previous versions. Control jumps back and forth between the base class and the child class, with the abstracts and overrides.

Ah, but at least this will make the next console app easier, right? Wrong. Maybe, if I write a console app next week, I'll remember how to implement the child class properly. Next month? Doubtful. Next quarter, next year? No way.

What's more: I am not the only developer at my company. Will my coworker save time by inheriting from my ConsoleApplicationBase? Not a chance! I wrote it in the way that made sense to me at the time - and if you asked 7 other developers to accomplish the task, you'd see 7 wildly different approaches. My coworker's best bet is to read the source code of my base class before they BEGIN to write their app. I would give even odds that letting them write the dumb "put everything in main()" version would be faster. And often, it will suffice!

Ah, but the general solution can be extended, giving functionality to the child classes for free! Worth it, right? Wrong.

  • Could we add a --throttle flag to the base class?
    • No; the child class would have to actually implement the delay.
  • Could we add a progress bar or timestamp logging to the base class?
    • No; it could interfere with output from the child class.
  • Could we add logic for parsing common arguments formats to the base class?
    • No; nothing we could add to the base class would be easier than the child class simply calling int.Parse or DateTime.Parse.

No matter which way you try to stretch ConsoleApplicationBase, you also add requirements or restrictions to the child classes. It is always simpler to use existing libraries, or just built-in C# features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment