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.
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 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.
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."
user_balance is changed incorrectly, you might have to check 500 different functions to find which one caused the bug.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 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.
Data and methods are bundled together, with controlled access through public interfaces
Core reuse through hierarchical relations and polymorphic behavior
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 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.
No side effects, deterministic outputs, referential transparency
Data structures never change, new instances created for modifications
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
formErrors, isFormValidthis.errors, this.isValidI 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
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.