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)
}
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)
}
}
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
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
- Logical errors in the code itself.
- Errors in the input data, especially user supplied input.
- 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
}
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)
}
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
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.
Top comments (0)