,

Patterns over Languages

In today’s world, there seems to be an intense focus on the programming language used for development. Some individuals even take a militant stance about their language of choice. While there are valid reasons why certain languages shine in specific contexts—such as the ecosystems and infrastructure built around them—I believe we often place too much emphasis on the language itself.

During my holiday, I implemented the Process API in C#, Java, and even experimented with Rust. By now, I’m deeply familiar with the core concepts of the Process API, so it wasn’t about figuring out what to build, but rather how to build it in each language. Through this process, I came to an important realization: it’s not about the language; it’s about the design patterns.

Many software challenges have already been addressed through well-defined patterns. The programming language is simply a tool to express what needs to be done, but a solid understanding of design patterns is what truly helps in solving architectural problems.

Take a pattern like Separation of Concerns—it transcends languages and is essential for every project. Similarly, lookup tables are a universal approach to improving performance when working with well-defined IDs or keys. While you might need to do some research when dealing with nuances like multithreading, the core principles remain the same. If you understand fundamental patterns, why they are used, and the benefits they bring, applying them becomes second nature—no matter the language.

This insight has inspired me to ensure that, over the next year, my junior developers become well-versed in design patterns.

Here’s a list of patterns to consider:

Core Design Principles

  1. Separation of Concerns (SoC)
    • Keep different concerns (or responsibilities) in separate modules or components to improve maintainability and readability.
  2. Single Responsibility Principle (SRP)
    • Every module, class, or function should have only one responsibility or reason to change.
  3. Open/Closed Principle (OCP)
    • Software entities should be open for extension but closed for modification.
  4. Dependency Inversion Principle (DIP)
    • Depend on abstractions rather than concrete implementations.
  5. Interface Segregation Principle (ISP)
    • Design interfaces so that clients are not forced to implement methods they don’t use.
  6. Don’t Repeat Yourself (DRY)
    • Avoid duplicating code by abstracting common functionality into reusable components.
  7. Keep It Simple, Stupid (KISS)
    • Strive for simplicity in design; avoid unnecessary complexity.
  8. You Aren’t Gonna Need It (YAGNI)
    • Don’t add functionality until it is necessary.
  9. Least Privilege (Principle of Least Authority)
    • Components should only have access to the information and resources they need to perform their role.
  10. Law of Demeter (LoD)
    • A module should only communicate with its immediate collaborators and not with indirect dependencies.

Architectural Patterns

  1. Model-View-Controller (MVC)
    • Separates the application into three interconnected components: Model (data), View (UI), and Controller (business logic).
  2. Model-View-ViewModel (MVVM)
    • Extends MVC with a “ViewModel” to facilitate two-way data binding between the UI and business logic.
  3. Event-Driven Architecture
    • Components communicate via events, enabling loose coupling and asynchronous workflows.
  4. Microservices Architecture
    • Break down applications into smaller, independent services that can be developed, deployed, and scaled independently.
  5. Layered Architecture (n-tier)
    • Separate applications into layers (e.g., presentation, business logic, and data access) to enforce separation of concerns.
  6. Entity-Component-System (ECS)
    • Decomposes functionality into entities, components, and systems, often used but not limited too game and simulation development.

Behavioral Design Patterns

  1. Observer Pattern
    • Define a one-to-many dependency where changes in one object are automatically notified to its dependents.
  2. Strategy Pattern
    • Define a family of algorithms, encapsulate each one, and make them interchangeable.
  3. State Pattern
    • Allow an object to change its behavior when its internal state changes.
  4. Command Pattern
    • Encapsulate requests as objects, allowing for parameterization and queuing of requests.
  5. Mediator Pattern
    • Use a mediator object to centralize communication between multiple components.
  6. Chain of Responsibility
    • Pass requests along a chain of handlers until one handles the request.

Structural Design Patterns

  1. Adapter Pattern
    • Allow incompatible interfaces to work together by wrapping one interface with another.
  2. Composite Pattern
    • Compose objects into tree structures to represent part-whole hierarchies.
  3. Decorator Pattern
    • Dynamically add new behavior to objects without modifying their structure.
  4. Facade Pattern
    • Provide a simplified interface to a larger body of code.
  5. Proxy Pattern
    • Provide a placeholder or surrogate for another object to control access to it.
  6. Flyweight Pattern
    • Minimize memory usage by sharing as much data as possible between similar objects.

Creational Design Patterns

  1. Factory Method
    • Define an interface for creating an object, but let subclasses alter the type of object created.
  2. Abstract Factory
    • Provide an interface for creating families of related objects without specifying their concrete classes.
  3. Builder Pattern
    • Construct complex objects step by step, separating construction logic from the representation.
  4. Singleton Pattern
    • Ensure a class has only one instance and provide a global point of access to it.
  5. Prototype Pattern
    • Create new objects by copying existing objects (cloning).

Data and Algorithm Patterns

  1. Repository Pattern
    • Encapsulate data access logic to keep it separate from business logic.
  2. Unit of Work
    • Aggregate multiple operations into a single transaction.
  3. Specification Pattern
    • Encapsulate business rules as reusable objects to check if certain criteria are met.
  4. Pipeline Pattern
    • Process data through a sequence of processing stages (or filters).
  5. CQRS (Command Query Responsibility Segregation)
    • Separate read and write operations to optimize performance and scalability.

Testing and Maintenance

  1. Test-Driven Development (TDD)
    • Write tests first, then develop code to satisfy the tests.
  2. Behavior-Driven Development (BDD)
    • Write tests in a natural language style that describes the behavior of the application.
  3. Mocking and Stubbing
    • Replace real objects with mocks/stubs during testing to isolate code behavior.
  4. Continuous Refactoring
    • Regularly improve the internal structure of code without changing its external behavior.

Concurrency Patterns

  1. Producer-Consumer
    • Use a queue to mediate between producers and consumers.
  2. Reader-Writer Lock
    • Allow multiple readers but restrict access to a single writer at a time.
  3. Thread Pool
    • Use a pool of threads to manage concurrent execution and improve performance.
  4. Async/Await
    • Write asynchronous code that looks synchronous for better readability.
  5. Actor Model
    • Treat “actors” as the fundamental units of computation that communicate by passing messages.