JavaScript Programming Paradigms: A Deep Engineering Perspective

The Engineering Foundation: Understanding Programming Paradigms

 

Programming paradigms represent fundamental approaches to structuring and organizing code. Each paradigm offers distinct advantages in terms of maintainability, testability, and scalability. Understanding these differences enables engineers to make informed decisions about code architecture and design patterns.

Modern JavaScript's flexibility allows seamless integration of multiple paradigms within a single codebase. However, consistency in approach and understanding the implications of each paradigm's state management philosophy are crucial for long-term code health.

This presentation uses a practical form validation scenario to illustrate real-world applications of each paradigm, examining how state is managed, how code is organized, and what trade-offs each approach presents.

Agenda

  • Procedural programming
  • Object-oriented programming
  • Function programing

In this talk, we will explore 3 types of programing styles (The big three)

Not in this talk: AOP, Reactive, Data Oriented, Esoteric, etc

Procedural Programming

Procedural programming organizes code as a sequence of functions that operate on shared data. State is typically managed through global variables or explicitly passed parameters, making data flow transparent but potentially creating tight coupling between components.

Core Characteristics

  • Functions as the primary organizational unit
  • Explicit state management
  • Top-down execution flow
  • Data and behavior separation

Engineering Benefits

  • Simple debugging and tracing
  • Clear execution order
  • Minimal abstraction overhead
  • Predictable performance

Trade-offs

  • Potential for tight coupling
  • Global state management challenges
  • Limited reusability
  • Scalability concerns

Why is hard to scale with procedual?

Procedural programming is excellent for small-to-medium tasks because it is direct and efficient. However, as a project grows from 1,000 lines of code to 1,000,000, it often hits a "complexity wall."

  • The "Spaghetti" State (Global Data)
    • The Issue: If a variable like user_balance is changed incorrectly, you might have to check 500 different functions to find which one caused the bug.
  • ​High Coupling (The House of Cards)
    • The Issue: If you change a Customer Data record from an array to a dictionary, every single function that processes customers will break, and need to be rewritten.

  • Lack of "Truth" (Code Duplication)

    • The Issue: If you have three types of "Users" (Admin, Guest, Member), you might end up writing three nearly identical versions of a save_to_database() function.

  • ​High Cognitive Load

    • You have to track a linear sequence of events and know exactly which functions affect which data at any given time.

// Procedural approach: explicit state and sequential validation
let formErrors = {};
let isFormValid = false;

// Define constants for username validation
const MIN_USERNAME_LENGTH = 3;
const MAX_USERNAME_LENGTH = 20;

// Define constant for password validation
const MIN_PASSWORD_LENGTH = 8;

const validateUsername = (username) => {
    const errors = [];
    if (!username || username.trim().length === 0) {
        errors.push('Username is required');
    }
    if (username && username.length < MIN_USERNAME_LENGTH) {
        errors.push(`Username must be at least ${MIN_USERNAME_LENGTH} characters long`);
    }
    if (username && username.length > MAX_USERNAME_LENGTH) {
        errors.push(`Username cannot exceed ${MAX_USERNAME_LENGTH} characters`);
    }
    return errors;
}

const validatePassword = (password) => {
    const errors = [];
    if (!password || password.length === 0) {
        errors.push('Password is required');
    }
    if (password && password.length < MIN_PASSWORD_LENGTH) {
        errors.push(`Password must be at least ${MIN_PASSWORD_LENGTH} characters long`);
    }
    return errors;
}

const validateEmail = (email) => {
    const errors = [];
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
    if (!email || email.trim().length === 0) {
        errors.push('Email is required');
    } else if (!emailRegex.test(email)) {
        errors.push('Email format is invalid');
    }
    return errors;
}

const validateForm = (formData) => {
    formErrors = {};
    
    formErrors.username = validateUsername(formData.username);
    formErrors.password = validatePassword(formData.password);
    formErrors.email = validateEmail(formData.email);
    
    isFormValid = Object.values(formErrors)
        .every(fieldErrors => fieldErrors.length === 0);
    
    return { errors: formErrors, isValid: isFormValid };
}

Procedural Implementation: Form Validation

This procedural implementation demonstrates explicit state management through global variables and clear function separation.

 

Each validation function is independent and testable, while the main validation function orchestrates the entire process sequentially.

Object-Oriented Programming

Object-oriented programming organizes code around objects that encapsulate both data and the methods that operate on that data. State is managed within object instances, providing clear ownership boundaries and enabling data hiding through encapsulation principles.

 

Encapsulation

Data and methods are bundled together, with controlled access through public interfaces

 

Inheritance

Core reuse through hierarchical relations and polymorphic behavior

 

Abstraction

Complex implementations hidden behind simplified, intuitive interfaces​

class FormValidator {
  constructor() {
    this.errors = {};
    this.isValid = false;
  }

  validate(formData) {
    this.errors = {};

    const usernameValidator = new UsernameValidator();
    const passwordValidator = new PasswordValidator();
    const emailValidator = new EmailValidator();

    this.errors.username = usernameValidator.validate(formData.username);
    this.errors.password = passwordValidator.validate(formData.password);
    this.errors.email = emailValidator.validate(formData.email);

    this.isValid = Object.values(this.errors).every(
      (fieldErrors) => fieldErrors.length === 0
    );

    return { errors: this.errors, isValid: this.isValid };
  }

  getFieldErrors(fieldName) {
    return this.errors[fieldName] || [];
  }

  hasFieldErrors(fieldName) {
    return this.getFieldErrors(fieldName).length > 0;
  }
}

class FieldValidator {
  validate(value) {
    const errors = [];
    if (this.isRequired() && this.isEmpty(value)) {
      errors.push(this.getRequiredMessage());
    }
    if (!this.isEmpty(value)) {
      errors.push(...this.validateContent(value));
    }
    return errors;
  }

  isEmpty(value) {
    return !value || value.trim().length === 0;
  }

  isRequired() {
    return true;
  }
  getRequiredMessage() {
    return 'This field is required';
  }
  validateContent(value) {
    return [];
  }
}

class UsernameValidator extends FieldValidator {
  getRequiredMessage() {
    return 'Username is required';
  }

  validateContent(value) {
    const errors = [];
    const [minLength, maxLength] = [3, 20];
    if (value.length < 3) {
      errors.push(`Username must be at least ${minLength} characters long`);
    }
    if (value.length > 20) {
      errors.push(`Username cannot exceed ${maxLength} characters`);
    }
    return errors;
  }
}

Object-Oriented Implemntation Implementation: Validator Classes

Functional Programming: Immutability and Pure Functions

Functional programming treats computation as the evaluation of mathematical functions, emphasizing immutability and avoiding state changes. This paradigm promotes predictable behavior through pure functions that always return the same output for the same input, making code easier to reason about and test.

 

Pure Functions

No side effects, deterministic outputs, referential transparency

 

Immutability

Data structures never change, new instances created for modifications

 

HOF

Functions that take or return other functions, enabling powerful abstractions

Function Composition - Complex operations built by combining simpler functions

// Functional approach: pure functions and immutable state
const createValidator = (predicate, errorMessage) => (value) =>
  predicate(value) ? [] : [errorMessage];

const compose =
  (...validators) =>
  (value) =>
    validators.flatMap((validator) => validator(value));

const isEmpty = (value) => !value || value.trim().length === 0;
const hasMinLength = (min) => (value) => value && value.length >= min;
const hasMaxLength = (max) => (value) => value && value.length <= max;
const matchesPattern = (pattern) => (value) => value && pattern.test(value);

// Username validators
const isUsernameRequired = createValidator(
  (value) => !isEmpty(value),
  'Username is required'
);
const hasValidUsernameLength = createValidator(
  (value) =>
    isEmpty(value) || (hasMinLength(3)(value) && hasMaxLength(20)(value)),
  'Username must be between 3 and 20 characters'
);

// Password validators
const isPasswordRequired = createValidator(
  (value) => !isEmpty(value),
  'Password is required'
);
const hasValidPasswordLength = createValidator(
  (value) => isEmpty(value) || hasMinLength(8)(value),
  'Password must be at least 8 characters long'
);

// Email validators
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isEmailRequired = createValidator(
  (value) => !isEmpty(value),
  'Email is required'
);
const hasValidEmailFormat = createValidator(
  (value) => isEmpty(value) || matchesPattern(emailPattern)(value),
  'Email format is invalid'
);

// Composed validators
const validateUsername = compose(isUsernameRequired, hasValidUsernameLength);
const validatePassword = compose(isPasswordRequired, hasValidPasswordLength);
const validateEmail = compose(isEmailRequired, hasValidEmailFormat);

const validateForm = (formData) => {
  const errors = {
    username: validateUsername(formData.username),
    password: validatePassword(formData.password),
    email: validateEmail(formData.email),
  };

  const isValid = Object.values(errors).every(
    (fieldErrors) => fieldErrors.length === 0
  );

  return { errors, isValid };
};

Functional Implenentation: Composable Validation

State Management: Paradigm Comparison

  • Procedural State (Explicit and Mutable - Shared globally)
    • Global variables: formErrors, isFormValid
    • Direct mutation through assignment operations
    • Clear state visibility but potential coupling issues
    • Debugging is straightforward due to explicit state tracking
  • OOP - Encapsulated and Controlled (State is owned by Objects, well-defined access & mutation points)
    • Instance properties: this.errors, this.isValid
    • Controlled mutation via method calls
    • Object boundaries define state ownership
    • Data hiding prevents uncontrolled mutation
  • Functional State - Immutable and Transformed (no direct state change, functions return new computation, no side effect)
    • Immutable return values (new each function call)
    • Predictable behavior, through pure function discipline
    • Easy testing due to deterministic actions

I am too confused - too much information

TL;TR please?

Aspect Procedural OOP Functional
State management Explicit, global variables Encapsulated instance properties Immutable transformations
Code Organization Sequential functions Classes and Inheritance Composable pure functions
Testing Complexity Medium (state setup required) Medium (instance management) Low (pure function isolation)
Debugging Difficulty Low (explicit flow) Medium (method call traces) Low (no side effects)
Performance Overhead Minimal Low to medium Medium (object creation)
Scalability Limited (tight coupling) High (modular design) High (composability)
Learning Curve Low Medium to high High (mathematical concepts)
Maintainability Medium (refactoring risk) High (encapsulation benefits) High (predictable)

Engineering Trade off

Language by Category

  • Procedural
    • C
    • Pascal
    • Fortran
    • COBOL
    • Go
  • OOP
    • Java
    • C#
    • C++
    • Ruby
    • Smaltalk
  • Functional
    • Haskel (Pure with Lazy eval)
    • List
    • F#
    • Clojure
    • Go

Multi paradigm languages

ike Python, JavaScript, and Scala support multiple paradigms but often have a "preferred" or dominant style in their community

Recommendation: Modern JavaScript applications benefit from hybrid approaches that leverage the strengths of each paradigm. Use functional techniques for data transformations, object-oriented patterns for complex state management, and procedural approaches for performance-critical sections.

JavaScript Programming Paradigms: A Deep Engineering Perspective

By Eyal Mrejen

JavaScript Programming Paradigms: A Deep Engineering Perspective

  • 28