You are viewing the Articles in the Object Oriented Design Category

Front-End System Design: 12 Fundamentals You Need to Know



To commemorate the 20th Anniversary of this Blog, I wanted to publish an Article which would stand the test of time; providing value to developers of all levels; both now, and for many years to come. In giving thought as to what subject should be covered, I contemplated the recurring issues I have observed over the course of the past 2 decades. In nearly all cases, the issues encountered were almost exclusively related to a lack of awareness of specific design fundamentals. As such, the subject for this article naturally revealed itself …


The world in which we live consists of one philosophical constant – change. This fact is ever apparent in the continually evolving field of front-end web development. The overwhelming amount of emerging libraries, frameworks, tools, and methodologies may seem daunting at times. However, by having an understanding of the 12 Fundamental system design principles outlined in this article, developers can move forward with confidence in the knowing that they have strategies to employ which transcend technologies and trends.

This article aims to provide insight into each principle; presenting them with a succinct overview, as well as a mental model by way of metaphor so as to help form a conceptual framework at a deeper level. Having an understanding of these fundamental principles and, more importantly, applying them in practice, will serve you well when making design decisions, both now and well into the future …

Note, references to current frameworks, methodologies, and language specifics have been intentionally omitted so as to reflect the timelessness of each principle.


Fundamental to software architecture are specific tried and tested core principles which provide a conceptual baseline upon which a system can be designed. While there are many aspects to consider, the 12 Principles discussed in this article have been chosen specifically due to their being fundamental in nature. They are as follows:

Each of these foundational principles provide innate tenants of quality software design. They transcend technologies and frameworks, and applying them in practice will naturally translate across all programming paradigms. It is important to develop an understanding of each at scale to the point where they become second-nature when making design decisions; be it a relatively simple function, or a large enterprise-class application.


If one were to ask any member of the teams I have guided throughout my career what the single most advocated principle I champion has been, I am rather certain they would unanimously respond with: Separation of Concerns – and for good reason as, software architecture is all about managing complexity, and by utilizing Separation of Concerns, systems become significantly easier to manage. In fact, each subsequent principle outlined within this article is inherently related to Separation of Concerns. Therefore, it is appropriate to include Separation of Concerns as the first fundamental.

Overview

Separation of Concerns involves structuring a system into distinct sections, each addressing a specific concern. All Front-End Developers are familiar with this concept at a high level as, separating markup (HTML), styling (CSS), and behavior (JS) is an age old best practice. This same concept can be applied to span across the entirety of the system, from individual pages and features, down to low-level implementation specifics such as functions and conditions.

For instance, we have all seen components which start out relatively simple and are focused on a single concern, only to degrade over time to include additional responsibilities which are beyond the scope of the component’s original intent. More often than not, this is the result of developers failing to take a step back to think through how changes should be integrated when tasked with introducing new requirements, and instead, making the mistake of just adding additional logic and behaviors to the existing component. This very issue is intrinsically related to many of the principles included in this article, specifically, Abstraction, Loose Coupling, and the Open / Closed Principle.

Mental Model

Think of a house, where the concept of Separation of Concerns is similar to the distinct roles each area has within the home. Just as a house is divided into different rooms, each serving a specific purpose, the principle of Separation of Concerns involves structuring a software system into distinct sections, each concerned with a specific aspect of the system.

In a typical home, you may have a kitchen, living room, bedrooms, and a bathroom. Each room is designed and equipped for a specific function. The kitchen has appliances for cooking, the living room has seating and entertainment options, bedrooms have beds and wardrobes, bathrooms have showers and toilets, and so on. This intentional separation allows for efficient organization and effective maintenance. This same concept can then be further iterated on for each individual area, such as separating the concerns for each function within a given area. By utilizing Separation of Concerns, a structured and organized living space which is much easier to maintain can be achieved; for instance, if you want to remodel the bathroom, you don’t need to make changes to any other areas of the home.

Just as a well-designed house provides a comfortable and functional living space, applying Separation of Concerns in software development results in a modular system which is much easier to maintain and facilitate scale. Each ‘room’ or component can be implemented, tested, and modified independently, resulting in the system as a whole being much easier to adapt to change.

Summary

Separation of Concerns is the foundational Principle by which all subsequent principles abide; it is simply about dividing a system into specific areas (concerns) based on what they are responsible for. By taking the time to define each concern based on it’s context and scope within the system, and implementing dedicated modules for each concern, the cumulative result will be a maintainable and scalable system which simply could not be achieved otherwise.


Just as with Separation of Concerns, if one were to ask what the second most advocated principle I champion has been, I am certain they would respond with: Abstraction – and likewise, for good reason as a key aspect of Abstraction is that it serves as the primary agent for reuse – this is a very important point to keep in mind. In addition, Abstraction is the key mechanism by which DRY Principles are facilitated.

Overview

The concept of Abstraction simply refers to the process of breaking down complexity into smaller, more discrete units, each responsible for a specific behavior in order to facilitate reuse and simplification.

Mental Model

Abstraction is perhaps the one of the most omnipresent concepts in life, so much so that it often goes completely unnoticed due to its ubiquity. The simplification of highly complex concepts and representations into easily comprehensible constructs can be found literally everywhere. For instance, your name is merely an abstraction of what would otherwise be a highly complex representation which encompasses your personality, experiences, characteristics, and so forth. The examples of abstraction are virtually endless as it is employed everywhere to simplify our day-to-day lives.

Summary

By utilizing Abstraction consistently, overtime you will get to the point where you intuitively “see” what should be abstracted, and when it should be abstracted – without having to give much thought to it at all.

Abstraction requires discipline as, developers may initially feel that doing so results in “more work”; however, I strongly advice against this mindset as it can easily be argued that not doing so will ultimately result in considerably more work down the line, at which point addressing the technical debt will undoubtedly require more effort. Pay now, or Pay more later.

A final take away I can attest to based on my experience is that be utilizing abstraction, it will result in your becoming a significantly faster developer. I state this with confidence as, the additional time required when abstracting needs to be managed with your agreed upon deliverables. However, by making a commitment to cease opportunities for reuse via abstraction you will inevitably become a much better, and much faster, developer. Couple this with the additional level of reuse abstraction affords and your future self and co-workers will thank you for having made the investment up front.


A core concept in object-oriented programming is that of Encapsulation, which involves co-locating state and the APIs which operate on that state within a single unit. While in traditional OOP, the unit would typically refer to a class, this concept can also be applied to virtually anything; from a large component down to a small function.

Overview

The idea behind Encapsulation is to simply restrict access of internal state to only that of the unit itself, which is a means of preventing accidental public exposure and unintentional access and / or misuse of the operations and state; thus access should only ever be exposed via a public API. The less state and internals exposed by a component, the better.

Mental Model

Encapsulation in software development can be metaphorically compared to a vending machine. Imagine a vending machine filled with various snacks and drinks. From the outside, users interact with a simple interface: they select a product and make a payment. However, the internal mechanisms of the vending machine – the way it stores items, manages inventory, and processes payments – are hidden from the user.

In this analogy, the vending machine represents a component or module in the system. Clients (users of the component), much like customers of the vending machine, only see and interact with its public interface. The internal state and logic of the component – akin to the inner workings of the vending machine – are encapsulated and hidden from the outside world.

This encapsulation ensures that the internal state of components are protected from unintended interference and misuse, similar to how a vending machine’s inventory and internal mechanisms are safeguarded behind its exterior. This allows developers to change the internal implementation without affecting other parts of the system that depend on the component, just as the internals of vending machine can be changed without altering the customer’s experience.

Summary

In front-end development, encapsulation allows us to create components that hide their internal state and behavior, and only expose what is absolutely necessary; thus, providing a clear and consistent API. This not only enhances readability and maintainability but also improves data integrity and security within the application. Effective use of encapsulation leads to more modular and scalable solutions, allowing developers to change one part of the system without affecting others – which is a good segue into our next fundamental.


We have all experienced this at one time or another – you make a change in one component and inadvertently introduce a regression in another. This is almost always the result of tight-coupling, which is precisely what Loose Coupling aims to address.

Overview

Loose Coupling is the concept of designing components such that changes in one area will not affect those in other areas. This decoupling can be accomplished utilizing many of the other 12 Fundamental Principles, specifically, Separation of Concerns, Abstraction, Encapsulation, Single Responsibility Principle, and the Dependency Inversion Principle.

Mental Model

Metaphorically, Loose Coupling can be compared to an entertainment system in a home where you have a TV, a speaker system, a streaming device, and a remote control. Each component operates independently: the TV displays content, the speakers provide audio, the streaming device offers various content options, and the remote controls these devices.

While each is connected to form the whole, they are not connected in such a way that a change in one component should necessitate changes in other components. For instance, if you replace the streaming device with a newer model, this doesn’t require a new TV or speaker system; they continue to function together seamlessly. This independence and interchangeability reflects the essence of loose coupling in software, whereby different modules or components interact with each other through well-defined interfaces, without being heavily reliant on the implementation specifics of the others in the system.

Summary

By designing components such that each operates independently and only interacts with other parts of the system based on specific integration points, we can build robust systems which are not prone to the negative effects of brittle, tightly coupled designs.

Loose Coupling always results in systems which are significantly easier to maintain and scale as, should a change be required in one area, the impact of that change in other areas will be minimal. These benefits can also be greatly enhanced when utilizing the Dependency Inversion Principle, which outlined as well in this article.

A final take away on Loose Coupling based on my experience is that, as general rule of thumb, if it is hard to unit test, the SUT is likely not utilizing Loose Coupling.


Having it’s origins in the context of Object Oriented Programming, the Principle of Composition Over Inheritance simply refers to the process of having a class composed of other classes to provide specific functionality rather than extending an existing class and providing the functionality directly in the sub-class.

Overview

While Composition Over Inheritance is traditionally an aspect of Object Oriented Programming, which is also relevant in the design of non-visual front-end APIs; more generally speaking, in front-end development, preferring composition over inheritance can be equated to designing complex features based on a composition of many smaller sub-features. This affords the ability to modify a part of the system without the need to make changes to other parts of the system, or the system as a whole.

Mental Model

Metaphorically, composition over inheritance is akin to assembling a kitchen within a home based on various standalone appliances. For example, imagine a kitchen where you have a coffee maker, a blender, a toaster, and a microwave. Each appliance performs a specific function and can be used independently or in combination with others to accomplish a task, such as making breakfast. If you need new functionality, say, a food processor, you can simply add it to your kitchen without altering the existing appliances.

This modular approach, where you compose a kitchen with different appliances, mirrors the principle of composition over inheritance in software. Rather than inheriting all functionalities from a monolithic parent component (a multi-function appliance), you compose a component with other components as needed (like adding various kitchen appliances). This in turn provides a great amount of flexibility and ease of maintenance; allowing for more customizable and adaptable systems.

Summary

While in functional component based frameworks inheritance isn’t always directly relevant, composition most certainly is. It is a primary tenant of modularity, and by composing features from a number of sub-features the resulting benefit is a systems which can adapt to change much more easily while implicitly adhering to many of the 12 Principles, specifically, Loose Coupling, Abstraction, and Separation of Concerns.

As with all of the 12 Fundamental Principles, the scope of Composition Over Inheritance can be applied at scale to all aspects of the system, such as breaking down large conditions into a set of smaller conditions, or large functions into a composition of smaller functions, composing larger features based on a set of smaller sub-features, etc.


Most developers have come across this problem at one time or another; a ticket gets created for a bug where users are complaining that they are making updates in one area of the application, but those changes are not being reflected in another area. This issue is almost always related to violating the Single Source of Truth Principle; as what should be represented as a single state, in a single location within the application is instead duplicated and is being referenced differently by different components.

Overview

The Principle of Single Source of Truth simply entails having a single, authoritative location where a particular piece of information (state, etc.) resides. In the context of front-end development, this generally equates to having immutable state management strategies where the state of the application is managed centrally rather than scattered across various components.

Mental Model

Single Source of Truth can be compared to a bank account number. Just as a bank account number uniquely identifies your account and all transactions associated with it, serving as the single, authoritative reference to your account, Single Source of Truth in software ensures that every piece of only data exists in single location. In the context of this metaphor, regardless of how many times you might access your account — be it through an ATM, online banking, or from a physical branch — the account number is always a reference to the same account. This prevents discrepancies and confusion.

Summary

It is important to design systems such that all references to application and / or asynchronous state are unified. Transformations of shared state should be the responsibility of the components which need to make such transformations; as opposed to duplicating state to accommodate the needs of specific components. By having a single, authoritative source of data, we can ensure that all components reference and / or update the same data / state, maintaining consistency and integrity throughout the application.


Anyone who regularly conducts code reviews has inevitably found themselves dumbfounded at one time or another as to why a specific piece of code has been included within a certain context. For example, an implementation specific, non-generic string utility added to a common String utilities module. Or a specific translation entry added to a completely unrelated translation document, etc. These types of mistakes always result in Low Cohesion, which is certainly not desirable, and is effectively the inverse of High Cohesion.

Overview

High Cohesion in front-end development refers to the practice of co-locating related functionality so that each module or component is focused and designed around a specific task. High Cohesion is often correlated to Loose Coupling, and vice-versa.

Mental Model

High Cohesion can be likened to a well-organized toolbox. In a toolbox, tools are organized in a way that each tool is grouped by its specific function – for instance, screwdrivers of various sizes in one compartment, wrenches in another, and so forth. Each compartment represents a cohesive unit, holding tools that are closely related in terms of their function. This makes it easy to find and use the tools for a particular task, enhancing efficiency and orderliness. Similarly, in software development, high cohesion refers to the practice of designing components or modules in a way that they are focused on a specific set of related tasks or functionalities. Like the compartmentalized toolbox where each section is dedicated to a specific type of tool, a highly cohesive software module or component focuses on a particular aspect of the application’s functionality, making the software easier to maintain, understand, and extend.

Summary

Utilizing high cohesion results in increased discoverability, which is crucial to facilitating reuse as, by having related components and APIs intuitively structured, it becomes much easier for teams to understand the system and find what exists within the codebase; thus reducing the probability of duplication – which lead us to our next Principle …


Another common issue we often find ourselves having to contend with is that of repetition. This can manifest in many, many ways. For instance, something as seemingly simple as repeated references to the same property over and over again rather than creating a variable which points to the property and using it throughout. Or developers simply copy / pasting implementations repeatedly across components as opposed to utilizing Abstraction to create a reusable implementation for use by all components. The number of permutations of duplication are virtually endless; however, by thinking in terms of DRY Principles, we can ensure such duplication is negated.

Overview

The DRY (Don’t Repeat Yourself) Principle emphasizes the importance of reducing repetition and duplication across a system. This can be accomplished by simply ensuring each piece of functionality is only implemented once, thus DRY helps promote a significantly cleaner, easy to comprehend codebase which promotes reuse and is easier to maintain.

Mental Model

Imagine you are baking cookies and you have a cookie cutter mold in a specific shape, say a star. This mold allows you to consistently produce star-shaped cookies without having to manually shape each one.

In a non-DRY approach, this is akin to not having a cookie cutter and shaping each cookie by hand. This method is not only time-consuming but also leads to inconsistencies in size and shape.

Applying the DRY principle is like using a cookie cutter mold. You create the design once, and then you use it repeatedly to produce consistent results. This ensures uniformity while also saving a significant amount of time. If you need to change the design, you simply modify the mold (the single implementation), and every new cookie (function or feature) produced thereafter will reflect this change automatically.

This metaphor illustrates how the DRY principle in software development encourages creating a single, reusable ‘mold’ for recurring patterns or functionality. Just like the cookie cutter allows for efficient and consistent cookie production, DRY leads to more efficient and consistent code by reducing repetition and simplifying maintenance. When a change is needed, we need only update a single implementation, which ensures all parts of the system which use the implementation (mold) are automatically updated.

Summary

In the context of software, DRY isn’t just about avoiding duplicate lines of code. It’s about recognizing patterns and abstracting them into reusable components – be it functions, classes, modules, etc.. This abstraction means that a single change can be reflected consistently throughout the application, while also being tested in isolation from the rest of the system.

One point to keep in mind; however, is that it is important to strike a balance. That is, overzealous application of DRY, like overly relying on a single cookie cutter mold for multiple unrelated purposes, can lead to convoluted and rigid structures. The key is to identify and abstract genuinely reusable elements, ensuring that the abstraction makes logical sense and enhances the system’s maintainability and scalability.


When designing a system it can be tempting to anticipate additional features and requirements that will be needed in the future. However, it is important to use constraint and address any anticipated changes by ensuring our designs can support them rather than adding them straight-away. This mindset embodies the Principle of KISS (Keep It Simple, Stupid).

Overview

The KISS Principle encourages simplicity in design. In front-end development, this can encompass anything favoring straight-forward solutions over unnecessarily complex ones, choosing simpler algorithms rather than overly arcane implementations, and resisting the urge to include additional libraries or heavy frameworks when doing so would be overkill.

Mental Model

The KISS Principle can be compared to cooking a meal. For instance, imagine you’re preparing a dish – you have a number of ingredients at your disposal. The KISS principle here would be synonymous with choosing to create a delicious, yet simple meal that requires fewer ingredients and straightforward techniques, rather than opting for an overly elaborate recipe that complicates the cooking process and increases the chances of something going wrong.

In the context of front-end development, KISS encourages the use of simple, straightforward solutions. It’s akin to choosing to make a classic dish that you know well, rather than attempting an intricate recipe for the first time when you have guests over in a few hours. For example, if you need to implement a feature that could be accomplished with basic JavaScript, it would be more in line with KISS to avoid introducing a new library that adds unnecessary complexity and overhead to your project.

It is important to note that this principle is not about always choosing the simplest possible solution, but rather, it is about not introducing unnecessary complexity – remember: software architecture is all about managing complexity.

In our cooking metaphor, it’s not about always opting to make a sandwich, but choosing not to prepare a five-course meal when a single, well-made dish would suffice. It’s about understanding the requirements and context, and then applying the most straightforward solution that effectively meets current requirements.

Summary

In essence, KISS is about efficiency and simplicity. It’s concept serves as a reminder to favor simplicity over complication – to focus on the essentials and keep things as uncomplicated as possible, while also being sure to account for additional complexity within your designs for when they are actually needed. The KISS Principle is closely related to our next principle as well …


Similar to the KISS Principle, the YAGNI Principle advocates for restraining from adding anything that is not immediately necessary to satisfy the current requirements. By resisting the temptation to over engineer parts of the system, we ensure our codebase remains lightweight and maintainable.

Overview

The YAGNI Principle advises against implementing features or functionalities until they are actually needed. In front-end development, this can include anything from adding additional libraries or tooling prior to having an actual need to include them, to delivering overly complex features prior to having an MVP released.

Mental Model

The YAGNI principle in software development can be likened to packing for a vacation – you have a suitcase and a wide variety of items you could potentially take with you. YAGNI is like having the wisdom to only pack what you know you will need for the trip, rather than burdening yourself with items for every possible – and unlikely – scenario. By omitting unnecessary items just in case you might need them, your planning and travel experience becomes much more simplified, not to mention, you’ll have a lighter suitcase to carry around.

In front-end development, applying YAGNI means focusing on what is essential for the application’s current requirements. It’s akin to packing your suitcase with just the right amount of essentials necessary for your trip, and leaving behind the “just-in-case” items that are unlikely to be used and add nothing more than additional weight. For instance, if you’re building a simple web application, YAGNI would advocate for only introducing a proportionally simple frameworks rather than a potentially overly complex option, especially if it is known upfront that the application will not require additional features once completed.

In essence, YAGNI encourages developers to add complexity only when it is justified by a real and present need.

Summary

The YAGNI Principle is about avoiding the temptation to include unnecessary features, tools, or additional functionality based purely on the speculation that they might be useful in the future. This temptation often arises due to developers wanting to utilize a new library of framework; however, the decision to do so should be based on the current needs of the business and technical specifics of the project – there is no need to introduce a solution for which there is no problem.

By adhering to YAGNI, we can ensure that our application remains agile, lightweight, and easy to manage, only ever adding what is needed, when it is needed.


The Law of Demeter (Principle of Least Knowledge) advocates that a given unit of code should have limited knowledge about other units; that is, only closely related units should have cross-cutting concerns with other units in the system. In front-end development, this could be expressed as a component should not directly manipulate or rely on the internal workings of other components aside from what is explicitly required.

Overview

The Law of Demeter, also referred to as The Principle of Least Knowledge, is a design guideline which emphasizes minimizing the knowledge that a particular module has about the internal workings of other modules in the system. This concept is fundamentally about reducing tight-coupling between components, so as to promote Loose Coupling.

Mental Model

Conceptually, this Principle can be thought of as having a “need-to-know” basis within an organization. For instance, imagine an organization where each department focuses solely on its specific responsibilities and communicates with other departments in a controlled and limited manner. Each department only knows and interacts with the information and resources that are directly related to its function, rather than being entangled in the specifics of other departments’ operations.

In the context of front-end development, this principle can be visualized as a component structure where each component is like an individual department. According to the Law of Demeter, a component should not have intricate knowledge of the internal workings of other components. It should interact with other components or sub-components in a restricted and well-defined manner. This approach is akin to departments communicating with one another through official channels and predefined protocols, ensuring that each maintains its autonomy and boundaries.

For example, in a web application, consider a parent component that renders child components. The Law of Demeter suggests that the parent should not directly manipulate the state or internal methods of the child components. Instead, it should pass state as needed, akin to sending official memos or directives. This encapsulates the child components’ internal logic, allowing them to operate independently of the parent’s implementation details, much like how a department functions autonomously within the larger framework of the organization.

Summary

The Law of Demeter in the context of front-end development is concerned with how components interact with each other: by maintaining their autonomy, and interacting through well-defined interfaces, much like departments within an organization communicate through official protocols. The result will be a much more modular, decoupled and cohesive system.


Arguably the most proliferated of all system design principles, SOLID Principles present a set of five specific design guidelines which collectively promote elegant software designs with an emphasis on maintainability and scale.

The 5 core Principles of SOLID are briefly described below.

  • This Principle is essentially akin to Separation of Concerns, whereby each module or component in the system should have only a single responsibility, thus reducing complexity and improving maintainability.

    Mental Model

    The Single Responsibility Principle can be metaphorically compared to the roles of individuals in an organization whereby each employee has a distinct and specific role. For example, an accountant is responsible for managing finances, a human resources officer handles employee-related issues, a salesperson focuses on selling the company’s products or services, and so on. Just as the accountant doesn’t manage sales, and the salesperson doesn’t handle HR issues, in software design, each module should have one, and only one, responsibility, and thus, only one reason to change. This results in an encapsulation of all of the responsibilities associated with a specific role within the organization.

  • The Open / Closed Principle prescribes that components should be open for extension, but closed for modification. This helps promote scalability while reducing the risk of introducing breaking changes to existing code.

    Mental Model

    The Open / Closed Principle in software design can be compared to a concert stage. For instance, imagine the stage is initially set up for a particular performance, say, a classical music concert. The stage (software module) is designed and built (implemented and tested), and serves its purpose (satisfies the requirements) for the concert. Now, suppose the next event is for a completely different genre, say, an EDM festival. Rather than completely dismantling and rebuilding the stage (modifying the existing code), the stage is simply extended – perhaps by adding new lighting system, an enhanced speaker array, etc., thus, extending the existing functionality.

  • The Liskov Substitution Principle specifies that objects within a system should be able to be used interchangably with instances of their subtypes without altering the correctness of the program.

    Mental Model

    Think of this principle as being similar to interchangeable power adapters for electronic devices. Just as different adapters can be used interchangeably with various devices without affecting their operation, components in a system should be replaceable with instances of their subtypes without altering the program’s correctness.

  • This Interface Segregation Principle advocates that larger components should be broken down into smaller, more specific components so as to ensure each only needs to interact with the APIs that are relevant to them.

    Mental Model

    Consider a multi-purpose screwdriver with detachable bits, each bit serving a specific function; such as a phillips head bit, a flathead screwdriver bit, etc.. Users attach only the bit they need, similar to how larger components should be broken down into smaller, more specific components. This way, each part of the system only interacts with the interfaces it requires, making it more manageable and easier to understand.

  • The Dependency Inversion Principle states that systems are to depend on abstractions, rather than concrete implementations. In doing so, we afford ourselves the ability to decouple dependencies, which results in significantly more flexibility. This principle is closely related to the concept of IoC Containers, Dependency Injection, and Inversion of Control.

    Mental Model

    This principle is like hiring a contractor to remodel your house. You’re not concerned with the specific tools they use; you’re interested in the skills and results they provide. In software, systems should depend on abstractions (the skills) rather than concrete implementations (the specific tools), allowing for greater flexibility and easier maintenance.


Hopefully this article has provided some insight into the timeless benefits afforded by the these 12 fundamental principles. If you are new to them, I encourage you to try a practical exercise where you simply apply these principles in a context unrelated to software development.

For instance, make a list of the 12 principles and utilize them to re-organizing your workspace, crossing out each as they have been applied. Once completed, try applying them for each specific area of your workspace. Continue this exercise for other areas of your living space, such as re-organizing your closet, or your kitchen cabinets, etc. These exercises will not only lead to practical and productive results, but will help you form a mental model of the principles in practice, which will naturally translate in your development practices. The more you apply them, the more intuitively you will begin to recognize when to apply them in your day to day work.

The landscape of front-end web development is constantly evolving; however, by understanding these 12 Fundamental Principles, and more importantly, applying them in practice, developers and teams can design elegant architectures in confidence which not only meet current and future demands, but also allow for ceasing new opportunities as well.

Cross-published at medium.com

Facilitating Reuse with TypeScript Generics

As is so often emphasized in my articles, facilitating reuse is perhaps one of the most import aspects of system design. For over a decade, languages such as Java, and C# have used generics as one of the primary mechanisms for reuse (I recall first hearing about them in C# back in 2005). Fortunately, Generics have been a key feature of TypeScript since the very beginning.

This article aims to briefly illustrate some of the main benefits of leveraging Generics in TypeScript to simplify implementations and maximize reuse.

Typescript Generics are a language feature that allow for providing reusable implementations based on type deferment. This means you can create a function, interface, or class with a placeholder for a type, thus affording the flexibility to use any type without losing the safety and robustness of type checking.

With Generics, types can be defined as an abstraction such that the types can be specified by clients based on their context. This affords the ability to allow implementations to adapt to the types specified while still ensuring type integrity is preserved.

Generics are defined within angle brackets <>, typically specified as “T” (for “Type”), where “T” is simply an arbitrary placeholder name which is used to denote and reference the type.

For example, consider the following function:

Using Generics, we can implement the function to accept a Generic Type rather than a fixed type:

In the above example, we define the generic within angle brackets, and reference the Type via the “T” value passed to the generic. Now, rather than only allowing strings to be logged, any type can be provided.

We can then use the generic by simply specifying the type when we invoke the function:

Note that in the above, we explicitly set the type to string; however, this isn’t necessary when the type can be inferred, in which case the type can be omitted if preferred:

We can also pass any type to the function’s generic as well:

Generics are not limited to functions. They can also be applied to interfaces and classes as well:

As can be seen in the above, just as with functions, we can define Generics for interfaces and classes using the same syntax.

TypeScript’s Generics also support more advanced features such as constraints, default types, and using multiple types, allowing for more precise and robust implementations.

Using Constraints
Using constraints in Generics allows for specifying requirements for the generic types. For instance, we can define a function which only operates on types that have a certain property or method.

Consider a getLength function which only accepts arguments that have a length property, ensuring the argument is array-like or a string:

Default Types
Default types in generics offer a way to specify a fallback type to be used when a type is not explicitly provided. This is quite useful for providing default implementations, while still affording the flexibility of allowing clients to override the default type.

For example, we can create a generic getData function that fetches data and returns a response of the provided type, or fallback to a default type if not provided:

Using Multiple Types
Multiple types can also be used in Generics, allowing for creating functions, interfaces, or classes that can handle more than one type of input, and / or return more than one type of output, offering greater flexibility and precision.

For instance, consider a simple merge function which combines two different objects into one, while preserving their distinct types:

We can also expand on this to include constraints to ensure the merge function only accepts parameters that are object-like:

TypeScript Generics offer a powerful way to build flexible, reusable, and type-safe solutions. By abstracting over types, Generics allow for providing implementations which can operate on a variety of data types, resulting in increased scalability and maintainable.gnificantly enhance the robustness and versatility of your code.

BDD/TDD Mental Models

Recently, I shared a simple 8-step procedure with my team which outlines some of the general questions I tend to ask myself when writing tests, even if, perhaps, only subconsciously so.

While quite simple in form, and somewhat obvious in process, this procedure helps to develop a useful mental model from which practical steps can be applied to common testing scenarios; which, in turn, helps to provide clarity of general design considerations, while also helping to guide specific implementation decisions.

First things First

Arguably, the single most important aspect of testing (and software development in general, for that matter) is to acquire a solid understanding of the problem domain; for, without having (at minimum) a general understanding of the problem one is intending to solve, important details are likely to be omitted which would have otherwise been considered, and thus, covered by our tests. Spend time understanding exactly what problem your code is intended to solve, then begin thinking about what to test for. Understand the Problem.

Small Steps

Once confident that a good understanding of the problem has been reached, we can then get started on writing our initial tests. Consider this as a first pass, if you will, whereas we are only concerned with getting our tests to pass in the simplest (typically, least elegant) way possible. The initial implementation code can be as raw (and ugly), as needed, as this can (and will) be addressed after our initial tests are passing. If we are writing tests against code that does not yet exist, then we will first write the implementation code (the code that is being tested), directly within the test case itself. Once the test passes, we can then refactor the code out from our test and into the SUT (code we are testing). If the code already exists (we are writing new tests against existing code), we still need to understand and consider the implementation of the code itself, and not just simply write tests against it. Reviewing and critiquing existing code is an excellent way of gaining a quick understanding of a given system. Seize initial opportunities. Start off slow.

Clean Pass

Once we’ve written our initial tests and they are passing, we can then safely go back into our new or existing implementation code and refactor it to our hearts content. If we break something, our tests will let us know. After all, one of the most rewarding aspect afforded by unit testing is the ability to refactor our code freely with little worry or concern that we will unintentionally break something without knowing. If something breaks, are tests will inform us. Tomorrow never comes in Software Development. Clean up as you go along.

Negative Tests

The most obvious tests to write are those which are against the things we are expecting the code to do. But what about if the code is used incorrectly? What if an argument is required and it is not provided, or it is of an invalid type? Does our code throw an exception? Does it simply return undefined? What should it do? These are all questions we should be asking ourselves once our expected test cases are passing. After that, we need to start thinking about ways to have our code appropriately respond to negative cases – we don’t want the entire app to become in an unpredictable state just because an uncaught exception was thrown due to some simple string formatting argument not being passed, etc.. Test the exceptional; Test the unexpected.

Stateless Tests

One of the most important considerations to make both during and especially after all of the above points have been considered, is the statelessness of the system while being tested. Always ask yourself, “Am I resetting the state of all my test’s dependencies back to an expected state?”. This is perhaps one of the most commonly overlooked, yet crucially important consideration to make. A good example illustrating why this is important can be found in the common scenario of a test that invokes a method which triggers an event. If any previously executed tests which handle the event have not been properly tore down (e.g. afterEach), the object will still exist; and thus handle the event. This typically results in a change in state, more often than not causing an unexpected error to be thrown. Always use set-ups (e.g. beforeEach) to configure your tests environment, fixtures, any dependencies your test requires to operate properly. If you are setting values on anything outside the context of your tests; always use mocks, stubs and tear-down methods (e.g. afterEach) to reset them back to an expected state. Remember, while your tests are not part of your applications source, they are certainly part of your projects source; this, in effect, requires them to be viewed as first class citizens; subject to the same quality design and implementation as project source. Tests will need to evolve and be continually maintained. Treat the test environment with respect; ensure you return it in a predictable state. Leave it the way you found it.

Continued Improvement

While the above description of Stateless Tests clearly states that the test environment should remain stateless, and thus “remain as we found it” prior to our tests, our actual implementations code should always be improved when improvements can be made; hence, The Broken Windows Theory is one we should all strive to live by. This especially holds true in the context of writing tests/specs against existing code. If the code is not up to par in any way – fix it. Ask yourself: “How easy was it for me to understand what this code does?”. “Is it documented in a meaningful way?”. “Would it be easier to understand if I added some quick examples?” (Often, adding examples is simple a matter of pointing to, or annotating the source with the test cases themselves). We can have the greatest, most elegant framework and foundation on which to build the greatest apps in the world, but if we allow ourselves to let our code quality degrade, our apps will gradually decay into chaos. Set a higher standard, and live by it. Leave the source better than you found it.

Meaningful Tests

It is quite easy to get caught up in the perceived quality of a system’s tests simply by measuring it against general Code Coverage metrics. This is a subject I have spoken to at length many times. While code coverage certainly has it’s purpose, and can be helpful, it is often not very reflective of reality. Judge your tests not by the number of test cases or units tested, but rather, judge based on the meaningfulness of each specific test case itself. Ask yourself “What is the overall value of this test?”, “Am I testing the obvious?” (such as a simple getter/setter). Focus on what’s important, test whats of most value first. This will afford one the satisfaction of knowing that if time constraints or something comes up which requires shifting focus to something else, the most important test cases are covered. Focus on what’s important.

Know when you are done

It is quite possible for one to go on refactoring beyond what is essential. As such, it’s important to know when you’re done. Some questions to ask yourself are: “Does the code do what it needs to do?”, “Is the code clean and understandable, performant, efficient, etc.?”. “Does it have adequate coverage?” If these questions can be answered in the affirmative, then you’re most likely done. Many times, it’s tempting to continually refactor; as the more one refactors, the more opportunities for further abstractions begin to arise. When confident that your most important objectives have been met, you’re done. No when to stop.

Concluding Thoughts

It is important to note that the above considerations are by no means exhaustive – and this is intentionally so; as each point is specifically intended to provide just enough guidance to sufficiently ask the right questions, and thus solve problems in a pragmatic manner.

Over the years, I have found that it can be particularly helpful for developers new to a specific domain, or new to TDD/BDD in general, to consider the steps listed above from time to time in a general, summarized form. After doing this regularly, it becomes second nature; engrained in one’s daily development process.

  1. Understand the Problem
  2. Start off slow
  3. Clean up as you go along
  4. Test the unexpected
  5. Leave the test environment the way you found it
  6. Leave the source better than you found it
  7. Focus on what’s important.
  8. No when to stop

Quick Tip: Backbone Collection Validation

Often times I find the native Backbone Collection implementation to be lacking when compared to it’s Backbone.Model counterpart. In particular, Collections generally lack in terms of direct integration with a backend persistence layer, as well as the ability to validate models within the context of the collection as a whole.

Fortunately, such short comings can easily be circumvented due to the extensibility of Backbone’s design as a generalized framework. In fact, throughout my experience utilizing Backbone, I can assert that there has yet to be a problem I have come across which I was unable to easily solve by leveraging one of the many Backbone extensions, or, more often than not, by simply overriding Backbone’s default implementation of a given API.

Validating Collections

Perhaps a common use-case for validating a collection of Models can be found when implementing editors which allow for adding multiple entries of a given form section (implemented as separate Views), whereby each section has a one-to-one correlation with an individual model. Rather than invoke validation on models from each individual view, and manage which model’s are in an invalid state from the context of a composite view, it can be quite useful to simply validate the collection from the composite view which, in turn, results in all models being validated and their associated views updating accordingly.

Assuming live validation is not being utilized, validation is likely to occur when the user submits the form. As such, it becomes necessary to validate each model after their views have updated them as a result of the form being submitted. This can be achieved quite easily by implementing an isValid method on the collection which simply invokes isValid on each model within the collection (or optionally, against specific models within the collection). A basic isValid implementation for a Collection is as follows:

As can be seen in the above example, the Collection’s isValid method simply invokes isValid on it’s models. This causes each model to be re-validated which, in turn, results in any invalid models triggering their corresponding invalidation events, allowing for views to automatically display validation indicators, messages, and the like; particularly when leveraging the Backbone.Validation Plugin.

This example serves well to demonstrate that, while Backbone may not provide everything one could ever ask for “out of the box”, it does provide a design which affords developers the ability to quickly, easily, and effectively extend the native framework as needed.

Function Modules in RequireJS

Having leveraged RequireJS as part of my preferred client-side web stack for some time now, I find it rather surprising how often I recall various features from the API docs which I have yet to use, and how they may apply to a specific solution I am implementing.

One such feature is the ability to define a module as a function. While at first this may seem a rather basic feature, and indeed it is, there are quite a few practical purposes in which returning a function as a module definition can prove useful.

Function Modules

As one may expect, returning a function as a Module in RequireJS is rather straight-forward. For example, a simple random function can be defined as a module as follows:

Then, the function module can simply be invoked as follows:

Function Modules as Factories

Perhaps a more practical example of implementing a function module is in the context of a Factory Method.

For instance, a function module can be used as a convenient means of creating a specific type of object on a clients behalf based on certain conditions.

Take (an intentionally simple) example of a function module which, given a specific role type, returns a corresponding view implementation (in this example, a Backbone View) for an Editor feature:

Client code can then simply invoke the factory in order to retrieve the appropriate Editor view implementation based on the specified role type:

Defining modules as functions which serve as factory methods can help simplify client code implementations, as the responsibility of determining the type of object to create, configure, etc., can be delegated to a dedicated object; thus allowing for simpler designs which better facilitate code reuse, testing, and maintenance.

As a general rule of thumb, I typically reserve implementing modules as functions for cases in which a package level method would be appropriate, or for factory implementations. However, there are other scenarios in which function modules may apply, and so they are certainly worth noting.

You can fork the above example here.

Fluent APIs and Method Chaining

Of the vast catalog of Design Patterns available at our disposal, often times I find it is the simpler, less prominent patterns which are used quite frequently, yet recieve much less recognition; a good example of which being the Method Chaining Pattern.

Method Chaining

The Method Chaining Pattern, as I have come to appreciate it over the years, represents a means of facilitating expressiveness and fluency when used articulately, and mere convenience in it’s less sophisticated use-cases.

Design Considerations

When considering Method Chaining, one should take heed not to simply use the pattern as merely syntactic sugar from which writing fewer lines of code can be achieved; but rather, Method Chaining should be used, perhaps more appropriately, as a means of implementing Fluent APIs which, in turn, allow for writing more concise expressions. By design, such expressions can be written, and thus read, in much the same way as natural language, though they need not be the same from a truly lexical perspective.

The resulting terseness afforded by Method Chaining, while convenient, is in most cases not in-of-itself a reason alone for leveraging the pattern.

Implementation

Method Chaining, when considered purely from an implementation perspective, is perhaps the simplest of all design patterns. It’s basic mandate simply prescribes returning a reference to the object on which a method is being called (in most languages, JavaScript in particular, the this pointer).

Consider the following (intentionally contrived) example:

As can be seen, implementing Method Chaining requires nothing more than simply having methods return a reference to this.

API Simplicity

Method Chaining is typically used when breaking from traditional Command Query Seperation (CQS) principles. The most common example being the merging of both getters (Queries) and setters (Commands). I especially like this technique, as, aside from being very easy to implement, it allows for an API to be used in a more contextual manner from the developers perspective as oppossed to that specified by the API designer’s preconceptions of how the API will be used. For example:

As can be seen, the message method serves as both a getter and setter, allowing users of the API to determine how the method should be invoked based on context, as well as affording developers the convenience of needing only to remember a single method name. This technique is used quite heavily in many JavaScript libraries and has undoubtedly contributed to their success.

We could further expand on this concept by determining a method’s invocation context based on the arguments provided, or the types of specific arguments, thus, in turn, merging various similar methods based on a particular context.

An important design recommendation to consider is that if you are writing an API which violates CQS (which is quite fine IMHO), as always, API consistency is important, thus all getters and setters should be implemented in the same manner.

Fluency

As was mentioned, in most cases, Method Chaining is leveraged to facilitate APIs which are intended to be used fluently (e.g. an Internal DSL). Such implementations typically provide methods which, by themselves, may have little meaning; however, when combined, allow for writing expressions which are self-descibing and make logical sense to users of the API.

For example, consider the way one might describe a Calendrical Event:

Vacation, begins June 21st, ends July 5th, recurs Yearly.

We can easily implement a Fluent API such that the above grammar can be emulated in code as follows:

The same methods can also be chained in different combinations, yet yield the same value:

Given the above example, we could further improve on the fluency of the implementation by adding intermediate methods which can, by themselves, simply serve to aid in readability, or, provide an alternate modifier for chaining:

When implementing Fluent APIs, we can design such that different logical chaining combinations can yield the same result, thus affording users of the API the convenience of determining the most appropriate expressions based on context or personal preference, even grammatically so. Illogical chaining combinations can be handled by either throwing an exception, or they can simply be ignored based on the context of a preceding invocation – though, of course, one should aim to avoid designs which allow for illogical chaining.

The Ubiquitous Example – jQuery

While Method Chaining and Fluent APIs, as with most design patterns, are language agnostic, in the JavaScript world perhaps the most well known implementation is the jQuery API; for example:

In addition to jQuery, there are numerous additional JavaScript Method Chaining and Fluent APIs of note, Jasmine in particular has a very expressive API which aligns excellently with it’s design goals. The various libraries which implement the Promises/A spec also provide very clear and concise Fluent APIs.

Concluding Thoughts

Over the years I have leveraged Method Chaining to facilitate the design of Fluent APIs for various use-cases. The two patterns, when combined, can be especially useful when designing Internal DSLs; either third-party libraries, or APIs specific to a particular business domain.