├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .prettierignore ├── .prettierrc ├── .tool-versions ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── abstract-factory-pattern ├── README.md ├── mysql.factory.ts ├── postgresql.factory.ts ├── types.ts └── usage.ts ├── adapter-pattern ├── README.md ├── stripe.adapter.ts ├── stripe.service.ts ├── types.ts └── usage.ts ├── bridge-pattern ├── README.md ├── renderer.bridge.ts ├── types.ts └── usage.ts ├── builder-pattern ├── README.md ├── query.builder.ts ├── types.ts └── usage.ts ├── chain-of-responsibility-pattern ├── README.md ├── handlers.ts ├── server.ts ├── types.ts └── usage.ts ├── command-pattern ├── README.md ├── command.ts ├── notes.txt ├── textDocument.ts ├── textEditor.ts ├── types.ts └── usage.ts ├── composite-pattern ├── README.md ├── directory.composite.ts ├── file.leaf.ts ├── types.ts └── usage.ts ├── decorator-pattern ├── README.md ├── measure.decorator.ts └── usage.ts ├── deno.json ├── deno.lock ├── facade-pattern ├── README.md ├── apiClient.ts ├── paymentValidator.ts ├── retryHandler.ts ├── stripe.facade.ts ├── stripeLogger.ts ├── types.ts └── usage.ts ├── factory-pattern ├── README.md ├── beverage.factory.ts ├── types.ts └── usage.ts ├── flyweight-pattern ├── README.md ├── connectionPoolManager.factory.ts ├── dbConnection.flyweight.ts ├── types.ts └── usage.ts ├── iterator-pattern ├── README.md ├── example1 │ ├── menu.ts │ ├── menuIterator.ts │ └── types.ts └── usage.ts ├── main.ts ├── main_test.ts ├── mediator-pattern ├── README.md ├── chatRoom.ts ├── chatRoomUser.ts ├── mediator.ts ├── types.ts └── usage.ts ├── observer-pattern ├── README.md ├── example1 │ ├── observer.ts │ └── subject.ts ├── example2 │ ├── observers.ts │ ├── subject.ts │ └── types.ts ├── types.ts └── usage.ts ├── singleton-pattern ├── README.md ├── database.singleton.ts ├── store.singleton.ts ├── store2.singleton.ts ├── usage.ts ├── user-store.ts └── user-store2.ts ├── strategy-pattern ├── README.md ├── logger.context.ts ├── loggers.strategies.ts ├── types.ts └── usage.ts └── template-pattern ├── README.md ├── example1 ├── DOMrenderer.ts ├── UserComponent.ts ├── baseComponent.ts └── types.ts └── usage.ts /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "arrowParens": "avoid", 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | deno 2.1.4 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # design-patterns-using-typescript 2 | 3 | This repo tries to explain common design patterns using examples coded in Typescript. 4 | Instead of abstract examples, you'll find implementations that mirror real-world tools you already use. 5 | E.g, to see how Redux does state management? Check out the [Singleton pattern example](singleton-pattern/store.singleton.ts) that emulates a Redux store. Curious how Express.js implements its middleware using the Chain of Responsibility Pattern? Check out this [CoR example](chain-of-responsibility-pattern/server.ts) 6 | 7 | ## Getting Started 8 | 9 | Run the `pattern` deno task to execute any of the patterns from the files. 10 | 11 | `deno task pattern abstract-factory-pattern` 12 | 13 | _if you don't have `deno` installed, refer to this [installation guide](https://docs.deno.com/runtime/getting_started/installation/)_ 14 | 15 | ## Patterns 16 | 17 | ### 1. Creational: How objects are created 18 | 19 | | Pattern Name | Description | 20 | |-------------|-------------| 21 | | **[Factory Pattern](factory-pattern/README.md)** | Centralizes object creation, hiding the complexity of instantiation logic. For example `createElement()` in React creates different types of DOM elements based on the tag name you provide. This pattern is useful when object creation involves complex logic that should be isolated from the rest of the application. | 22 | | **[Abstract Factory Pattern](abstract-factory-pattern/README.md)** | Provides an interface for creating families of related objects without specifying their concrete classes. For example, this pattern would be useful to build a data access layer for an application. This layer must support multiple databases like PostgreSQL and MySQL. | 23 | | **[Builder Pattern](builder-pattern/README.md)** | Enables the construction of complex objects through a step-by-step approach, where each step can be customized independently. Query builders in ORMs use this pattern to chain methods like `.select()`, `.where()`, and `.orderBy()` to construct complex database queries (knex, drizzle). | 24 | | **[Singleton Pattern](singleton-pattern/README.md)** | Ensures a class has only one instance throughout the application's lifecycle while providing global access to that instance. Frontend stores (redux), database connections, and logging services often use this pattern to maintain a single source of truth. | 25 | 26 | ### 2. Structural: How objects are laid out in relation to each other 27 | 28 | | Pattern Name | Description | 29 | |-------------|-------------| 30 | | **[Adapter Pattern](adapter-pattern/README.md)** | Allows incompatible interfaces to work together by wrapping an object in an adapter that makes it compatible with another interface. Similar to how libraries like Axios provide a consistent API across different environments (browser's fetch API, Node's http module) by adapting their specific implementations to a common interface. | 31 | | **[Bridge Pattern](bridge-pattern/README.md)** | Separates an abstraction from its implementation, allowing them to vary independently. Consider how a cross-platform UI framework (react-native) might separate the abstract definition of a component (Button, Input) from its platform-specific implementation (Web, Mobile, Desktop). This allows both the component API and the platform implementations to evolve separately. | 32 | | **[Composite Pattern](composite-pattern/README.md)** | Creates tree-like structures where individual objects and compositions of objects can be treated uniformly. Think of it like a tree structure where both leaves (single objects) and branches (groups of objects) can be handled the same way. This is helpful when building systems with hierarchical data, like UI components or file systems. | 33 | | **[Decorator Pattern](decorator-pattern/README.md)** | Allows behavior to be added to individual objects dynamically without affecting other objects of the same class. Examples include Express.js middleware (you can wrap your route handlers with additional behaviors like authentication, logging, or error handling) and TypeScript decorators (@injectable, @observable) for enhancing classes with additional functionality. | 34 | | **[Facade Pattern](facade-pattern/README.md)** | Provides a simplified interface to a complex subsystem. Consider how the fetch API provides a clean interface that hides the complexity of making HTTP requests, or how ORMs like Sequelize provide a straightforward interface for database operations while hiding the complexity of SQL queries and connection management. This pattern is essential when you want to provide a simple API for a complex system. | 35 | | **[Flyweight Pattern](flyweight-pattern/README.md)** | Helps reduce memory usage by sharing common data among multiple objects. Instead of creating separate instances for every object, it reuses existing ones when possible. Game engines use this pattern to optimize memory usage by sharing graphical assets, textures, and object instances. | 36 | 37 | 38 | ### 3. Behavioral: How objects communicate with each other 39 | 40 | 41 | | Pattern Name | Description | 42 | |-------------|-------------| 43 | | **[Strategy Pattern](strategy-pattern/README.md)** | Defines a family of algorithms and makes them interchangeable. Payment processing systems often use this pattern to switch between different payment methods (credit card, PayPal, crypto), each with its own processing algorithm but sharing a common interface. | 44 | | **[Template Pattern](template-pattern/README.md)** | Provides a way to define the skeleton of an algorithm in a base class and allows subclasses to override specific steps. Used to provide framework, library users with a simple means of extending standard functionality using inheritance. Examples - File processing where base class handles opening/closing files while subclasses implement specific parsing (CSV, XML, JSON). Test frameworks where base test classes provide setup/teardown methods that individual test cases extend | 45 | | **[Observer Pattern](observer-pattern/README.md)** | Establishes a one-to-many relationship between objects, where multiple observers are notified automatically of any state changes in the subject they're observing. React's useState and Redux's subscribe mechanism implement this pattern, allowing components to automatically update when state changes. Event handling systems in Node.js also use this pattern with EventEmitter. | 46 | | **[Mediator Pattern](mediator-pattern/README.md)** | Reduces direct connections between components by making them communicate through a central mediator. Chat applications use this pattern to handle message routing between users without them needing to know about each other directly. State management via a store in most frontend applications uses this pattern. | 47 | | **[Chain of Responsibility Pattern](chain-of-responsibility-pattern/README.md)** | Passes requests along a chain of handlers until one handles the request. Examples include Express.js middleware chain where each middleware function can either handle the request, modify it and pass it on, or trigger an error. | 48 | | **[Command Pattern](command-pattern/README.md)** | Encapsulates a request as an object, allowing you to parameterize clients with different requests, queue requests, and support undoable operations. Text editors use this pattern to implement undo/redo functionality by encapsulating each edit as a command object. Task queuing systems also use this pattern to package tasks with their parameters for delayed execution. | 49 | | **[Iterator Pattern](iterator-pattern/README.md)** | Provides a way to access elements of a collection sequentially without exposing its underlying representation. JavaScript's built-in iterators (used with for...of loops) demonstrate this pattern. GraphQL clients use this pattern for pagination, allowing you to traverse large datasets piece by piece without loading everything at once. | 50 | 51 | 52 | 53 | ### References 54 | 55 | 1. https://refactoring.guru/design-patterns 56 | 2. https://www.freecodecamp.org/news/javascript-design-patterns-explained 57 | 3. https://www.patterns.dev 58 | 4. [10 Design Patterns Explained - fireship.io](https://www.youtube.com/watch?v=tv-_1er1mWI) 59 | 5. _and some LLM help to tidy up things_ 60 | -------------------------------------------------------------------------------- /abstract-factory-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Abstract Factory Pattern 2 | 3 | Builds on the factory pattern. 4 | 5 | It is creational design pattern that provides an interface for creating families 6 | of related or dependent objects without specifying their concrete classes. 7 | 8 | ## Key Concepts 9 | 10 | 1. **Abstract Factory Interface**: Declares methods for creating a set of 11 | related or dependent objects. 12 | 2. **Concrete Factories**: Implement the Abstract Factory interface to produce 13 | family-specific objects. 14 | 3. **Abstract Products**: Declare interfaces for different product types. 15 | 4. **Concrete Products**: Implement the Abstract Product interfaces. 16 | 5. **Client Code**: Uses the Abstract Factory and Abstract Products. It does not 17 | know the specifics of the Concrete Factories or Products, enabling easy 18 | extension and scalability. 19 | 20 | #### How is it different than Factory Pattern? 21 | 22 | Factory pattern creates objects of one product type while Abstract Factory 23 | Pattern creates families of related objects of different product types. 24 | 25 | > Family: A family refers to a group of related products (e.g., PostgreSQL's 26 | > connection and CRUD operations or MongoDB's connection and CRUD operations). 27 | 28 | > Product: A product is an individual entity in the family (e.g., 29 | > DatabaseConnection or CRUDOperations). 30 | 31 | ## Use Cases 32 | 33 | 1. Suppose you are building a data access layer for an application. This layer must 34 | support multiple databases like PostgreSQL and MySQL. The layer needs to handle 35 | db connection and CRUD operators. 36 | 2. In UI development, this pattern allows you to create entire sets of components (buttons, inputs, modals) that share a consistent theme or style. 37 | 38 | ## Further Reading 39 | 40 | https://refactoring.guru/design-patterns/abstract-factory 41 | -------------------------------------------------------------------------------- /abstract-factory-pattern/mysql.factory.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseConnection, DatabaseFactory, DatabaseOperations, TData, TResource } from "./types.ts"; 2 | 3 | // -- Product Classes -- // 4 | 5 | class MysqlConnection implements DatabaseConnection { 6 | connect(): void { 7 | console.log("Connection to Mysql has been established"); 8 | } 9 | } 10 | 11 | class MysqlOperations implements DatabaseOperations { 12 | create(data: TData): TResource { 13 | console.log("Creating item with", data); 14 | const item: TResource = {}; 15 | return item; 16 | } 17 | 18 | read(query: TData): TResource { 19 | console.log("Reading item with query", query); 20 | const item: TResource = {}; 21 | return item; 22 | } 23 | 24 | update(id: string, data: TData): TResource { 25 | console.log(`Updating item with ${id} and data ${JSON.stringify(data)}`); 26 | const item: TResource = {}; 27 | return item; 28 | } 29 | 30 | delete(id: string): void { 31 | console.log(`Deleting item with ${id}`); 32 | } 33 | } 34 | 35 | // -- Factory for MySQL -- // 36 | export class MysqlFactory implements DatabaseFactory { 37 | connection() { 38 | return new MysqlConnection(); 39 | } 40 | operations() { 41 | return new MysqlOperations(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /abstract-factory-pattern/postgresql.factory.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseConnection, DatabaseFactory, DatabaseOperations, TData, TResource } from "./types.ts"; 2 | 3 | // -- Product Classes -- // 4 | 5 | class PostgresConnection implements DatabaseConnection { 6 | connect(): void { 7 | console.log("Connection to Postgres has been established"); 8 | } 9 | } 10 | 11 | class PostgresOperations implements DatabaseOperations { 12 | create(data: TData): TResource { 13 | console.log("Creating item with", data); 14 | const item: TResource = {}; 15 | return item; 16 | } 17 | 18 | read(query: TData): TResource { 19 | console.log("Reading item with query", query); 20 | const item: TResource = {}; 21 | return item; 22 | } 23 | 24 | update(id: string, data: TData): TResource { 25 | console.log(`Updating item with ${id} and data ${JSON.stringify(data)}`); 26 | const item: TResource = {}; 27 | return item; 28 | } 29 | 30 | delete(id: string): void { 31 | console.log(`Deleting item with ${id}`); 32 | } 33 | } 34 | 35 | // -- Factory for Postgres -- // 36 | 37 | export class PostgresFactory implements DatabaseFactory { 38 | connection() { 39 | return new PostgresConnection(); 40 | } 41 | operations() { 42 | return new PostgresOperations(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /abstract-factory-pattern/types.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore no-explicit-any 2 | export type TData = Record; 3 | // deno-lint-ignore no-explicit-any 4 | export type TResource = Record; 5 | 6 | // -- Product Interfaces -- // 7 | 8 | export interface DatabaseConnection { 9 | connect(): void; 10 | } 11 | 12 | export interface DatabaseOperations { 13 | read(query: TData): TResource; 14 | delete(id: string): void; 15 | update(id: string, data: TData): TResource; 16 | create(data: TData): TResource; 17 | } 18 | 19 | // -- Abtrast Factory Interface -- // 20 | 21 | export interface DatabaseFactory { 22 | connection(): DatabaseConnection; 23 | operations(): DatabaseOperations; 24 | } 25 | -------------------------------------------------------------------------------- /abstract-factory-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { MysqlFactory } from "./mysql.factory.ts"; 2 | import { PostgresFactory } from "./postgresql.factory.ts"; 3 | import { DatabaseConnection, DatabaseFactory, DatabaseOperations, TData } from "./types.ts"; 4 | 5 | class DataAccessLayer { 6 | private conn: DatabaseConnection; 7 | private op: DatabaseOperations; 8 | 9 | constructor(factory: DatabaseFactory) { 10 | this.conn = factory.connection(); 11 | this.op = factory.operations(); 12 | } 13 | init() { 14 | this.conn.connect(); 15 | } 16 | create(data: TData) { 17 | return this.op.create(data); 18 | } 19 | update(id: string, data: TData) { 20 | return this.op.update(id, data); 21 | } 22 | read(query: TData) { 23 | return this.op.read(query); 24 | } 25 | delete(id: string) { 26 | return this.op.delete(id); 27 | } 28 | } 29 | 30 | const databaseType = "mysql"; 31 | 32 | const dal = 33 | databaseType === "mysql" ? new DataAccessLayer(new MysqlFactory()) : new DataAccessLayer(new PostgresFactory()); 34 | 35 | dal.init(); 36 | 37 | dal.create({ name: "Ohn", age: 23 }); 38 | dal.read({ name: "Ohn" }); 39 | dal.update("gsd23gv", { age: 24 }); 40 | dal.delete("gsd23gv"); 41 | -------------------------------------------------------------------------------- /adapter-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Adapter Pattern 2 | 3 | The Adapter Pattern is a structural design pattern used to make two incompatible 4 | interfaces or objects work together. An adapter wraps one of the objects to hide 5 | the complexity of conversion happening behind the scenes. The wrapped object 6 | isn’t aware of the adapter. 7 | 8 | Think of an adapter as a plug converter that allows a device with a US plug to 9 | work with a European socket. Popular Usage in Software 10 | 11 | ## Use Cases 12 | 13 | 1. **Libraries or SDKs**: When you use a library that doesn't match your 14 | application's data structure or format, an adapter can transform your data to 15 | fit the library's expectations. 16 | 2. **Databases**: An adapter can convert raw database query results into a format 17 | your application can use easily. 18 | 3. **Frontend**: An adapter can normalize API responses to fit the model 19 | used in your UI components. 20 | 21 | ## Further Reading 22 | 23 | https://refactoring.guru/design-patterns/adapter 24 | -------------------------------------------------------------------------------- /adapter-pattern/stripe.adapter.ts: -------------------------------------------------------------------------------- 1 | import { IPaymentProcessor, IStripe } from "./types.ts"; 2 | 3 | export class StripeAdapter implements IPaymentProcessor { 4 | constructor(private stripeService: IStripe) { 5 | this.stripeService = stripeService; 6 | } 7 | async processPayment( 8 | cc: string, 9 | amount: number, 10 | name: string 11 | ): Promise<{ transcationId: string; paid: boolean; timestamp: string }> { 12 | const transactionInfo = await this.stripeService.chargeCard(cc, amount); 13 | console.log(`transaction ${transactionInfo.chargeId} processed with status ${transactionInfo.status} for ${name}`); 14 | return { 15 | transcationId: transactionInfo.chargeId, 16 | paid: transactionInfo.status == "success", 17 | timestamp: transactionInfo.createdAt, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /adapter-pattern/stripe.service.ts: -------------------------------------------------------------------------------- 1 | import { IStripe } from "./types.ts"; 2 | 3 | export class Stripe implements IStripe { 4 | private appId: string; 5 | constructor(appId: string) { 6 | this.appId = appId; 7 | } 8 | chargeCard( 9 | cc: string, 10 | amount: number 11 | ): Promise<{ 12 | status: "success" | "failed" | "pending"; 13 | chargeId: string; 14 | createdAt: string; 15 | }> { 16 | console.log(`trying to charge card ${cc} for amount ${amount} for app ${this.appId}`); 17 | return new Promise(resolve => 18 | setTimeout(() => { 19 | if (amount > 100) { 20 | resolve({ 21 | status: "success", 22 | chargeId: crypto.randomUUID(), 23 | createdAt: Date.now.toString(), 24 | }); 25 | } else { 26 | resolve({ 27 | status: "failed", 28 | chargeId: "", 29 | createdAt: Date.now.toString(), 30 | }); 31 | } 32 | }) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /adapter-pattern/types.ts: -------------------------------------------------------------------------------- 1 | // application's expected interface for payment processor 2 | export interface IPaymentProcessor { 3 | processPayment( 4 | cc: string, 5 | amount: number, 6 | name: string 7 | ): Promise<{ transcationId: string; paid: boolean; timestamp: string }>; 8 | } 9 | 10 | // external service (stripe) interface 11 | export interface IStripe { 12 | chargeCard( 13 | cc: string, 14 | amount: number 15 | ): Promise<{ status: "success" | "failed" | "pending"; chargeId: string; createdAt: string }>; 16 | } 17 | -------------------------------------------------------------------------------- /adapter-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { StripeAdapter } from "./stripe.adapter.ts"; 2 | import { Stripe } from "./stripe.service.ts"; 3 | import { IPaymentProcessor } from "./types.ts"; 4 | 5 | const stripeService = new Stripe("myapp-4fjdnf33"); 6 | 7 | const paymentAdapter = new StripeAdapter(stripeService); 8 | 9 | async function processOrderPayment(processor: IPaymentProcessor, data: { cc: string; amount: number; name: string }) { 10 | if (!data.amount) return; 11 | console.log("processing payment for " + data.name); 12 | await processor.processPayment(data.cc, data.amount, data.name); 13 | } 14 | processOrderPayment(paymentAdapter, { 15 | cc: "4032-4953-4313-1234", 16 | amount: 1000, 17 | name: "Meg", 18 | }); 19 | 20 | processOrderPayment(paymentAdapter, { 21 | cc: "5143-4953-4313-1234", 22 | amount: 0.99, 23 | name: "Jim", 24 | }); 25 | 26 | /** 27 | * 28 | processing payment for Meg 29 | trying to charge card 4032-4953-4313-1234 for amount 1000 for app myapp-4fjdnf33 30 | processing payment for Jim 31 | trying to charge card 5143-4953-4313-1234 for amount 0.99 for app myapp-4fjdnf33 32 | transaction 7287094f-e89f-4c37-a9c8-1c32259b665c processed with status success for Meg 33 | transaction processed with status failed for Jim 34 | */ 35 | -------------------------------------------------------------------------------- /bridge-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Bridge Pattern 2 | 3 | The Bridge Pattern is a structural design pattern that separates an abstraction from its implementation, allowing the two to vary independently. It’s like building a bridge between two things so they can work together without being tightly coupled. 4 | 5 | ## Key Concepts 6 | 7 | 1. **Abstraction**: The high-level interface or abstract class. Defines what needs to be done and delegates the implementation to the Implementor. 8 | 9 | 2. **Concrete Abstraction**: A concrete version of the Abstraction. Extends or customizes the high-level behavior. 10 | 11 | 3. **Implementor (Bridge)**: the interface for low-level operations. Defines how the tasks are performed without knowing about the Abstraction. 12 | 13 | 4. **Concrete Implementor**: A specific implementation of the Implementor. Provides platform-specific or detailed behavior. 14 | 15 | 5. **Client**: Interacts with the Abstraction. Is decoupled from the details of the Implementor 16 | 17 | ## Use Cases 18 | 19 | 1. **JDBC**: Database operations (abstraction) separated from specific database drivers (implementation) 20 | 2. **React Native**: UI components (abstraction) with platform-specific renderers (implementation) 21 | 3. **Graphics libraries**: Drawing operations (abstraction) separated from device-specific rendering (implementation) 22 | 23 | This pattern is particularly useful when you need to support multiple platforms or implementations while maintaining a consistent API. 24 | 25 | ## Further Reading 26 | 27 | https://refactoring.guru/design-patterns/bridge 28 | 29 | ### Difference Between Bridge and Adapter 30 | 31 | Bridge focuses on separating abstraction from implementation to allow both to evolve independently. It's used when you anticipate multiple variations of both abstractions and implementations. For example, React Native uses bridge pattern to maintain consistent component APIs while allowing platform-specific rendering implementations. 32 | 33 | Adapter focuses on making existing incompatible interfaces work together. It's used when integrating legacy systems or third-party components. For example, database ORMs often use adapters to make different database APIs conform to a single interface, or how polyfills adapt modern JavaScript APIs to work in older browsers. 34 | -------------------------------------------------------------------------------- /bridge-pattern/renderer.bridge.ts: -------------------------------------------------------------------------------- 1 | import { IRenderingEngine } from "./types.ts"; 2 | 3 | // concrete implementation / bridge 4 | export class WebRenderingEngine implements IRenderingEngine { 5 | renderElement(element: string, props: Record): void { 6 | console.log(`${element} rendered to web with props ${JSON.stringify(props)}`); 7 | } 8 | } 9 | 10 | // concrete implemenation / bridge 11 | export class AndroidRenderingEngine implements IRenderingEngine { 12 | renderElement(element: string, props: Record): void { 13 | console.log(`${element} rendered to android with props ${JSON.stringify(props)}`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bridge-pattern/types.ts: -------------------------------------------------------------------------------- 1 | // abstraction 2 | export interface IComponent { 3 | name: string; 4 | render(props: Record): void; 5 | } 6 | 7 | // implementor / bridge interface 8 | export interface IRenderingEngine { 9 | renderElement(element: string, props: Record): void; 10 | } 11 | -------------------------------------------------------------------------------- /bridge-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | // emulates how modern UI frameworks handle platform-specific rendering. 2 | // The abstraction (UI components) remains consistent while the implementation (rendering logic) varies by platform. 3 | 4 | import { WebRenderingEngine } from "./renderer.bridge.ts"; 5 | import { IComponent, IRenderingEngine } from "./types.ts"; 6 | 7 | // concrete abstraction 8 | class Button implements IComponent { 9 | name: string; 10 | private engine: IRenderingEngine; 11 | constructor(engine: IRenderingEngine) { 12 | this.name = "button"; 13 | this.engine = engine; 14 | } 15 | render(props: Record): void { 16 | this.engine.renderElement(this.name, props); 17 | } 18 | } 19 | 20 | // usage 21 | const engine = new WebRenderingEngine(); 22 | const button = new Button(engine); 23 | button.render({ color: "blue", size: "small" }); 24 | -------------------------------------------------------------------------------- /builder-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Builder Pattern 2 | 3 | It is a creational design pattern that is used to construct complex objects 4 | step-by-step. It helps in creating different representations of an object with 5 | numerous configuration options. 6 | 7 | ## Use Cases 8 | 9 | you’re building an application where users have highly configurable profiles. A 10 | user profile may include optional attributes like contact information, roles, 11 | permissions, preferences, etc. It makes for better design to use builder pattern 12 | to create a user with specific profile instead of having muliple contructors in 13 | a class or optional params to create one. 14 | 15 | You need to build an API client builder that lets you configure multiple options like base URL, headers, timeout, and authentication separately before creating the final client instance. 16 | 17 | Another example would be a SQL query builder where one needs to use multiple 18 | clauses (WHERE, SELECT, GROUP BY, etc). This pattern provides a structured way 19 | to construct these queries in a type-safe, extendable, and reusable manner. One 20 | such example is this Query Builder from Knex 21 | https://github.com/knex/knex/blob/master/lib/query/querybuilder.js#L329 22 | 23 | ## Further Reading 24 | 25 | https://refactoring.guru/design-patterns/builder 26 | -------------------------------------------------------------------------------- /builder-pattern/query.builder.ts: -------------------------------------------------------------------------------- 1 | import { Query } from "./types.ts"; 2 | 3 | export class QueryBuilder { 4 | private query: Partial = {}; 5 | 6 | select(columns: string) { 7 | this.query.select = this._getEntities(columns); 8 | return this; 9 | } 10 | 11 | from(tables: string) { 12 | this.query.from = this._getEntities(tables); 13 | return this; 14 | } 15 | groupBy(columns: string) { 16 | this.query.groupBy = this._getEntities(columns); 17 | return this; 18 | } 19 | orderBy(columns: string) { 20 | this.query.orderBy = this._getEntities(columns); 21 | return this; 22 | } 23 | join(table: string, condition: string) { 24 | if (!this.query.joins) this.query.joins = []; 25 | this.query.joins.push(`JOIN ${table} ON ${condition}`); 26 | return this; 27 | } 28 | where(condition: string) { 29 | if (!this.query.where) this.query.where = []; 30 | this.query.where.push(condition); 31 | return this; 32 | } 33 | private _getEntities(entities: string) { 34 | if (entities.length == 0) throw new Error("bad input"); 35 | return entities.split(", "); 36 | } 37 | build() { 38 | if (!this.query.select || !this.query.from) { 39 | throw new Error("SELECT and FROM clauses are mandatory."); 40 | } 41 | 42 | const parts = [`SELECT ${this.query.select.join(", ")}`, `FROM ${this.query.from.join(", ")}`]; 43 | if (this.query.joins?.length) parts.push(this.query.joins.join(" ")); 44 | if (this.query.where?.length) { 45 | parts.push("WHERE " + this.query.where.join(" AND ")); 46 | } 47 | if (this.query.groupBy?.length) { 48 | parts.push("GROUP BY " + this.query.groupBy.join(", ")); 49 | } 50 | if (this.query.orderBy?.length) { 51 | parts.push("ORDER BY " + this.query.orderBy.join(", ")); 52 | } 53 | return parts.join("\n"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /builder-pattern/types.ts: -------------------------------------------------------------------------------- 1 | export type Query = { 2 | select: string[]; 3 | from: string[]; 4 | where?: string[]; 5 | joins?: string[]; 6 | orderBy?: string[]; 7 | groupBy?: string[]; 8 | }; 9 | -------------------------------------------------------------------------------- /builder-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { QueryBuilder } from "./query.builder.ts"; 2 | 3 | const queryOne: string = new QueryBuilder() 4 | .select("users.name, COUNT(purchases.id) AS total_purchases") 5 | .from("users, purchases") 6 | .where("users.id = purchases.user_id") 7 | .groupBy("users.name") 8 | .build(); 9 | 10 | console.log(queryOne); 11 | 12 | console.log("...\n"); 13 | 14 | const queryTwo: string = new QueryBuilder() 15 | .select("users.name, users.email") 16 | .from("users") 17 | .join("orders", "users.id = orders.user_id") 18 | .where("orders.count > 100") 19 | .where("users.location = 'USA'") 20 | .groupBy("users.name, users.email") 21 | .orderBy("users.name") 22 | .build(); 23 | 24 | console.log(queryTwo); 25 | 26 | /** 27 | 28 | SELECT users.name, COUNT(purchases.id) AS total_purchases 29 | FROM users, purchases 30 | WHERE users.id = purchases.user_id 31 | GROUP BY users.name 32 | ... 33 | 34 | SELECT users.name, users.email 35 | FROM users 36 | JOIN orders ON users.id = orders.user_id 37 | WHERE orders.count > 100 AND users.location = 'USA' 38 | GROUP BY users.name, users.email 39 | ORDER BY users.name 40 | 41 | */ 42 | -------------------------------------------------------------------------------- /chain-of-responsibility-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Chain of Responsibility Pattern 2 | 3 | The Chain of Responsibility (CoR) pattern is a behavioral design pattern that allows multiple objects to process a request without explicitly defining which object will handle it. The request is passed along a chain of handlers, and each handler can either process the request or pass it to the next handler. This pattern decouples senders from receivers, making the system more flexible and scalable as you can easily add or remove handlers without changing the client code. 4 | 5 | ## Key Concepts 6 | 7 | 1. **Handler Interface**: Defines a method for handling requests and setting the next handler. 8 | 2. **Concrete Handlers**: Implement specific logic to decide whether to process or forward a request. 9 | 3. **Chaining Mechanism**: Client builds the chain and initiates the request. Handlers are linked in a specific order to form a processing pipeline. 10 | 11 | ## Use Cases 12 | 13 | 1. HTTP Middleware (Express.js, NestJS): Authentication, authorization, validation, and logging. 14 | 15 | 2. UI Event Handling: Event propagation in DOM (bubbling/capturing). 16 | 17 | 3. Payment processing (e.g., validating card details, checking fraud, processing transactions). 18 | 19 | ## Further Reading 20 | 21 | https://refactoring.guru/design-patterns/chain-of-responsibility 22 | -------------------------------------------------------------------------------- /chain-of-responsibility-pattern/handlers.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, HttpResponse, MiddlewareHandler } from "./types.ts"; 2 | 3 | // Concrete Handlers 4 | 5 | export class AuthMiddleware implements MiddlewareHandler { 6 | process(req: HttpRequest, res: HttpResponse, next: () => void): void { 7 | const apiKey = req.headers["X-API-KEY"] || req.headers["X-API-KEY".toLocaleLowerCase()]; 8 | 9 | // mocking authentication 10 | if (apiKey != "SECRET_KEY_231") { 11 | (res.status = 401), (res.body = "Unauthorised: Invalid API Key"); 12 | return; 13 | } 14 | 15 | console.log("Authentication Passed"); 16 | next(); // onto next middleware in the chain 17 | } 18 | } 19 | 20 | export class RateLimitMiddleware implements MiddlewareHandler { 21 | private requestCountPerIP: Map; 22 | private MAX_REQUESTS = 5; 23 | constructor() { 24 | this.requestCountPerIP = new Map(); 25 | } 26 | process(req: HttpRequest, res: HttpResponse, next: () => void): void { 27 | const requestsCount = (this.requestCountPerIP.get(req.ip) || 0) + 1; 28 | if (requestsCount >= this.MAX_REQUESTS) { 29 | (res.status = 429), (res.body = "Too Many Requests"); 30 | return; 31 | } 32 | this.requestCountPerIP.set(req.ip, requestsCount); 33 | console.log(`Rate limit: ${requestsCount}/${this.MAX_REQUESTS}`); 34 | next(); 35 | } 36 | } 37 | 38 | export class LoggerMiddlerware implements MiddlewareHandler { 39 | process(req: HttpRequest, res: HttpResponse, next: () => void): void { 40 | console.log(`Request | ${req.method} | ${req.url} | ${req.ip}`); 41 | next(); 42 | console.log(`Response | ${res.status}`); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /chain-of-responsibility-pattern/server.ts: -------------------------------------------------------------------------------- 1 | import { HttpRequest, HttpResponse, MiddlewareHandler } from "./types.ts"; 2 | 3 | export class Server { 4 | private middlewares: MiddlewareHandler[]; 5 | constructor() { 6 | this.middlewares = []; 7 | } 8 | use(middleware: MiddlewareHandler) { 9 | this.middlewares.push(middleware); 10 | } 11 | handleRequest(req: HttpRequest) { 12 | const res: HttpResponse = { status: 200, body: "OK" }; 13 | 14 | // Chain middlewares using recursive closure 15 | const runMiddleware = (index: number) => { 16 | if (index >= this.middlewares.length) return; 17 | 18 | const middleware = this.middlewares[index]; 19 | middleware.process(req, res, () => runMiddleware(index + 1)); 20 | }; 21 | 22 | runMiddleware(0); 23 | return res; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chain-of-responsibility-pattern/types.ts: -------------------------------------------------------------------------------- 1 | export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; 2 | 3 | export type HttpRequest = { 4 | url: string; 5 | method: HttpMethod; 6 | headers: Record; 7 | ip: string; 8 | body?: string; 9 | }; 10 | 11 | export type HttpResponse = { 12 | status: number; 13 | body: string; 14 | }; 15 | 16 | // Handler Interface 17 | export interface MiddlewareHandler { 18 | process(req: HttpRequest, res: HttpResponse, next: () => void): void; 19 | } 20 | -------------------------------------------------------------------------------- /chain-of-responsibility-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | // Emulating Expressjs 2 | 3 | import { AuthMiddleware, LoggerMiddlerware, RateLimitMiddleware } from "./handlers.ts"; 4 | import { Server } from "./server.ts"; 5 | import { HttpRequest } from "./types.ts"; 6 | 7 | const server = new Server(); 8 | 9 | server.use(new LoggerMiddlerware()); 10 | server.use(new RateLimitMiddleware()); 11 | server.use(new AuthMiddleware()); 12 | 13 | // Simulate requests 14 | const validRequest: HttpRequest = { 15 | url: "/api/v3/posts", 16 | method: "GET", 17 | headers: { "x-api-key": "SECRET_KEY_231" }, 18 | ip: "192.168.1.1", 19 | }; 20 | 21 | const blockedRequest: HttpRequest = { 22 | url: "/api/v3/posts", 23 | method: "GET", 24 | headers: {}, // Missing API key 25 | ip: "192.168.1.1", 26 | }; 27 | 28 | console.log("--- Valid Request ---"); 29 | console.log(server.handleRequest(validRequest)); 30 | 31 | console.log("\n--- Blocked Request ---"); 32 | console.log(server.handleRequest(blockedRequest)); 33 | 34 | /** 35 | 36 | --- Valid Request --- 37 | Request | GET | /api/v3/posts | 192.168.1.1 38 | Rate limit: 1/5 39 | Authentication Passed 40 | Response | 200 41 | { status: 200, body: "OK" } 42 | 43 | --- Blocked Request --- 44 | Request | GET | /api/v3/posts | 192.168.1.1 45 | Rate limit: 2/5 46 | Response | 401 47 | { status: 401, body: "Unauthorised: Invalid API Key" } 48 | 49 | */ 50 | -------------------------------------------------------------------------------- /command-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Command Pattern 2 | 3 | It is a behavioral design pattern that turns a request into an object. This transformation lets you pass requests as a method arguments, delay or queue a request’s execution, and support undoable operations. It decouples the sender (who wants something done) from the receiver (who actually performs the action). 4 | 5 | Used when you want to queue operations, schedule their execution, or execute them remotely. A command can be serialized, which means converting it to a string that can be easily written to a file or a database. You can delay and schedule command execution. you can put them in queue, log or send them over the network. 6 | 7 | Used when you want to implement undoable actions, e.g text editor. you turn your operations into a command and then push them in a stack which you can pop out to replay previous commands. 8 | 9 | ## Key Concepts 10 | 11 | 1. **Command Interface** – Defines the contract for executing an operation. 12 | 2. **Concrete Commands** – Implement the command interface and define actions. 13 | 3. **Invoker** – Stores and invokes commands when needed. 14 | 4. **Receiver** – The actual entity that performs the task. 15 | 16 | ## Use Cases 17 | 18 | 1. Handling undo/redo actions in Text Editor. 19 | 2. Managing player actions in Game Engine. 20 | 3. Task Scheduling via queues. 21 | 22 | ## Further Reading 23 | 24 | https://refactoring.guru/design-patterns/command 25 | -------------------------------------------------------------------------------- /command-pattern/command.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "./textDocument.ts"; 2 | import { ICommand } from "./types.ts"; 3 | 4 | // Concrete Command Creator Class 5 | export class Command implements ICommand { 6 | type: string; 7 | data: Record; 8 | protected lastInsertionIndex: number = -1; 9 | protected lastDeletedText: string = ""; 10 | constructor(type: string, data: Record) { 11 | this.type = type; 12 | this.data = data; 13 | } 14 | execute(doc: TextDocument): void { 15 | switch (this.type) { 16 | case "insert": { 17 | const { text, position } = this.data as { text: string; position: undefined }; 18 | this.lastInsertionIndex = doc.insert(text, position); 19 | break; 20 | } 21 | case "delete": { 22 | const { start, end } = this.data as { start: number; end: number }; 23 | this.lastDeletedText = doc.delete(start, end); 24 | break; 25 | } 26 | case "replace": { 27 | const { oldText, newText } = this.data as { oldText: string; newText: string }; 28 | this.lastInsertionIndex = doc.replace(oldText, newText); 29 | break; 30 | } 31 | default: { 32 | throw new Error("Unsupported Operation"); 33 | } 34 | } 35 | } 36 | undo(doc: TextDocument): void { 37 | switch (this.type) { 38 | case "insert": { 39 | const { text } = this.data as { text: string; position: undefined }; 40 | doc.delete(this.lastInsertionIndex - text.length, this.lastInsertionIndex - 1); 41 | break; 42 | } 43 | case "delete": { 44 | const { start } = this.data as { start: number; end: number }; 45 | this.lastInsertionIndex = doc.insert(this.lastDeletedText, start); 46 | break; 47 | } 48 | case "replace": { 49 | const { oldText, newText } = this.data as { oldText: string; newText: string }; 50 | this.lastInsertionIndex = doc.replace(newText, oldText); 51 | break; 52 | } 53 | default: { 54 | throw new Error("Unsupported Operation"); 55 | } 56 | } 57 | } 58 | toJSON() { 59 | return JSON.stringify({ 60 | data: this.data, 61 | type: this.type, 62 | lastInsertionIndex: this.lastInsertionIndex, 63 | lastDeletedText: this.lastDeletedText, 64 | }); 65 | } 66 | static fromJSON(command: string) { 67 | const parsedCommand = JSON.parse(command) as Command; 68 | const cmd = new Command(parsedCommand.type, parsedCommand.data); 69 | cmd.setLastDeletedText(parsedCommand.lastDeletedText); 70 | cmd.setLastInsertionIndex(parsedCommand.lastInsertionIndex); 71 | } 72 | protected setLastInsertionIndex(index: number) { 73 | this.lastInsertionIndex = index; 74 | } 75 | protected setLastDeletedText(text: string) { 76 | this.lastDeletedText = text; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /command-pattern/notes.txt: -------------------------------------------------------------------------------- 1 | {"document":"hello world\nhello earth","version":6,"undoStack":["{\"data\":{\"text\":\"hello world\"},\"type\":\"insert\",\"lastInsertionIndex\":11,\"lastDeletedText\":\"\"}","{\"data\":{\"text\":\"\\nhello earth\"},\"type\":\"insert\",\"lastInsertionIndex\":23,\"lastDeletedText\":\"\"}"],"redoStack":["{\"data\":{\"oldText\":\"hello\",\"newText\":\"hi\"},\"type\":\"replace\",\"lastInsertionIndex\":17,\"lastDeletedText\":\"\"}"]} -------------------------------------------------------------------------------- /command-pattern/textDocument.ts: -------------------------------------------------------------------------------- 1 | // Receiver - Peforms the actions on the Doc 2 | export class TextDocument { 3 | content: string; 4 | version: number; 5 | title: string; 6 | 7 | constructor(title: string) { 8 | this.title = title; 9 | this.content = ""; 10 | this.version = 0; 11 | } 12 | 13 | insert(text: string, position: number | null = null): number { 14 | if (position === null || position > this.content.length) { 15 | this.content += text; 16 | this.version++; 17 | } else { 18 | this.content = this.content.slice(0, position) + text + this.content.slice(position); 19 | this.version++; 20 | } 21 | return position === null ? this.content.length : position + text.length; 22 | } 23 | delete(start: number, end: number): string { 24 | if (start < 0 || end <= 0 || this.content.length <= 0) return ""; 25 | const deletedText = this.content.slice(start, end + 1); 26 | this.content = this.content.slice(0, start) + this.content.slice(end + 1); 27 | this.version++; 28 | return deletedText; 29 | } 30 | replace(oldText: string, newText: string): number { 31 | this.content = this.content.replaceAll(oldText, newText); 32 | this.version++; 33 | return this.content.lastIndexOf(newText) + newText.length; 34 | } 35 | 36 | view() { 37 | const header = `=== ${this.title} V${this.version} ===`; 38 | const footer = "=".repeat(header.length); 39 | return `${header}\n${this.content}\n${footer}`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /command-pattern/textEditor.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "./command.ts"; 2 | import { TextDocument } from "./textDocument.ts"; 3 | 4 | // Invoker - Stores and invokes commands when needed. 5 | export class TextEditor { 6 | private doc: TextDocument; 7 | private undoStack: Command[]; 8 | private redoStack: Command[]; 9 | 10 | constructor() { 11 | this.doc = new TextDocument("untitled.txt"); 12 | this.undoStack = []; 13 | this.redoStack = []; 14 | } 15 | execute(commands: Command[]) { 16 | commands.forEach(cmd => cmd.execute(this.doc)); 17 | this.undoStack.push(...commands); 18 | this.redoStack = []; 19 | } 20 | undo() { 21 | const cmd = this.undoStack.pop(); 22 | if (cmd) { 23 | cmd.undo(this.doc); 24 | this.redoStack.push(cmd); 25 | } 26 | } 27 | redo() { 28 | const cmd = this.redoStack.pop(); 29 | if (cmd) { 30 | cmd.execute(this.doc); 31 | this.undoStack.push(cmd); 32 | } 33 | } 34 | view() { 35 | return `BasicEditor\n${this.doc.view()}`; 36 | } 37 | save(fileName: string) { 38 | const session = { 39 | document: this.doc.content, 40 | version: this.doc.version, 41 | undoStack: this.undoStack.map(cmd => cmd.toJSON()), 42 | redoStack: this.redoStack.map(cmd => cmd.toJSON()), 43 | }; 44 | Deno.writeTextFileSync(`command-pattern/${fileName}`, JSON.stringify(session)); 45 | } 46 | load(fileName: string) { 47 | const file = Deno.readTextFileSync(`command-pattern/${fileName}`); 48 | if (file) { 49 | const session = JSON.parse(file); 50 | this.doc = new TextDocument(fileName); 51 | this.doc.content = session.document; 52 | this.doc.version = session.version; 53 | this.undoStack = session.undoStack.map((cmd: string) => Command.fromJSON(cmd)); 54 | this.redoStack = session.redoStack.map((cmd: string) => Command.fromJSON(cmd)); 55 | } else { 56 | throw new Error("file not found"); 57 | } 58 | } 59 | close() { 60 | this.redoStack = []; 61 | this.undoStack = []; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /command-pattern/types.ts: -------------------------------------------------------------------------------- 1 | export interface ICommand { 2 | type: string; 3 | data: unknown; 4 | execute(doc: T): void; 5 | undo(doc: T): void; 6 | toJSON(): string; 7 | } 8 | -------------------------------------------------------------------------------- /command-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "./command.ts"; 2 | import { TextEditor } from "./textEditor.ts"; 3 | 4 | const textEditor = new TextEditor(); 5 | 6 | textEditor.execute([new Command("insert", { text: "hello world" }), new Command("insert", { text: "\nhello earth" })]); 7 | textEditor.execute([new Command("replace", { oldText: "hello", newText: "hi" })]); 8 | 9 | console.log(textEditor.view()); 10 | /** 11 | BasicEditor 12 | === untitled.txt V3 === 13 | hi world 14 | hi earth 15 | ================ 16 | */ 17 | 18 | textEditor.undo(); // replace undone 19 | textEditor.undo(); // insert undone 20 | console.log(textEditor.view()); 21 | /** 22 | BasicEditor 23 | === untitled.txt V5 === 24 | hello world 25 | ================ 26 | */ 27 | 28 | textEditor.redo(); 29 | console.log(textEditor.view()); // previous insert redone 30 | /** 31 | BasicEditor 32 | === untitled.txt V6 === 33 | hello world 34 | hello earth 35 | */ 36 | 37 | textEditor.save("notes.txt"); 38 | textEditor.close(); 39 | 40 | const textEditor2 = new TextEditor(); // another text editor instance loading the notes file 41 | 42 | textEditor2.load("notes.txt"); 43 | console.log(textEditor2.view()); 44 | 45 | textEditor2.execute([new Command("delete", { start: 0, end: 5 })]); 46 | console.log(textEditor2.view()); 47 | 48 | /** 49 | BasicEditor 50 | === notes.txt V7 === 51 | world 52 | hello earth 53 | ================ 54 | */ 55 | 56 | textEditor2.undo(); // undo delete 57 | console.log(textEditor2.view()); 58 | /** 59 | BasicEditor 60 | === notes.txt V8 === 61 | hello world 62 | hello earth 63 | ================ 64 | */ 65 | -------------------------------------------------------------------------------- /composite-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Composite Pattern 2 | 3 | The Composite Pattern is a structural design pattern that lets you treat individual objects and groups of objects uniformly which means you can compose objects into tree structures and then work with these structures as if they were individual objects. 4 | 5 | Think of it like a tree structure where both leaves (single objects) and branches (groups of objects) can be handled the same way. This is helpful when building systems with hierarchical data, like UI components or file systems. 6 | 7 | ## Key Concepts 8 | 9 | 1. **Component**: A common interface or abstract class for all objects in the hierarchy. 10 | 2. **Leaf**: A basic, indivisible object that implements the `Component` interface. 11 | 3. **Composite**: A container object (or branch) that also implements the `Component` interface and can hold other `Component` objects (both Leafs and other Composites). 12 | 4. **Tree Stucture**: Underlying entity represented via tree structure (parent - child heirarchy) 13 | 5. **Uniformity**: The ability to treat both Leafs and Composites the same way via the `Component` interface. 14 | 15 | ## Use Cases 16 | 17 | 1. **Frontend Frameworks**: Building UI trees with components (e.g., React's virtual DOM and component composition model where both leaf components and container components share the same basic interface) 18 | 2. **Query Builders**: Representing complex database query structures. 19 | 3. **Filesystem Operations**: Representing files (Leaf) and directories (Composite). 20 | 4. **Image Editors**: Handling shapes (Leaf) and groups of shapes (Composite). 21 | 22 | ## Further Reading 23 | 24 | https://refactoring.guru/design-patterns/composites 25 | -------------------------------------------------------------------------------- /composite-pattern/directory.composite.ts: -------------------------------------------------------------------------------- 1 | import { IFileSystemNode } from "./types.ts"; 2 | 3 | // Composite Class 4 | export class Directory implements IFileSystemNode { 5 | private children: IFileSystemNode[]; 6 | name: string; 7 | constructor(name: string) { 8 | this.name = name; 9 | this.children = []; 10 | } 11 | getSize(): number { 12 | return this.children.reduce((sum, node) => sum + node.getSize(), 0); 13 | } 14 | print(indent: string): void { 15 | console.log(`${indent}📁 ${this.name}/ (${this.getSize()} KB)`); 16 | this.children.forEach(node => { 17 | node.print(indent + " "); 18 | }); 19 | } 20 | add(node: IFileSystemNode) { 21 | this.children.sort((nodeA, nodeB) => nodeA.name.localeCompare(nodeB.name)); 22 | this.children.push(node); 23 | } 24 | remove(node: IFileSystemNode) { 25 | const idx = this.children.indexOf(node); 26 | if (idx > -1) this.children.splice(idx, 1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /composite-pattern/file.leaf.ts: -------------------------------------------------------------------------------- 1 | import { IFileSystemNode } from "./types.ts"; 2 | 3 | // Leaf Class 4 | export class File implements IFileSystemNode { 5 | name: string; 6 | private size: number; 7 | 8 | constructor(name: string, size: number) { 9 | this.name = name; 10 | this.size = size; 11 | } 12 | getSize(): number { 13 | return this.size; 14 | } 15 | print(indent: string): void { 16 | console.log(`${indent}📄 ${this.name} (${this.size} KB)`); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /composite-pattern/types.ts: -------------------------------------------------------------------------------- 1 | // Component Interface 2 | export interface IFileSystemNode { 3 | name: string; 4 | getSize(): number; 5 | print(indent: string): void; 6 | } 7 | // both Leaf and Composite class will implement IFileSystemNode interface 8 | -------------------------------------------------------------------------------- /composite-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { Directory } from "./directory.composite.ts"; 2 | import { File as File_ } from "./file.leaf.ts"; 3 | 4 | const root = new Directory("root"); 5 | const docs = new Directory("docs"); 6 | const pics = new Directory("pics"); 7 | 8 | const file1 = new File_("notes.txt", 10000); 9 | const file2 = new File_("sidney.jpeg", 4028); 10 | const file3 = new File_("resume.pdf", 3534); 11 | const file4 = new File_("cat.png", 3002); 12 | 13 | root.add(docs); 14 | root.add(pics); 15 | 16 | file1.print(""); // 📄 notes.txt (10000 KB) 17 | 18 | docs.add(file1); 19 | docs.add(file3); 20 | 21 | pics.add(file2); 22 | pics.add(file4); 23 | 24 | root.print(" "); 25 | /** 26 | 📁 root/ (20564 KB) 27 | 📁 docs/ (13534 KB) 28 | 📄 notes.txt (10000 KB) 29 | 📄 resume.pdf (3534 KB) 30 | 📁 pics/ (7030 KB) 31 | 📄 sidney.jpeg (4028 KB) 32 | 📄 cat.png (3002 KB) 33 | */ 34 | 35 | root.remove(docs); 36 | 37 | root.print(" "); 38 | /** 39 | 📁 root/ (7030 KB) 40 | 📁 pics/ (7030 KB) 41 | 📄 sidney.jpeg (4028 KB) 42 | 📄 cat.png (3002 KB) 43 | */ 44 | -------------------------------------------------------------------------------- /decorator-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Decorator Pattern 2 | 3 | The Decorator Pattern is a structural design pattern that allows you to dynamically add new behavior to an object without modifying its existing code. 4 | 5 | Key Concepts 6 | 7 | 1. **Composition over Inheritance**: Instead of extending a class, decorators wrap an object and modify its behavior. 8 | 2. **Flexible and Scalable**: You can stack multiple decorators to add multiple functionalities dynamically. 9 | 10 | ## Use Cases 11 | 12 | Express.js middleware functions - they wrap around route handlers to add authentication, logging, or error handling. 13 | 14 | In Angular, the `@Component`, `@Injectable`, and `@Input` decorators modify class behavior by adding metadata and functionality to classes and properties. 15 | 16 | In NestJS uses decorators to define controllers, services, middleware, and validation. `@Controller()` - Defines a class as a controller handling HTTP routes.` @Get()`, `@Post()` - Maps methods to specific HTTP routes. 17 | 18 | ## Further Reading 19 | 20 | https://blog.logrocket.com/practical-guide-typescript-decorators/ 21 | 22 | https://refactoring.guru/design-patterns/decorator 23 | 24 | https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators 25 | 26 | ## Differences with other patterns 27 | 28 | 1. Need to make incompatible things work together? Use Adapter 29 | 30 | 2. Need to separate concerns that vary independently? Use Bridge 31 | 32 | 3. Need to treat individual objects and compositions uniformly? Use Composite 33 | 34 | 4. Need to add behaviors dynamically? Use Decorator 35 | -------------------------------------------------------------------------------- /decorator-pattern/measure.decorator.ts: -------------------------------------------------------------------------------- 1 | // method decorator 2 | export function measure(mode?: "debug") { 3 | // deno-lint-ignore ban-types 4 | return function (target: Function, context: ClassMethodDecoratorContext) { 5 | if (context.kind == "method") { 6 | // the `this` parameter must be the first parameter in the parameter list, and it only exists for type-checking purposes. It gets erased when TypeScript is compiled to JavaScript. 7 | return function (this: unknown, ...args: unknown[]) { 8 | const debug = mode === "debug"; 9 | const name = context.name; 10 | const start = performance.now(); 11 | debug && console.log(`entering fn: '${String(name)}' | params: ${args} | ${start}`); 12 | const result = target.apply(this, args); 13 | const end = performance.now(); 14 | debug && console.log(`existing fn: '${String(name)}' | params: ${args} | ${end}`); 15 | console.log(`execution time for '${String(name)}' fn: ${end - start} ms`); 16 | return result; 17 | }; 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /decorator-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { required, validate } from "./class-validator.decorator.ts"; 2 | import { measure } from "./measure.decorator.ts"; 3 | 4 | // Example 1 - Using method decorator 5 | class Database { 6 | private name: string; 7 | constructor(name: string) { 8 | this.name = name; 9 | } 10 | @measure("debug") 11 | query(sql: string) { 12 | for (let i = 0; i < 10e6; i++) { 13 | continue; 14 | } 15 | console.log("query executed: " + sql); 16 | } 17 | } 18 | 19 | const db = new Database("postgresql"); 20 | db.query("SELECT * FROM USERS;"); 21 | /** 22 | entering fn: 'query' | params: SELECT * FROM USERS; | 93.522292 23 | query executed: SELECT * FROM USERS; 24 | existing fn: 'query' | params: SELECT * FROM USERS; | 126.713459 25 | execution time for 'query' fn: 33.19116700000001 ms 26 | */ -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "dev": "deno run --watch main.ts", 4 | "pattern": "deno run --allow-read --allow-write --allow-net main.ts", 5 | "fmt": "deno run -A npm:prettier --write ." 6 | }, 7 | "imports": { 8 | "@std/assert": "jsr:@std/assert@1" 9 | }, 10 | "fmt": { 11 | "exclude": ["**/*"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@b-fuze/deno-dom@*": "0.1.49", 5 | "jsr:@std/assert@1": "1.0.10", 6 | "jsr:@std/internal@^1.0.5": "1.0.5", 7 | "npm:prettier@*": "3.4.2" 8 | }, 9 | "jsr": { 10 | "@b-fuze/deno-dom@0.1.49": { 11 | "integrity": "45c40175fdd1e74ab2d4b54c4fdaabbad6e5b76ee28df12a48e076b3fa7901a9" 12 | }, 13 | "@std/assert@1.0.10": { 14 | "integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3", 15 | "dependencies": [ 16 | "jsr:@std/internal" 17 | ] 18 | }, 19 | "@std/internal@1.0.5": { 20 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" 21 | } 22 | }, 23 | "npm": { 24 | "prettier@3.4.2": { 25 | "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==" 26 | } 27 | }, 28 | "workspace": { 29 | "dependencies": [ 30 | "jsr:@std/assert@1" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /facade-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Facade Pattern 2 | 3 | The Facade Pattern is a structural design pattern that provides a simplified, higher-level interface to a complex system of classes, libraries, or subsystems. It helps to hide the complexities behind a unified interface and easy to use API. This helps reduce coupling between client code and the underlying system. 4 | 5 | ## Key Concepts 6 | 7 | 1. **Encapsulation**: Hides the complexities of multiple subsystems. 8 | 2. **Simplification**: Provides a single API to interact with complex components. 9 | 10 | ## Use Cases 11 | 12 | This might be most widely used pattern out there. 13 | 14 | 1. Database ORMs (like TypeORM) that hide SQL complexity 15 | 2. Frontend frameworks (like React Router) that simplify navigation 16 | 3. Cloud SDKs (like AWS SDK) that abstract AWS services 17 | 4. HTTP clients (like Axios) that simplify REST API calls 18 | 19 | ## Differences with other patterns 20 | 21 | **Adapter pattern** wraps over one object. **Facade Pattern** creates a single object to represent a subsystem of multiple objects. **Flyweight Pattern** creates lots of little objects. 22 | 23 | **Facade** defines a simplified interface to a subsystem of objects. The subsystem itself is unaware of the facade. **Mediator** facilitate communication between components of the system. The components only know about the mediator object and don’t communicate directly. 24 | 25 | 26 | ## Further Reading 27 | 28 | https://refactoring.guru/design-patterns/facade 29 | -------------------------------------------------------------------------------- /facade-pattern/apiClient.ts: -------------------------------------------------------------------------------- 1 | import { PaymentRequest, PaymentResponse } from "./types.ts"; 2 | 3 | export class ApiClient { 4 | private readonly baseUrl = "https://api.stripe.com/v1"; 5 | 6 | charge(request: PaymentRequest): Promise { 7 | // Simulate successful API call 8 | return Promise.resolve({ 9 | id: `ch_${Math.random().toString(36).slice(2)}`, 10 | amount: request.amount, 11 | status: "succeeded", 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /facade-pattern/paymentValidator.ts: -------------------------------------------------------------------------------- 1 | import { PaymentRequest } from "./types.ts"; 2 | // Stripe SDKs validate card formats (Luhn check) before making API calls 3 | export class PaymentValidator { 4 | validate(request: PaymentRequest): void { 5 | if (!/^\d{16}$/.test(request.cardNumber)) { 6 | throw new Error("Invalid card number"); 7 | } 8 | if (request.amount <= 0) { 9 | throw new Error("Amount must be positive"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /facade-pattern/retryHandler.ts: -------------------------------------------------------------------------------- 1 | // Mimics Stripe's automatic retries for idempotent requests (network errors, rate limits) 2 | export class RetryHandler { 3 | private readonly maxRetries = 3; 4 | 5 | async execute(fn: () => Promise): Promise { 6 | let attempts = 0; 7 | 8 | while (attempts < this.maxRetries) { 9 | try { 10 | return await fn(); 11 | } catch (error) { 12 | console.error(error); 13 | attempts++; 14 | if (attempts >= this.maxRetries) break; 15 | } 16 | } 17 | 18 | throw new Error("Max retries exceeded"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /facade-pattern/stripe.facade.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient } from "./apiClient.ts"; 2 | import { StripeLogger } from "./stripeLogger.ts"; 3 | import { PaymentValidator } from "./paymentValidator.ts"; 4 | import { RetryHandler } from "./retryHandler.ts"; 5 | import { PaymentRequest, PaymentResponse } from "./types.ts"; 6 | 7 | export class StripeFacade { 8 | constructor( 9 | private validator: PaymentValidator, 10 | private apiClient: ApiClient, 11 | private retryHandler: RetryHandler, 12 | private logger: StripeLogger 13 | ) {} 14 | 15 | async processPayment(payment: PaymentRequest): Promise { 16 | try { 17 | this.validator.validate(payment); 18 | return await this.retryHandler.execute(() => this.apiClient.charge(payment)); 19 | } catch (error) { 20 | this.logger.logError(error as Error); 21 | throw error; // Re-throw for client handling 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /facade-pattern/stripeLogger.ts: -------------------------------------------------------------------------------- 1 | export class StripeLogger { 2 | logError(error: Error): void { 3 | console.error(`[STRIPE ERROR][${new Date().toISOString()}] ${error.message}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /facade-pattern/types.ts: -------------------------------------------------------------------------------- 1 | export interface PaymentRequest { 2 | amount: number; 3 | currency: string; 4 | cardNumber: string; 5 | expMonth: number; 6 | expYear: number; 7 | cvc: string; 8 | } 9 | 10 | export interface PaymentResponse { 11 | id: string; 12 | amount: number; 13 | status: "succeeded" | "failed"; 14 | } 15 | -------------------------------------------------------------------------------- /facade-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient } from "./apiClient.ts"; 2 | import { StripeLogger } from "./stripeLogger.ts"; 3 | import { PaymentValidator } from "./paymentValidator.ts"; 4 | import { RetryHandler } from "./retryHandler.ts"; 5 | import { StripeFacade } from "./stripe.facade.ts"; 6 | import { PaymentRequest } from "./types.ts"; 7 | 8 | const stripe = new StripeFacade(new PaymentValidator(), new ApiClient(), new RetryHandler(), new StripeLogger()); 9 | 10 | const paymentRequest: PaymentRequest = { 11 | amount: 9999, // $99.99 12 | currency: "usd", 13 | cardNumber: "4242424242424242", 14 | expMonth: 12, 15 | expYear: 2026, 16 | cvc: "123", 17 | }; 18 | 19 | try { 20 | const result = await stripe.processPayment(paymentRequest); 21 | console.log(`Payment ${result.id} succeeded: $${result.amount / 100}`); 22 | } catch (error) { 23 | console.log("Payment failed. Please try another card.", error); 24 | } 25 | -------------------------------------------------------------------------------- /factory-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Factory Pattern 2 | 3 | Factory pattern is an OOPS way of refering to _creating objects with specific 4 | functionality and encapsulating the creation process so as to not expose the 5 | process to the client._ 6 | 7 | Factory can be anything that creates an object. In JS/TS world, it can be a 8 | function or a class. 9 | 10 | Factories make it easier to swap or extend the creation logic without changing 11 | client code. 12 | 13 | Adding new types of similar objects is simpler, as you can modify existing 14 | factory for object creation. 15 | 16 | ## Key Concepts 17 | 18 | 1. **Product Interface/Abstract Class**: Defines the type of objects the factory 19 | will create. 20 | 2. **Concrete Products**: Specific implementations of the product interface. 21 | 3. **Factory Interface/Abstract Class**: Declares a method for creating objects. 22 | 4. **Concrete Factory**: Implements the creation logic to instantiate specific 23 | products. 24 | 25 | 26 | ## Further Reading 27 | 28 | https://refactoring.guru/design-patterns/factory-method 29 | -------------------------------------------------------------------------------- /factory-pattern/beverage.factory.ts: -------------------------------------------------------------------------------- 1 | import { Beverage, Beverages, IBeverageFactory } from "./types.ts"; 2 | 3 | // -- Product Classes -- // 4 | 5 | class Coffee implements Beverage { 6 | name: Beverages = "coffee"; 7 | quantity: number; 8 | type: string; 9 | 10 | constructor(quantity: number, type: string) { 11 | this.quantity = quantity; 12 | this.type = type; 13 | } 14 | serve(): string { 15 | return `${this.name}: ${this.quantity} | ${this.type}`; 16 | } 17 | } 18 | 19 | class Tea implements Beverage { 20 | name: Beverages = "tea"; 21 | quantity: number; 22 | 23 | constructor(quantity: number) { 24 | this.quantity = quantity; 25 | } 26 | serve(): string { 27 | return `${this.name}: ${this.quantity}`; 28 | } 29 | } 30 | 31 | class Juice implements Beverage { 32 | name: Beverages = "juice"; 33 | quantity: number; 34 | type: string; 35 | 36 | constructor(quantity: number, type: string) { 37 | this.quantity = quantity; 38 | this.type = type; 39 | } 40 | serve(): string { 41 | return `${this.name}: ${this.quantity} | ${this.type}`; 42 | } 43 | } 44 | 45 | // -- Beverage Factory Class -- // 46 | 47 | export class BeverageFactory implements IBeverageFactory { 48 | createBeverage(beverage: Beverages, quantity: number, type?: string): Beverage { 49 | switch (beverage) { 50 | case "coffee": 51 | if (!type) throw new Error("define a type"); 52 | return new Coffee(quantity, type); 53 | case "tea": 54 | return new Tea(quantity); 55 | case "juice": 56 | if (!type) throw new Error("define a type"); 57 | return new Juice(quantity, type); 58 | default: 59 | throw new Error("Invalid Beverage"); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /factory-pattern/types.ts: -------------------------------------------------------------------------------- 1 | // -- Product Interface -- // 2 | export interface Beverage { 3 | name: string; 4 | quantity: number; 5 | type?: string; 6 | serve(): string; 7 | } 8 | 9 | export type Beverages = "coffee" | "tea" | "juice"; 10 | 11 | // -- Abstract Factory Interface -- // 12 | export interface IBeverageFactory { 13 | createBeverage(beverage: Beverages, quantity: number, type?: string): Beverage; 14 | } 15 | -------------------------------------------------------------------------------- /factory-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { BeverageFactory } from "./beverage.factory.ts"; 2 | 3 | const factory = new BeverageFactory(); 4 | 5 | const coffee = factory.createBeverage("coffee", 10, "latte"); 6 | console.log(coffee.serve()); 7 | 8 | const tea = factory.createBeverage("tea", 2); 9 | 10 | console.log(tea.serve()); 11 | 12 | const juice = factory.createBeverage("juice", 20, "orange"); 13 | 14 | console.log(juice.serve()); 15 | -------------------------------------------------------------------------------- /flyweight-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Flyweight Pattern 2 | 3 | It is a structural design pattern that helps reduce memory usage by sharing common data among multiple objects. Instead of creating separate instances for every object, it reuses existing ones when possible. 4 | 5 | For example, if an application has thousands of objects with some shared properties, we can store those properties in a separate, shared object. This avoids redundant memory consumption and improves performance. Also used when one doesn't want create new resource and share existing resource from a pool 6 | 7 | ## Key Concepts 8 | 9 | 1. **Intrinsic State**: Shared data that remains the same across multiple instances. 10 | 2. **Extrinsic State**: Unique data specific to each instance, not shared. 11 | 3. **Concrete Flyweight**: Implements the Flyweight interface with intrinsic state 12 | 4. **Flyweight Factory**: Manages and reuses shared objects instead of creating new ones. 13 | 14 | ## Use Cases 15 | 16 | 1. Game engines use the flyweight pattern to optimize memory usage by sharing graphical assets, textures, and object instances. 17 | 2. Widgets and UI components are shared instead of creating new instances in GUI frameworks 18 | 3. React uses the flyweight pattern in its Virtual DOM and component rendering. The reconciliation process ensures that React reuses existing DOM elements instead of creating new ones. 19 | 4. Three.js implements instancing and shared geometry buffers to optimize rendering large numbers of 3D objects. 20 | 5. Database connection pooling, caching query results 21 | 6. Text editors, instead of each character object storing its own font and styling information, these properties are shared among characters with the same formatting. Similarly, 22 | 23 | ## Further Reading 24 | 25 | https://refactoring.guru/design-patterns/flyweight 26 | 27 | https://www.oreilly.com/library/view/learning-javascript-design/9781449334840/ch09s18.html 28 | -------------------------------------------------------------------------------- /flyweight-pattern/connectionPoolManager.factory.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseConnection } from "./dbConnection.flyweight.ts"; 2 | import { DatabaseConfig } from "./types.ts"; 3 | 4 | // Extrinsic State 5 | class ConnectionState { 6 | public connection: DatabaseConnection; 7 | public inUse: boolean; 8 | constructor(connection: DatabaseConnection, inUse: boolean = false) { 9 | (this.connection = connection), (this.inUse = inUse); 10 | } 11 | } 12 | 13 | // Flyweight Factory: Manages shared database connections 14 | export class ConnectionPoolManager { 15 | private static instance: ConnectionPoolManager; 16 | private connectionPools: Map = new Map(); 17 | 18 | private constructor() {} 19 | 20 | // Singleton pattern: Ensures only one ConnectionPoolManager instance 21 | static getInstance(): ConnectionPoolManager { 22 | if (!ConnectionPoolManager.instance) { 23 | ConnectionPoolManager.instance = new ConnectionPoolManager(); 24 | } 25 | return ConnectionPoolManager.instance; 26 | } 27 | 28 | private generateConfigKey(config: DatabaseConfig): string { 29 | return `${config.host}:${config.port}/${config.database}`; 30 | } 31 | 32 | // Get or create a connection from the pool (Flyweight retrieval) 33 | async getConnection(config: DatabaseConfig, maxConnections: number): Promise { 34 | const configKey = this.generateConfigKey(config); 35 | 36 | if (!this.connectionPools.has(configKey)) { 37 | this.connectionPools.set(configKey, []); 38 | } 39 | 40 | const pool = this.connectionPools.get(configKey)!; 41 | 42 | // find any connection not in use 43 | let connectionState = pool.find(connState => !connState.inUse); 44 | 45 | // If no connection is available and we haven't reached max connections, create new connection 46 | if (!connectionState && pool.length < maxConnections) { 47 | const newConnection = new DatabaseConnection(config); 48 | await newConnection.connect(); 49 | connectionState = new ConnectionState(newConnection); 50 | pool.push(connectionState); 51 | } 52 | 53 | if (connectionState) { 54 | connectionState.inUse = true; 55 | return connectionState.connection; 56 | } 57 | 58 | throw new Error("No connections available in the pool"); 59 | } 60 | 61 | // Release a connection back to the pool 62 | releaseConnection(config: DatabaseConfig, connection: DatabaseConnection): void { 63 | const configKey = this.generateConfigKey(config); 64 | const pool = this.connectionPools.get(configKey); 65 | 66 | if (pool) { 67 | const connectionState = pool.find(connState => connState.connection.getId() === connection.getId()); 68 | if (connectionState) { 69 | connectionState.inUse = false; 70 | } 71 | } 72 | } 73 | 74 | getPoolStats(config: DatabaseConfig): { 75 | totalConnections: number; 76 | activeConnections: number; 77 | availableConnections: number; 78 | } { 79 | const configKey = this.generateConfigKey(config); 80 | const pool = this.connectionPools.get(configKey) || []; 81 | 82 | const totalConnections = pool.length; 83 | const activeConnections = pool.filter(connState => connState.inUse).length; 84 | 85 | return { 86 | totalConnections, 87 | activeConnections, 88 | availableConnections: totalConnections - activeConnections, 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /flyweight-pattern/dbConnection.flyweight.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseConfig } from "./types.ts"; 2 | 3 | // Concrete Flyweight Class: Represents a shared connection 4 | export class DatabaseConnection implements DatabaseConnection { 5 | private connectionId: string; 6 | 7 | constructor(private config: DatabaseConfig) { 8 | this.connectionId = crypto.randomUUID(); 9 | } 10 | 11 | // Simulate connecting to the database 12 | async connect(): Promise { 13 | console.log(`Connecting to ${this.config.database} at ${this.config.host}:${this.config.port}`); 14 | await new Promise(resolve => setTimeout(resolve, 100)); 15 | } 16 | 17 | getId(): string { 18 | return this.connectionId; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /flyweight-pattern/types.ts: -------------------------------------------------------------------------------- 1 | // Flyweight Interface (Defines common operations for shared objects) 2 | export interface DatabaseConnectionFlyweight { 3 | connect(): Promise; 4 | getId(): string; 5 | } 6 | 7 | // Intrinsic State: Configuration that is shared across multiple connections 8 | export interface DatabaseConfig { 9 | host: string; 10 | port: number; 11 | username: string; 12 | password: string; 13 | database: string; 14 | } 15 | -------------------------------------------------------------------------------- /flyweight-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionPoolManager } from "./connectionPoolManager.factory.ts"; 2 | import { DatabaseConfig } from "./types.ts"; 3 | 4 | async function main() { 5 | const config: DatabaseConfig = { 6 | host: "localhost", 7 | port: 5432, 8 | username: "user", 9 | password: "password", 10 | database: "econ-store", 11 | }; 12 | const maxConnections = 5; 13 | 14 | const poolManager = ConnectionPoolManager.getInstance(); 15 | 16 | try { 17 | // Simulate multiple connection requests 18 | const connections = await Promise.all([ 19 | poolManager.getConnection(config, maxConnections), 20 | poolManager.getConnection(config, maxConnections), 21 | poolManager.getConnection(config, maxConnections), 22 | ]); 23 | 24 | console.log("Pool stats after getting connections:", poolManager.getPoolStats(config)); 25 | 26 | // Release connections back to the pool 27 | connections.forEach(conn => { 28 | poolManager.releaseConnection(config, conn); 29 | }); 30 | 31 | console.log("Pool stats after releasing connections:", poolManager.getPoolStats(config)); 32 | } catch (error) { 33 | console.error("Error:", error); 34 | } 35 | } 36 | main(); 37 | 38 | /** 39 | Connecting to econ-store at localhost:5432 40 | Connecting to econ-store at localhost:5432 41 | Connecting to econ-store at localhost:5432 42 | Pool stats after getting connections: { totalConnections: 3, activeConnections: 3, availableConnections: 0 } 43 | Pool stats after releasing connections: { totalConnections: 3, activeConnections: 0, availableConnections: 3 } 44 | */ 45 | -------------------------------------------------------------------------------- /iterator-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Iterator Pattern 2 | 3 | The Iterator pattern is a behavioral design pattern that provides a standardized way to traverse through a collection of elements without exposing its underlying structure. 4 | 5 | Clients can iterate using next()/hasNext() without knowing if data comes from an array, database, or tree (abstraction) and same iterator interface works across different collections (reusability). 6 | Having this pattern implemented new traversal logic (e.g., depth-first vs. breadth-first) can be added without changing the collection. 7 | 8 | ## Key Concepts 9 | 10 | **Iterator interface** defines methods for traversing: typically `hasNext()` to check if more elements exist and `next()` to retrieve the next element. 11 | **Collection interface** declares methods for getting an iterator instance. 12 | **Concrete Iterator** implements the traversal behavior for a specific collection type. 13 | **Concrete Collection** creates and returns appropriate iterator instances. 14 | 15 | ## Use Cases 16 | 17 | 1. React’s `.map()` for rendering lists. 18 | 2. DOM traversal APIs like `NodeIterator` let you walk through document elements. 19 | 3. Database cursors in MongoDB and PostgreSQL implement iterators to stream large result sets. 20 | 4. Node.js's `ReadableStream` use iterator patterns for processing data chunks. 21 | 22 | ## Further Reading 23 | 24 | https://refactoring.guru/design-patterns/iterator 25 | 26 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_generators 27 | -------------------------------------------------------------------------------- /iterator-pattern/example1/menu.ts: -------------------------------------------------------------------------------- 1 | import { MenuIterator } from "./menuIterator.ts"; 2 | import { MenuItem } from "./types.ts"; 3 | 4 | // Concrete Collection implementation for menu structure 5 | export class Menu { 6 | constructor(private root: MenuItem) {} 7 | 8 | getIterator(maxDepth: number): MenuIterator { 9 | return new MenuIterator(this.root, maxDepth); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iterator-pattern/example1/menuIterator.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem, MenuItemWithDepth } from "./types.ts"; 2 | 3 | export class MenuIterator implements Iterator { 4 | private stack: { node: MenuItem; depth: number }[] = []; 5 | private currentDepth: number = 0; 6 | private readonly maxDepth: number; 7 | constructor(root: MenuItem, maxDepth: number) { 8 | this.stack.push({ node: root, depth: 1 }); 9 | this.maxDepth = maxDepth; 10 | } 11 | 12 | next(): IteratorResult { 13 | if (this.stack.length <= 0) return { done: true, value: undefined }; 14 | 15 | // stack is used instead of queue as `pop()` is O(1) while `shift()` is O(n) 16 | const entry = this.stack.pop()!; 17 | this.currentDepth = entry.depth; 18 | const currentNode = entry.node; 19 | 20 | if (currentNode.children && this.currentDepth < this.maxDepth) { 21 | const nextDepth = this.currentDepth + 1; 22 | 23 | // reversing so that insertion order is correct 24 | const children = [...currentNode.children].reverse(); 25 | 26 | children.forEach(child => this.stack.push({ node: child, depth: nextDepth })); 27 | } 28 | 29 | return { 30 | value: { ...currentNode, depth: this.currentDepth }, 31 | done: false, 32 | }; 33 | } 34 | [Symbol.iterator](): Iterator { 35 | return this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /iterator-pattern/example1/types.ts: -------------------------------------------------------------------------------- 1 | export interface MenuItem { 2 | name: string; 3 | children?: MenuItem[]; 4 | } 5 | 6 | export interface MenuItemWithDepth extends MenuItem { 7 | depth: number; 8 | } 9 | -------------------------------------------------------------------------------- /iterator-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | // Example 1 - Menu Iterator 2 | // Use Cases 3 | // Show hierarchy up to certain levels in Breadcrumb Navigation. 4 | // Limit navigation depth for screen readers 5 | // Create partial sitemaps with depth constraints 6 | // Optimize UI rendering of deep hierarchies by limiting traversal 7 | 8 | import { Menu } from "./example1/menu.ts"; 9 | import { MenuItem, MenuItemWithDepth } from "./example1/types.ts"; 10 | 11 | const menuData: MenuItem = { 12 | name: "Home", 13 | children: [ 14 | { 15 | name: "File", 16 | children: [ 17 | { name: "New Text File" }, 18 | { name: "New File" }, 19 | { name: "Open", children: [{ name: "Open Recent" }] }, 20 | ], 21 | }, 22 | { 23 | name: "Settings", 24 | children: [{ name: "Theme" }, { name: "Extensions" }], 25 | }, 26 | ], 27 | }; 28 | 29 | const menu = new Menu(menuData); 30 | const depthTwoIterator = menu.getIterator(2); 31 | const depthThreeIterator = menu.getIterator(3); 32 | 33 | let result: IteratorResult; 34 | 35 | while (true) { 36 | result = depthTwoIterator.next(); 37 | if (result.done) break; 38 | console.log("-".repeat(result.value.depth) + result.value.name); 39 | } 40 | /** 41 | -Home 42 | --File 43 | --Settings 44 | */ 45 | 46 | // Defining [Symbol.iterator]() for iterator allows native traversal using for..of 47 | for (const item of depthThreeIterator) { 48 | console.log("-".repeat(item.depth) + item.name); 49 | } 50 | /** 51 | -Home 52 | --File 53 | ---New Text File 54 | ---New File 55 | ---Open 56 | --Settings 57 | ---Theme 58 | */ 59 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | // Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts 2 | if (import.meta.main) { 3 | const [pattern] = Deno.args; 4 | 5 | if (!pattern) { 6 | console.error("Usage: deno run main.ts "); 7 | Deno.exit(1); 8 | } 9 | 10 | try { 11 | // Dynamically import the module based on the provided pattern 12 | const modulePath = `./${pattern}/usage.ts`; 13 | await import(modulePath); 14 | 15 | // deno-lint-ignore no-explicit-any 16 | } catch (error: any) { 17 | console.error(`Something went wrong ${pattern}`); 18 | console.error(error.message); 19 | console.error(error); 20 | Deno.exit(1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /main_test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/33j33/design-patterns-using-typescript/8b62a7ca9ee7f8750824f9cf500ebf50bc912e76/main_test.ts -------------------------------------------------------------------------------- /mediator-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Mediator Pattern 2 | 3 | It is a behavioral design pattern that helps reduce direct dependencies between components by introducing a mediator. Instead of components communicating directly with each other, they communicate through the mediator, making the system loosely coupled and easier to manage as adding new components doesn't affect existing ones. 4 | 5 | Think of it like an air traffic controller - instead of planes communicating directly with each other, they all communicate through the control tower. 6 | 7 | ## Key Concepts 8 | 9 | 1. **Mediator**: The central object that handles communication between components. 10 | 2. **Colleagues (Components)**: Objects that need to communicate but do so through the mediator instead of directly. 11 | 12 | ## Use Cases 13 | 14 | 1. Handling communication between UI components without tight coupling. Example: Redux store for state management - acts as a mediator between application state and components. 15 | 2. Managing communication between services in microservices architecture. Example: Message Brokers (RabbitMQ, Kafka). 16 | 3. Managing message routing between users in Chat rooms/channels 17 | 18 | ## Difference with other patterns 19 | 20 | **Chain of Responsibility** passes a request sequentially along a dynamic chain of potential receivers until one of them handles it. 21 | 22 | **Command** establishes unidirectional connections between senders and receivers. 23 | 24 | **Mediator** eliminates direct connections between senders and receivers, forcing them to communicate indirectly via a mediator object. 25 | 26 | **Observer** lets receivers dynamically subscribe to and unsubscribe from receiving requests. 27 | 28 | ## Further Reading 29 | 30 | https://www.patterns.dev/vanilla/mediator-pattern 31 | 32 | https://refactoring.guru/design-patterns/mediator 33 | -------------------------------------------------------------------------------- /mediator-pattern/chatRoom.ts: -------------------------------------------------------------------------------- 1 | import { IMediator, Message } from "./types.ts"; 2 | 3 | // Concrete Component 4 | export class ChatRoom { 5 | private mediator: IMediator; 6 | chats: Message[]; 7 | name: string; 8 | constructor(name: string, mediator: IMediator) { 9 | this.mediator = mediator; 10 | this.name = name; 11 | this.chats = []; 12 | this.mediator.notify("global", "room-created", this); 13 | } 14 | addMessage(message: Message) { 15 | this.chats.push(message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mediator-pattern/chatRoomUser.ts: -------------------------------------------------------------------------------- 1 | import { IMediator, IUser, Message } from "./types.ts"; 2 | 3 | // Concrete Component 4 | export class ChatRoomUser implements IUser { 5 | name: string; 6 | id: string; 7 | private mediator: IMediator; 8 | private rooms: Set; 9 | constructor(name: string, mediator: IMediator) { 10 | this.name = name; 11 | this.id = crypto.randomUUID(); 12 | this.mediator = mediator; 13 | this.rooms = new Set(); 14 | } 15 | sendMessage(roomName: string, content: string): void { 16 | const message: Message = { 17 | from: this.name, 18 | content, 19 | timestamp: Date.now(), 20 | id: crypto.randomUUID(), 21 | }; 22 | this.mediator.notify(roomName, "message-sent", this.name, undefined, message); 23 | } 24 | receiveMessage(roomName: string, message: Message, sender: string): void { 25 | console.log( 26 | `\tnotification to ${this.name}: msg from ${sender} in ${roomName} \n\t ${message.content.slice(0, 120)}...` 27 | ); 28 | } 29 | join(roomName: string): void { 30 | this.rooms.add(roomName); 31 | this.mediator.notify(roomName, "user-joined", this); 32 | } 33 | exit(roomName: string): void { 34 | this.rooms.delete(roomName); 35 | this.mediator.notify(roomName, "user-left", this.name); 36 | } 37 | get userRooms() { 38 | return Array.from(this.rooms); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mediator-pattern/mediator.ts: -------------------------------------------------------------------------------- 1 | import { ChatRoom } from "./chatRoom.ts"; 2 | import { ChatRoomUser } from "./chatRoomUser.ts"; 3 | import { Event, IMediator, Message } from "./types.ts"; 4 | 5 | /** 6 | * Concrete Mediators coordinates communication between components (chatRoom and chatRoomUser). 7 | */ 8 | export class Mediator implements IMediator { 9 | private roomUsersMap: WeakMap; 10 | private roomsMap: Map; 11 | constructor() { 12 | this.roomUsersMap = new WeakMap(); 13 | this.roomsMap = new Map(); 14 | } 15 | notify(namespace: string, event: Event, sender: unknown, _receiver?: unknown, data?: unknown): void { 16 | if (namespace === "global" && event === "room-created") { 17 | const room = sender as ChatRoom; 18 | this.roomsMap.set(room.name, room); 19 | console.log(`${room.name} | ${event}`); 20 | return; 21 | } 22 | const room = this.roomsMap.get(namespace); 23 | if (!room) return; 24 | const roomUsers = this.roomUsersMap.get(room) || []; 25 | switch (event) { 26 | case "message-sent": { 27 | const message = data as Message; 28 | const senderName = sender as string; 29 | console.log(`${room.name} | ${event} | by ${sender} `); 30 | roomUsers 31 | .filter(user => user.name != senderName) 32 | .forEach(user => user.receiveMessage(room.name, message, senderName)); 33 | room.addMessage(message); 34 | break; 35 | } 36 | 37 | case "user-left": { 38 | const userName = sender as string; 39 | console.log(`${room.name} | ${event} | ${sender}`); 40 | this.roomUsersMap.set( 41 | room, 42 | roomUsers.filter(user => user.name != userName) 43 | ); 44 | break; 45 | } 46 | 47 | case "user-joined": { 48 | const user = sender as ChatRoomUser; 49 | roomUsers.push(user); 50 | console.log(`${room.name} | ${event} | ${user.name}`); 51 | this.roomUsersMap.set(room, roomUsers); 52 | break; 53 | } 54 | default: { 55 | console.error("invalid event"); 56 | break; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mediator-pattern/types.ts: -------------------------------------------------------------------------------- 1 | export type Message = { 2 | from: string; 3 | to?: string; 4 | timestamp: number; 5 | content: string; 6 | id: string; 7 | }; 8 | 9 | export type Event = "user-left" | "user-joined" | "message-sent" | "room-created"; 10 | 11 | export interface IUser { 12 | name: string; 13 | id: string; 14 | sendMessage(roomName: string, content: string): void; 15 | join(roomName: string): void; 16 | exit(roomName: string): void; 17 | } 18 | 19 | /** 20 | * The Mediator interface declares a method used by components to notify the 21 | * mediator about various events. The Mediator may react to these events and 22 | * pass the execution to other components. 23 | */ 24 | 25 | export interface IMediator { 26 | notify(namespace: string, event: Event, sender: unknown, receiver?: unknown, data?: unknown): void; 27 | } 28 | -------------------------------------------------------------------------------- /mediator-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { ChatRoom } from "./chatRoom.ts"; 2 | import { ChatRoomUser } from "./chatRoomUser.ts"; 3 | import { Mediator } from "./mediator.ts"; 4 | 5 | const mediator = new Mediator(); 6 | 7 | const learnPythonGroup = new ChatRoom("learnPython", mediator); 8 | 9 | const john = new ChatRoomUser("john", mediator); 10 | const jane = new ChatRoomUser("jane", mediator); 11 | const janice = new ChatRoomUser("janice", mediator); 12 | 13 | john.join(learnPythonGroup.name); 14 | jane.join(learnPythonGroup.name); 15 | janice.join(learnPythonGroup.name); 16 | 17 | janice.sendMessage(learnPythonGroup.name, "Hi! everyone. What's the best place to learn python?"); 18 | john.sendMessage(learnPythonGroup.name, "You can start with: Automate the boring stuff with Python"); 19 | 20 | /** 21 | * 22 | learnPython | room-created 23 | learnPython | user-joined | john 24 | learnPython | user-joined | jane 25 | learnPython | user-joined | janice 26 | learnPython | message-sent | by janice 27 | notification to john: msg from janice in learnPython 28 | Hi! everyone. What's the best place to learn python?... 29 | notification to jane: msg from janice in learnPython 30 | Hi! everyone. What's the best place to learn python?... 31 | learnPython | message-sent | by john 32 | notification to jane: msg from john in learnPython 33 | You can start with: Automate the boring stuff with Python... 34 | notification to janice: msg from john in learnPython 35 | You can start with: Automate the boring stuff with Python... 36 | */ 37 | 38 | janice.exit(learnPythonGroup.name); 39 | 40 | const learnTS = new ChatRoom("learnTS", mediator); 41 | const emily = new ChatRoomUser("emily", mediator); 42 | 43 | janice.join(learnTS.name); 44 | emily.join(learnTS.name); 45 | 46 | emily.sendMessage(learnTS.name, "helloooo woooorld"); 47 | 48 | /** 49 | learnPython | user-left | janice 50 | learnTS | room-created 51 | learnTS | user-joined | janice 52 | learnTS | user-joined | emily 53 | learnTS | message-sent | by emily 54 | notification to janice: msg from emily in learnTS 55 | helloooo woooorld... 56 | */ 57 | -------------------------------------------------------------------------------- /observer-pattern/README.md: -------------------------------------------------------------------------------- 1 | # Observer Pattern 2 | 3 | The Observer Pattern is a behavioral design pattern used to establish a one-to-many dependency between objects. When one object (called the subject) changes, all its dependent objects (observers) are automatically notified and updated. 4 | The subject and observer are loosely connected, making it easy to extend without modifying existing code. 5 | 6 | It differs with a pure pub/sub system, publishers and subscribers often don't know about each other directly, while in the traditional observer pattern, the subject maintains direct references to its observers. 7 | 8 | ## Key Concepts 9 | 10 | 1. **Subject (Observable)**: This is the core component that holds the state and sends notifications. It maintains a list of observers and notifies them of state changes. 11 | 12 | 2. **Observer Interface**: This defines the contract that all observers must follow. It specifies how observers will receive updates. 13 | 14 | 3. **Concrete Observers**: These are the actual implementations that receive and react to updates from the subject. 15 | 16 | ## Use Cases 17 | 18 | Observer pattern is very widely used in modern software. You see it in most places. Redux's store subscriptions, RxJS's Observable streams, Node.js EventEmitter, WebSocket connections, DOM event listeners 19 | 20 | ### Similar Patterns 21 | 22 | ```js 23 | // Observer Pattern 24 | subject.attach(observer); 25 | 26 | // Pub/Sub Pattern 27 | messageBroker.subscribe("topic", callback); 28 | 29 | // Event-Emitter Pattern 30 | eventEmitter.on("eventName", handler); 31 | 32 | // Listener Pattern 33 | element.addEventListener("click", listener); 34 | ``` 35 | 36 | ## Further Reading 37 | 38 | https://www.patterns.dev/vanilla/observer-pattern 39 | 40 | https://refactoring.guru/design-patterns/observer 41 | 42 | https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter 43 | -------------------------------------------------------------------------------- /observer-pattern/example1/observer.ts: -------------------------------------------------------------------------------- 1 | import { IObserver } from "../types.ts"; 2 | 3 | // Concrete Observer 4 | export class Observer implements IObserver { 5 | name: string; 6 | constructor(name: string) { 7 | this.name = name; 8 | } 9 | update(data: T): void { 10 | console.log(`data received to observer: ${this.name} | ${JSON.stringify(data)}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /observer-pattern/example1/subject.ts: -------------------------------------------------------------------------------- 1 | import { IObserver, ISubject } from "../types.ts"; 2 | 3 | // Concrete Subject 4 | export class Subject implements ISubject { 5 | private observers: Set>; 6 | name: string; 7 | constructor(name: string) { 8 | this.observers = new Set(); 9 | this.name = name; 10 | } 11 | attach(observer: IObserver): void { 12 | if (!this.observers.has(observer)) this.observers.add(observer); 13 | } 14 | detach(observer: IObserver): void { 15 | this.observers.delete(observer); 16 | } 17 | notify(data: T): void { 18 | this.observers.forEach(o => o.update(data)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /observer-pattern/example2/observers.ts: -------------------------------------------------------------------------------- 1 | import { IObserver } from "../types.ts"; 2 | import { ProductPrice } from "./types.ts"; 3 | 4 | // Concrete Observers 5 | 6 | // updates ui whenever on price change 7 | export class UIUpdater implements IObserver { 8 | update(data: ProductPrice): void { 9 | console.log(`UI update: Price updated to ${data.price}${data.currency} for ${data.productId}`); 10 | } 11 | } 12 | 13 | // updates the inventory on price change 14 | export class Investory implements IObserver { 15 | update(data: ProductPrice): void { 16 | console.log(`Inventory System: Updating price records for product ${data.productId}`); 17 | } 18 | } 19 | 20 | // notifies users of price change 21 | export class Notifier implements IObserver { 22 | private readonly PRICE_CHANGE_THRESHOLD = 0.1; // 10% 23 | private productMap: Map; 24 | constructor() { 25 | this.productMap = new Map(); 26 | } 27 | update(data: ProductPrice): void { 28 | const previousPrice = this.productMap.get(data.productId); 29 | if (previousPrice) { 30 | const diff = Math.abs(data.price - previousPrice) / previousPrice; 31 | if (diff >= this.PRICE_CHANGE_THRESHOLD) { 32 | console.log(`Users notified of price change for ${data.productId} | Discount - ${Math.floor(diff * 100)}%`); 33 | } 34 | } 35 | this.productMap.set(data.productId, data.price); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /observer-pattern/example2/subject.ts: -------------------------------------------------------------------------------- 1 | // Concrete Subject (observable) 2 | 3 | import { IObserver, ISubject } from "../types.ts"; 4 | import { ProductPrice } from "./types.ts"; 5 | 6 | export class PriceMonitor implements ISubject { 7 | private observers: Set>; 8 | 9 | constructor() { 10 | this.observers = new Set(); 11 | } 12 | 13 | attach(observer: IObserver): void { 14 | if (!this.observers.has(observer)) this.observers.add(observer); 15 | } 16 | 17 | detach(observer: IObserver): void { 18 | this.observers.delete(observer); 19 | } 20 | 21 | notify(data: ProductPrice): void { 22 | this.observers.forEach(o => o.update(data)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /observer-pattern/example2/types.ts: -------------------------------------------------------------------------------- 1 | export interface ProductPrice { 2 | productId: string; 3 | price: number; 4 | currency: string; 5 | lastUpdated: number; 6 | } 7 | -------------------------------------------------------------------------------- /observer-pattern/types.ts: -------------------------------------------------------------------------------- 1 | // Observer Interface 2 | export interface IObserver { 3 | update(data: T): void; 4 | } 5 | 6 | // Subject Interface 7 | export interface ISubject { 8 | attach(observer: IObserver): void; 9 | detach(observer: IObserver): void; 10 | notify(data: T): void; 11 | } 12 | -------------------------------------------------------------------------------- /observer-pattern/usage.ts: -------------------------------------------------------------------------------- 1 | import { Observer } from "./example1/observer.ts"; 2 | import { Subject } from "./example1/subject.ts"; 3 | import { Investory, Notifier, UIUpdater } from "./example2/observers.ts"; 4 | import { PriceMonitor } from "./example2/subject.ts"; 5 | 6 | // Example 1 7 | 8 | type Video = { 9 | title: string; 10 | duration: number; 11 | }; 12 | 13 | const ytchannel = new Subject