Common JavaScript Program Design Patterns: A Comprehensive Guide
Written on
Chapter 1: Understanding JavaScript Design Patterns
In the realm of programming, the journey often begins with theoretical knowledge. Before engaging in practical coding, one might find themselves immersed in design principles and patterns in textbooks, which can be quite overwhelming. The crucial aspect during this phase is to firmly grasp these concepts. Although the insights gained from literature may initially appear shallow, through years of coding experience, individuals cultivate effective habits that address real-world challenges. Over time, these habits become integrated with their theoretical understanding.
To genuinely appreciate the nuances of programming, one must engage in diverse experiences. These habits typically enhance the readability, extensibility, and efficiency of code. Below are some of my preferred patterns.
Typing Patterns π
Data types consist of specific values and the operations applicable to those values. When a data type is identified, it often dictates the logical structure of the code, creating a symbiotic relationship. Past experiences indicate a strong link between the complexity of logical implementation and the quality of design associated with data types. Selecting the right data types can greatly simplify logic, enhance scalability, and improve readability.
This concept mirrors the relationship between data structures and algorithms π§ , as the latter significantly impacts the performance of hardware.
Example:
Consider the following code snippet that contains multiple "magic strings," which can be quite daunting at first glance. Enumerations (Enums) serve as collections of constants, and when code relies on fixed constants that may obscure understanding, itβs beneficial to define an enumeration for clarity.
if (group === 'customer_free_group') {
list.push({
id: 'customer_free_cid',
type: 'EQ',
bizType: 'customer_free',
});
} else if (group === 'customer_biz_group') {
list.push({
id: 'customer_biz_cid',
type: 'AND',
bizType: 'customer_biz',
});
} else if (group === 'contact_group') {
list.push({
id: 'contact_cid',
type: 'IN',
bizType: 'contact',
});
}
By employing an enumeration type to define constants, we clarify the logic of the code:
enum GroupTypes {
customerFreeGroup = 'customer_free_group', // Free customer group
customerBizGroup = 'customer_biz_group', // Charge customer group
contactGroup = 'contact_group', // Contact group
}
enum LogicalOperationTypes {
EQ = 'EQ',
IN = 'IN',
AND = 'AND',
GT = 'GT',
LT = 'LT',
}
enum BizTypes {
customer_free = 'customer_free', // Free customer
customer_biz = 'customer_biz', // Charging customer
contact = 'contact', // Contact person
}
This adjustment significantly enhances readability and facilitates future expansion:
if (group === GroupTypes.customerFreeGroup) {
list.push({
id: IdTypes.customer_free_cid,
type: LogicalOperationTypes.EQ,
bizType: BizTypes.customer_free,
});
} else if (group === GroupTypes.customerBizGroup) {
list.push({
id: IdTypes.customer_biz_cid,
type: LogicalOperationTypes.AND,
bizType: BizTypes.customer_biz,
});
} else if (group === GroupTypes.contactGroup) {
list.push({
id: IdTypes.contact_cid,
type: LogicalOperationTypes.IN,
bizType: BizTypes.contact,
});
}
π As we progress, we observe numerous instances of similar code structures. There are various patterns to eliminate redundancy, and using appropriate type definitions is one approach. When repetitive structures appear, it may be time to consider a mapping table.
const groupConfigMap = {
customer_free_group: {
id: IdTypes.customer_free_cid,
type: LogicalOperationTypes.EQ,
bizType: BizTypes.customer_free,
},
customer_biz_group: {
id: IdTypes.customer_biz_cid,
type: LogicalOperationTypes.AND,
bizType: BizTypes.customer_biz,
},
contact_group: {
id: IdTypes.contact_cid,
type: LogicalOperationTypes.IN,
bizType: BizTypes.contact,
},
};
π After establishing this, the main code logic transforms into:
if (group in groupConfigMap) {
list.push(groupConfigMap[group]);
}
π This approach simplifies the code, reducing it from 20 lines to just 3, making it more concise and manageable. If new group types arise in the future, they can simply be added to the mapping table.
If your conditional logic heavily relies on specific key values, ποΈ consider using a mapping table. This technique can convert complex logic into straightforward lookups, π minimizing redundancy and enhancing clarity. π οΈ
Function Extraction Patterns π
When a function contains numerous branches and loops, it can become lengthy and challenging to read. In such scenarios, extracting portions of code that are either repeated or responsible for distinct tasks into a new function can be beneficial. Each new function should adhere to the single responsibility principle, ensuring that it performs a singular task.
Example π:
Hereβs a snippet from a function where the bizName is determined based on bizStatus and bizAction:
if (bizStatus === 'RUNNING') {
bizName = 'flow_in_approval';
} else if (bizStatus === 'COMPLETED') {
if (bizAction === 'modify') {
bizName = 'flow_modify';} else if (bizAction === 'revoke') {
bizName = 'flow_revoke';} else {
bizName = 'flow_completed';}
} else {
bizName = 'sw_flow_forward';
}
By restructuring the code, we can simplify it:
const BizStatus = {
RUNNING: 'RUNNING',
COMPLETED: 'COMPLETED',
};
const BizAction = {
MODIFY: 'modify',
REVOKE: 'revoke',
REFUSE: 'refuse'
};
const getBizName = (bizStatus: BizStatus, bizAction: BizAction) => {
let bizName = '';
if (bizStatus === BizStatus.RUNNING) {
bizName = 'flow_in_approval';} else if (bizStatus === BizStatus.COMPLETED) {
if (bizAction === BizAction.MODIFY) {
bizName = 'flow_modify';} else if (bizAction === BizAction.REVOKE) {
bizName = 'flow_revoke';} else {
bizName = 'flow_completed';}
} else {
bizName = 'flow_forward';}
return bizName;
}
The original function now exhibits a clearer sequential flow:
bizName = getBizName(bizStatus, bizAction);
Summary π
When functions grow excessively large and convoluted, it's advisable to consider segmenting them into smaller, more focused functions. Gradually breaking down a complex function allows for a simpler structure in the main code. Thoughtful naming of these new functions significantly boosts readability and maintainability.
However, if a function like getBizName still feels off, further techniques for managing branching logic can be employed.
Branching Logic Patterns π
Branching structures are prevalent in programming, allowing for different execution paths based on conditions. Nevertheless, excessive branching can complicate understanding and maintenance. Optimizing branch logic can enhance code clarity, maintainability, and performance.
Guard clauses are a programming style that leverages conditional statements to exit function execution early, avoiding excessive nesting and promoting flatter code. They are commonly utilized for parameter validation or condition checks, allowing for immediate exits when conditions aren't met.
Example π:
Building on the previous example, let's streamline the code by merging nested conditions into a single expression using logical operators and applying guard clauses:
const getBizName = (bizStatus, bizAction) => {
if (bizStatus === BizStatus.RUNNING) {
return 'flow_in_approval';}
if (bizStatus === BizStatus.COMPLETED && bizAction === BizAction.MODIFY) {
return 'flow_modify';}
if (bizStatus === BizStatus.COMPLETED && bizAction === BizAction.REVOKE) {
return 'flow_revoke';}
if (bizStatus === BizStatus.COMPLETED) {
return 'flow_completed';}
return 'flow_forward';
};
This example could also benefit from type definitions, and further function extraction may be warranted depending on the complexity of the code.
Summary π
Simplifying conditional expressions, utilizing guard clauses, and employing mapping tables are effective strategies for managing branching logic, enhancing both readability and maintainability. Keeping code clean and adaptable requires ongoing maintenance and the application of best practices.
Final Thoughts
JavaScript program design involves mastering various patterns and principles that ultimately bolster code readability, maintainability, and efficiency. By embracing typing patterns π and utilizing enums and mappings, we enhance clarity. Through function extraction patterns π, such as employing guard clauses and the strategy pattern, we break down complex logic into manageable units, adhering to the single responsibility principle.
These strategies, alongside others like branching logic patterns π, equip us with essential tools to navigate the intricacies of program design, ensuring that code remains clean, adaptable, and effective. π οΈ
In Plain English π
Thank you for being part of the In Plain English community! Before you go, consider following us on our various platforms for more insightful content.
Explore the Factory Pattern in JavaScript with this detailed video, providing insights into design patterns that enhance coding efficiency.
In just 10 minutes, gain a solid understanding of 10 essential design patterns that can elevate your programming skills.