Contents

Design Patterns in Software Development: A Comprehensive Review with C and Go Examples

Design Patterns in Software Development: A Comprehensive Review with C and Go Examples

Introduction

  • Purpose of This Article: This article is not intended to promote design patterns as the ultimate solution. Instead, it aims to explain the challenges and limitations of design patterns, particularly in the context of C and Go. We’ll explore why many traditional patterns might be unnecessary or even counterproductive in these languages.
  • Definition and Context: Design patterns are reusable solution templates for common problems encountered in software development. However, their value and applicability vary significantly across different programming languages and paradigms.
  • History and Gang of Four (GoF): Design patterns gained popularity with the 1994 book “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, known as the “Gang of Four.” While this book was groundbreaking for object-oriented languages like Java, its patterns don’t always translate well to other languages.
  • Why Discuss Design Patterns? Understanding design patterns is important, but so is knowing when not to use them. In C and Go, many problems that design patterns solve in object-oriented languages can be addressed more elegantly using the languages’ native features.

Alternative Approaches to Design Patterns in Go

Go language offers a different approach to classical design patterns. Go’s design philosophy is based on these principles:

  1. Composition over Inheritance:

    • Go prefers composition over inheritance
    • Interfaces are implemented implicitly
    • This approach eliminates the need for many classical design patterns
  2. Concurrency First:

    • Go’s concurrency model (goroutines and channels) offers alternatives to many design patterns
    • For example, channels can be used instead of the Observer pattern
    • Package-level variables and sync.Once can be used instead of Singleton
  3. Simple is Better:

    • Go prefers simple and clear solutions over complex design patterns
    • In many cases, classical design patterns are considered “anti-patterns” in Go
  4. Idiomatic Go:

    • The correct approach in Go is to use the language’s own features and best practices
    • For example, use package-level variables and sync.Once instead of Singleton pattern
    • Use channels instead of Observer pattern
    • Use constructor functions instead of Factory pattern

This approach, in line with Go’s “less is more” philosophy, makes the code simpler, more understandable, and maintainable.

Categories of Design Patterns

  1. Creational Patterns: Manage object creation processes. Examples: Singleton, Factory Method, Builder.
  2. Structural Patterns: Organize the structure and relationships of objects. Examples: Adapter, Decorator, Composite.
  3. Behavioral Patterns: Manage communication and responsibilities between objects. Examples: Observer, Strategy, Command.

Creational Patterns

  • Singleton Pattern: Ensures a class has only one instance and provides a global access point to it.
  • Factory Method Pattern: Delegates object creation to subclasses, allowing them to decide which class to instantiate.
  • Builder Pattern: Manages the step-by-step construction of complex objects.

Structural Patterns

  • Adapter Pattern: Converts one interface into another, allowing incompatible interfaces to work together.
  • Decorator Pattern: Dynamically adds new responsibilities to objects.
  • Composite Pattern: Organizes objects into a tree structure.

Behavioral Patterns

  • Observer Pattern: Automatically notifies other objects when a change occurs in one object.
  • Strategy Pattern: Defines a family of algorithms and encapsulates each one.
  • Command Pattern: Encapsulates a request as an object.

Advantages and Disadvantages of Design Patterns

  • Advantages: Enhance code readability, maintainability, and reusability.
  • Disadvantages: Can create complex structures and have a steep learning curve.

Design Patterns in Modern Software Development

  • Patterns in Microservice Architecture: Design patterns are used to manage communication and responsibilities between services in microservice architecture.
  • Patterns in Functional Programming: In functional programming, design patterns manage relationships and responsibilities between functions.

Criticisms and Limitations

  • Overuse and Misapplication: Design patterns can lead to unnecessary complexity when overused.
  • Learning Curve: Understanding and applying design patterns requires significant experience and knowledge.
  • Modern Alternatives: Some modern programming paradigms offer alternatives to traditional design patterns.

Real-World Applications

  • Case Studies: Examples of design patterns in popular software projects.
  • Performance Evaluations: The impact of design patterns on performance and memory usage.
  • Best Practices: Guidelines on when and how to effectively use design patterns.

1. Singleton Pattern

Purpose: Ensures a class has only one instance and provides a global access point to it.

Singleton in C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

typedef struct {
    int data;
} Singleton;

Singleton* getInstance() {
    static Singleton instance;
    return &instance;
}

int main() {
    Singleton* s1 = getInstance();
    s1->data = 42;
    Singleton* s2 = getInstance();
    printf("%d\n", s2->data); // 42
    return 0;
}

Singleton in Go (Idiomatic Approach)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
import (
    "fmt"
    "sync"
)

type singleton struct {
    data int
}

var (
    instance *singleton
    once     sync.Once
)

func getInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

func main() {
    s1 := getInstance()
    s1.data = 42
    s2 := getInstance()
    fmt.Println(s2.data) // 42
}

2. Factory Method Pattern

Purpose: Delegates object creation to subclasses, allowing them to decide which class to instantiate.

Factory Method in C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    void (*speak)();
} Animal;

void dogSpeak() { printf("Woof!\n"); }
void catSpeak() { printf("Meow!\n"); }

Animal* createAnimal(const char* type) {
    Animal* a = malloc(sizeof(Animal));
    if (strcmp(type, "dog") == 0) a->speak = dogSpeak;
    else a->speak = catSpeak;
    return a;
}

int main() {
    Animal* dog = createAnimal("dog");
    dog->speak();
    Animal* cat = createAnimal("cat");
    cat->speak();
    free(dog); free(cat);
    return 0;
}

Factory Method in Go (Idiomatic Approach)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
import "fmt"

type Animal interface {
    Speak()
}

type Dog struct{}
func (d Dog) Speak() { fmt.Println("Woof!") }

type Cat struct{}
func (c Cat) Speak() { fmt.Println("Meow!") }

// Constructor function - Go's idiomatic approach
func NewAnimal(kind string) Animal {
    switch kind {
    case "dog":
        return Dog{}
    case "cat":
        return Cat{}
    default:
        return nil
    }
}

func main() {
    dog := NewAnimal("dog")
    dog.Speak()
    cat := NewAnimal("cat")
    cat.Speak()
}

3. Observer Pattern

Purpose: Automatically notifies other objects when a change occurs in one object.

Observer in C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#define MAX_OBSERVERS 10

typedef void (*ObserverFunc)(int);

ObserverFunc observers[MAX_OBSERVERS];
int observer_count = 0;

void attach(ObserverFunc obs) {
    observers[observer_count++] = obs;
}

void notify(int data) {
    for (int i = 0; i < observer_count; ++i) {
        observers[i](data);
    }
}

void observer1(int data) { printf("Observer1: %d\n", data); }
void observer2(int data) { printf("Observer2: %d\n", data); }

int main() {
    attach(observer1);
    attach(observer2);
    notify(42);
    return 0;
}

Observer in Go (Idiomatic Approach)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import "fmt"

// Using channels instead of Observer pattern in Go
func main() {
    // Create channel
    updates := make(chan int)
    
    // Observer 1
    go func() {
        for update := range updates {
            fmt.Printf("Observer1: %d\n", update)
        }
    }()
    
    // Observer 2
    go func() {
        for update := range updates {
            fmt.Printf("Observer2: %d\n", update)
        }
    }()
    
    // Notify change
    updates <- 42
    close(updates)
}

4. Adapter Pattern

Purpose: Converts one interface into another, allowing incompatible interfaces to work together.

Adapter in C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

typedef struct {
    void (*request)();
} Target;

void specificRequest() { printf("Specific request!\n"); }

void adapterRequest() { specificRequest(); }

int main() {
    Target t;
    t.request = adapterRequest;
    t.request();
    return 0;
}

Adapter in Go (Idiomatic Approach)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import "fmt"

// Target interface
type Target interface {
    Request()
}

// Adaptee
type Adaptee struct{}
func (a Adaptee) SpecificRequest() { fmt.Println("Specific request!") }

// Adapter
type Adapter struct {
    adaptee Adaptee
}

// Interface implementation
func (a Adapter) Request() {
    a.adaptee.SpecificRequest()
}

func main() {
    adapter := Adapter{Adaptee{}}
    adapter.Request()
}

Conclusion

Design patterns provide effective and reusable solutions to common problems in software development. However, each programming language has its own features and best practices. In modern languages like Go, it may be more appropriate to use the language’s own features instead of classical design patterns. In this article, we examined both classical design patterns and Go’s idiomatic approaches.


Resources