DEV Community

Navicstein Rotciv
Navicstein Rotciv

Posted on

What is defensive programming?

What is defensive programming?

Defensive programming is a form of defensive design intended to develop programs that are capable of detect potential security abnormalities and make predetermined response - Wikipedia

in other words, defensive programming is writing code to handle cases that you do not think will, or even can happen, because you have a belief that your own beliefs are unreliable or assuming the worst case: that your users are complete crazy people and you must defend yourself and your program from their crazy inputs

for example, lets take a function that add two numbers and yields an output

func add(a, b int) (int, error) {
    if a == 0 || b == 0 {
        // gracefully return and error
        return 0, errors.New("can't add zero values")
    }

    // finally compute the function
    return a + b, nil
}

func main() {
    result, err := add(10, 30)
    if err != nil {
        log.Fatalln(err)
    }
    log.Println(result)
}

Enter fullscreen mode Exit fullscreen mode

Here, errors are checked before the resulting operation is done

You can fight logical errors with unit tests that cover all code paths using code coverage tools for example, you can write a test to guard against zero values

package main

import "testing"

func TestAdd(t *testing.T) {
    output, err := add(0, 1)

    if err != nil {
        t.Fatalf(`TestAdd = %d, error -> %s`, output, err)
    }
}

Enter fullscreen mode Exit fullscreen mode

Which fails

Running tool: /opt/go/bin/go test -timeout 30s -run ^TestAdd$ github.com/navicstein/def

--- FAIL: TestAdd (0.00s)
    /home/navicstein/Idea/def/main_test.go:9: TestAdd = 0, error -> can't add zero values
FAIL
FAIL    github.com/navicstein/def   0.002s
FAIL
Enter fullscreen mode Exit fullscreen mode

In a static language like C or Go, the compiler would perform some of the sanity checks for us, for example not allowing to pass string where a number is expected.

There are three sources of errors to guard against

  1. Logical errors in the code itself.
  2. Errors in the input data, especially user supplied input.
  3. Errors in the environment

let's look at another real world example, in this example we're going to assume we'll actually convert any video to an .mp4 video file

package main

import (
    "errors"
    "log"
    "path/filepath"
    "strings"
    "time"

    "golang.org/x/exp/slices"
)

type VideoParams struct {
    Url          string
    AudioBitrate string
    VideoBitrate string
    VideoCodec   string
    AudioCodec   string
}

// ConvertAnyVideoToMp4 converts any video to mp4
func ConvertAnyVideoToMp4(args VideoParams) error {
    if args.Url == "" {
        return errors.New("an input URL is required, please provide one")
    }

    var (
        fileExt = filepath.Ext(args.Url)
        exts    = []string{".flv", ".avi"}
    )

    // don't convert video is already an mp4 file
    if strings.ToLower(fileExt) == ".mp4" {
        return errors.New("video is alread an mp4 file, nothing to do")
    }

    // validate against supported files
    if !slices.Contains(exts, fileExt) {
        return errors.New("unsupported video file")
    }

    // Here, we're just supplying defaults as we can tolerate the nil arguments
    if args.AudioBitrate == "" {
        args.AudioBitrate = "8k"
    }

    if args.VideoBitrate == "" {
        args.VideoBitrate = "30k"
    }

    if args.AudioCodec == "" {
        args.AudioCodec = "mp3"
    }

    if args.VideoCodec == "" {
        args.VideoCodec = "4264"
    }

    doneCh := make(chan bool, 1)

    func() {
        log.Printf("Converting with: %#v", args)
        time.AfterFunc(time.Second*5, func() {
            doneCh <- true
        })
    }()

    if <-doneCh {
        log.Println("Finished converting video to mp4")
    }

    return nil
}

Enter fullscreen mode Exit fullscreen mode

And in our usage file

    args := VideoParams{
        Url: "https://sample-videos.com/video123/flv/720/big_buck_bunny_720p_1mb.flv",
    }

    if err := ConvertAnyVideoToMp4(args); err != nil {
        log.Fatalln(err)
    }
Enter fullscreen mode Exit fullscreen mode

Which we can see that we have default values for attributes that were not specified by the user

❯ go run .
2022/09/11 03:40:12 Converting with: main.VideoParams{Url:"https://sample-videos.com/video123/flv/720/big_buck_bunny_720p_1mb.flv", AudioBitrate:"8k", VideoBitrate:"30k", VideoCodec:"4264", AudioCodec:"mp3"}
2022/09/11 03:40:17 Finished converting video to mp4
Enter fullscreen mode Exit fullscreen mode

in the example above, we're just checking and setting defaults then overriding the defaults with user defined attributes but It's a good practice to write test cases before the actual implementation of a function because it gives you a clear representation of what the function will do

Difference between defensive programming and exception handling

The term “defensive programming” seems to have two complementary meanings. In the first meaning, the
term is used to describe a programming style based on assertions, where you explicitly assert any assumptions
that should hold true as long as the software operates correctly.
In the other meaning, however, “defensive programming” denotes a programming style that aims at making
operations more robust to errors, by accepting a wider range of inputs.

While
Exception handling is the process of responding to unwanted or unexpected events when a computer program runs

Some key concepts to take home

Anything that can go wrong will go wrong - Murphy's Law

  • Error conditions will occur, and your code needs to deal with them (Out of memory, disk full, file missing, file corrupted, network error, etc)
  • Software should be tested to see how it performs under various error conditions
  • Program defensively, i.e., assume that errors are going to arise, and write code to detect them when they do.
  • Put assertions in programs to check their state as they run, and to help readers understand how those programs are supposed to work.
  • Use preconditions to check that the inputs to a function are safe to use.
  • Use post conditions to check that the output from a function is safe to use.
  • Write tests before writing code in order to help determine exactly what that code is supposed to do.

https://faculty.cs.byu.edu/~rodham/cs240/lecture-notes/Lecture-16-ErrorHandling&DefensiveProgramming/Lecture-16-ErrorHandlingAndDefensiveProgramming.pdf

https://www.state-machine.com/doc/Samek0308.pdf

https://glebbahmutov.com/blog/defensive-coding-examples/

Top comments (0)