Skip to content

Instantly share code, notes, and snippets.

@34fame
Created September 15, 2022 15:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 34fame/612cd8c4abc452ffd7220d438a79315b to your computer and use it in GitHub Desktop.
Save 34fame/612cd8c4abc452ffd7220d438a79315b to your computer and use it in GitHub Desktop.
Article: Design Patterns Using Functional JavaScript

Design Patterns in JavaScript

Created: June 15, 2022 11:30 AM Last Edited Time: June 19, 2022 4:19 PM Status: In Progress Type: Tech Notes

Problem Statements

  • I am working on a project that is using an SQL database (e.g. PostgreSQL) and the customer decides they want to move to a NoSQL database (e.g. MongooseDb). Now I have to go through my entire project to refactor everything to use this new database.
  • I am starting a new project. The customer wants to manage users and products. My last project had similar requirements. I can copy/paste code from my previous project but, due to the way the code was written, there are lots of changes to be made so I may be better off just starting from scratch.
  • I have built several projects for numerous customers and I am responsible for maintaining all of these projects. This becomes too much for me to handle alone so I hire more developers. Each customer request for fixes or features is written in their respective project. Even though I may have multiple customers asking for the same feature I end up with different implementations created by different developers. When a fix is implemented, it is likely only done in one of the projects and we may run into the same problem in our other projects in the future.

These problems result in numerous negative outcomes such as inefficient use of time, inability to scale, increased security risks, to name a few.

Solution Overview

As a developer, I create numerous projects with varying architectures. As I work on different projects I want to build upon my “toolkit” to increase my efficiency, reduce mistakes, and simply adhere to best practices. I have created a basic example of this toolkit that implements database integration and model management. There would certainly be more to this toolkit but hopefully this is enough of a “real” example to get you started on building your own toolkit.

This sample code implements Abstract Factory design pattern. The purpose of using this pattern is to adhere to the Open/Closed Principle (OCP) in SOLID. This is implemented such that our “client” will interact with our Abstract Factories rather than interacting directly with numerous Factories. Our “client” code won’t need to change when we add a new Factory. Our Abstract Factories will require minimal or no code changes to implement a new Factory.

This example has an Abstract Factory called DbStoreFactory and an Abstract Factory called ModelFactory. The goal of the DbStoreFactory is to abstract calls to our databases so that clients don’t need to be aware of what databases we are using, their distinct schemas, and their unique functions. The goal of the ModelFactory is to abstract and normalize the operations associated with any model we need to manage. A “model” would be an entity type or collection that we will (likely) store in some database table. For example, we may have a “users” model that defines the properties (attributes or schema) and methods (functions) required to manage a “user”.

Model Factory

Overview

As mentioned before, the primary purpose of using the Abstract Factory design pattern is to make sure we are implementing the OCP. The secondary benefit to this design pattern is reusability across projects. A perfect example of this is our ModelFactory Abstract Factory.

When building a new application, one of the first steps is to define your models. Let’s say this application is a blogging application. At a minimum we need to define a user and post model. Each model would include its properties (e.g. JSON schema) and its methods (e.g. CRUD operations).

Down the road we are commissioned to build another application. This application is a social messaging application. Once again, we need models for users and posts. In addition, we need a model for channels. Ideally, we want to reuse our code from the last project and extend it to include the new model.

Implementation

The first piece of the implementation is to create our Abstract Factory that we will name ModelFactory. Then, for every model, we will create a Concrete Factory, such as UserModelFactory and PostModelFactory.

The ModelFactory will include any properties or methods that are shared amongst all models. For example, you could assign a type property for all models. The ModelFactory will have a parameter that tells it which model the “client” wants to interface with.

function ModelFactory(model) {
	return {
		// Properties
		type: model,
	
		// Methods
		list: function() {
			console.log(`Ran 'list()' for ${model}`)
		},

		// Model Injection
		// ...coming soon...
	}
}

Now we can create our UserModelFactory and create the interface with ModelFactory. In the ModelFactory we need to look at the model parameter to know which model to inject. We could use an if or switch statement but I’ve decided to use an object for a quicker reference.

const models = {
  users: new UserModelFactory(),
}
function ModelFactory(model) {
  return {
    // Properties
    type: model,

    // Methods
    list: function () {
      console.log(`ModelFactory: Ran 'list()' for ${model}`)
    },

    // Model Injection
    ...models[model],
  }
}
function UserModelFactory() {
  return {
    create: function (user) {
      console.log(`UserModelFactory: Ran 'create()'`)
    },
  }
}

The second piece of the implementation is in our “client” code. (Client code refers to any code that interfaces with our Factories).

const $user = new ModelFactory('users')
console.log($user)
$user.list()
$user.create({ name: 'Troy' })

/*
{
	type: 'users',
	list: f list(),
	create: f create()
} 
ModelFactory: Ran 'list()' for users
UserModelFactory: Ran 'create()'
*/

From this implementation you can see that when we instantiated the users model we ended up with the type property and list method from ModelFactory and the create method from the UserModelFactory.

Now, let’s add our posts model.

const models = {
  users: new UserModelFactory(),
	posts: new PostModelFactory()
}
function PostModelFactory() {
  return {
    create: function (obj) {
      console.log(`PostModelFactory: Ran 'create()'`)
    },
  }
}

So we added our new model to our model definitions. Then we created our PostModelFactory. Notice we did not have to make any change to our ModelFactory!

Now let’s interface with the posts model in our “client”.

const $post = new ModelFactory('posts')
console.log($post)
$post.list()
$post.create({ author: 'Troy', message: 'Hello, World' })

/*
{
	type: 'posts',
	list: f list(),
	create: f create()
} 
ModelFactory: Ran 'list()' for posts
PostModelFactory: Ran 'create()'
*/

Voila! We “extended” our ModelFactory and made it very simple for the “client” to use because it’s used in the exact same way as with the users model.

Now we need to add our database integration(s) so our models can perform their CRUD operations.

Database Factory

Overview

In our Problem Statements we talked about how a change in database platforms had a significant ripple effect across our entire project. We don’t want that to happen going forward and we will, once again, use the Abstract Factory model and create our DbStoreFactory. FWIW, I chose this name because I will also be adding a FileStoreFactory for interfacing with file storage services and these names make sense to me.

For the sake of this example, let’s say we will be using the Firebase Firestore and Firebase Realtime databases. We won’t actually be coding that integration here but we will show how it will be structured.

Implementation

The pattern for how this will work is the same pattern we used for the ModelFactory so I will just show the outcome of the DbStoreFactory implementation here.

const stores = {
  FirebaseFirestore: new FirebaseFirestoreDbStoreFactory(),
  FirebaseRealtime: new FirebaseRealtimeDbStoreFactory(),
}
function DbStoreFactory(store) {
  return {
    // Properties
    type: store,

    // Methods
    list: function() {},
    create: function() {},
    update: function() {},
    delete: function() {},

    // DbStore Injection
    ...stores[store],
  }
}
function FirebaseFirestoreDbStoreFactory() {
  return {
    list: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'list()'`)
    },
    create: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'create()'`)
    },
    update: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'update()'`)
    },
    delete: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'delete()'`)
    },
  }
}
function FirebaseRealtimeDbStoreFactory() {
  return {
    list: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'list()'`)
    },
    create: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'create()'`)
    },
    update: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'update()'`)
    },
    delete: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'delete()'`)
    },
  }
}

There we have our database integrations via the Abstract Factory method. Obviously this isn’t actually talking to the database platforms but that isn’t the point of this.

One subtle difference in the DbStoreFactory is that I included the methods that will be found in every Concrete Factory. I like that because I can just go to the DbStoreFactory code to see all of the methods that will be available in the different Concrete Factories.

Now we need the “client” piece of the implementation. In this case, the model factories are the “client” because that is where we will make the database calls. In this project example we will be using Firebase Firestore for all models. That means we will instantiate the database session directly in ModelFactory because it is shared across all models. Then we update the list method to use the database integration.

function ModelFactory(model) {
  return {
    // Properties
    type: model,
    db: new DbStoreFactory('FirebaseFirestore'),

    // Methods
    list: function () {
      console.log(`ModelFactory: Ran 'list()' for ${model}`)
      return this.db.list()
    },

    // Model Injection
    ...models[model],
  }
}

From the perspective of ModelFactory, changing a database platform, say from Firebase Firestore to Firebase Realtime, will only require a change to the db property definition so that it points to the new store definition. We just have to ensure that DbStoreFactory provides the normalized set of methods. Meaning, the models only need to worry about the database list method. It is up to the different database factories to translate that to the database platforms way of returning a list.

Next we need to update our model factories to implement the database calls.

function UserModelFactory() {
  return {
    create: function (obj) {
      console.log(`UserModelFactory: Ran 'create()'`)
      this.db.create(obj)
    },
  }
}
function PostModelFactory() {
  return {
    create: function (obj) {
      console.log(`PostModelFactory: Ran 'create()'`)
      this.db.create(obj)
    },
  }
}

We should never need to change the database code in any of our model factories… ever! We can extend them by adding new methods but we should not change the existing once we are deployed.

There is no need to change our “client” code because the database interfaces are completely abstracted.

const $user = new ModelFactory('users')
console.log($user)
$user.list()
$user.create({ name: 'Troy' })

const $post = new ModelFactory('posts')
console.log($post)
$post.list()
$post.create({ author: 'Troy', message: 'Hello, World' })

/*
{
  type: 'users',
  db: {
    type: 'FirebaseFirestore',
    list: ƒ list(),
    create: ƒ create(),
    update: ƒ update(),
    delete: ƒ delete()
  },
  list: ƒ list(),
  create: ƒ create()
}
"ModelFactory: Ran 'list()' for users"
"FirebaseFirestoreDbStoreFactory: Ran 'list()'"
"UserModelFactory: Ran 'create()'"
"FirebaseFirestoreDbStoreFactory: Ran 'create()'"
{
  type: 'posts',
  db: {
    type: 'FirebaseFirestore',
    list: ƒ list(),
    create: ƒ create(),
    update: ƒ update(),
    delete: ƒ delete()
  },
  list: ƒ list(),
  create: ƒ create()
}
"ModelFactory: Ran 'list()' for posts"
"FirebaseFirestoreDbStoreFactory: Ran 'list()'"
"PostModelFactory: Ran 'create()'"
"FirebaseFirestoreDbStoreFactory: Ran 'create()'"
*/
const stores = {
  FirebaseFirestore: new FirebaseFirestoreDbStoreFactory(),
  FirebaseRealtime: new FirebaseRealtimeDbStoreFactory(),
}

function DbStoreFactory(store) {
  return {
    // Properties
    type: store,

    // Methods
    list: function () {},
    create: function () {},
    update: function () {},
    delete: function () {},

    // DbStore Injection
    ...stores[store],
  }
}

function FirebaseFirestoreDbStoreFactory() {
  return {
    list: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'list()'`)
    },
    create: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'create()'`)
    },
    update: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'update()'`)
    },
    delete: function () {
      console.log(`FirebaseFirestoreDbStoreFactory: Ran 'delete()'`)
    },
  }
}

function FirebaseRealtimeDbStoreFactory() {
  return {
    list: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'list()'`)
    },
    create: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'create()'`)
    },
    update: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'update()'`)
    },
    delete: function () {
      console.log(`FirebaseRealtimeDbStoreFactory: Ran 'delete()'`)
    },
  }
}

const models = {
  users: new UserModelFactory(),
  posts: new PostModelFactory(),
}

function ModelFactory(model) {
  return {
    // Properties
    type: model,
    db: new DbStoreFactory('FirebaseFirestore'),

    // Methods
    list: function () {
      console.log(`ModelFactory: Ran 'list()' for ${model}`)
      return this.db.list()
    },

    // Model Injection
    ...models[model],
  }
}

function UserModelFactory() {
  return {
    create: function (obj) {
      console.log(`UserModelFactory: Ran 'create()'`)
      this.db.create(obj)
    },
  }
}

function PostModelFactory() {
  return {
    create: function (obj) {
      console.log(`PostModelFactory: Ran 'create()'`)
      this.db.create(obj)
    },
  }
}

const $user = new ModelFactory('users')
console.log($user)
$user.list()
$user.create({ name: 'Troy' })

const $post = new ModelFactory('posts')
console.log($post)
$post.list()
$post.create({ author: 'Troy', message: 'Hello, World' })
/* Import Libraries */
const { Validator } = require('jsonschema')
const v = new Validator()
/* END Import Libraries */

/* Constants */
const stores = {
  FirebaseFirestore: new DbFirebaseFirestoreFactory(),
  FirebaseRealtime: () => {},
}

const models = {
  products: {
    init: new ProductModel(),
    schema: {
      type: 'object',
    },
  },
  users: {
    init: new UserModel(),
    schema: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        age: { type: 'number' },
      },
      required: ['name'],
    },
  },
}

const validateJson = (obj, schema) => {
  log('>>> validateJson', obj, schema)
  const test = v.validate(obj, schema)
  if (test.valid) {
    log('<<< validateJson', { valid: true })
    return { valid: true }
  }
  log('<<< validateJson', { valid: false, errors: test.errors })
  return { valid: false, errors: test.errors }
}

const log = console.log
/* END Constants */

/* Database Factories */
function DbStoreFactory(store) {
  return stores[store]
}

function DbFirebaseFirestoreFactory() {
  return {
    list: function ({ collection, orderBy, limit }) {
      log('>>> DbFirebaseFirestoreFactory.list', collection, orderBy, limit)
      return new Promise((res, rej) => {
        setTimeout(() => {
          log('<<< DbFirebaseFirestoreFactory.list', [])
          res([])
        }, 500)
      })
    },
  }
}
/* END Datasbase Factories */

/* Model Factories */
function ModelFactory(model) {
  const db = new DbStoreFactory('FirebaseFirestore')

  return {
    list: async function () {
      log('>>> ModelFactory.list', model)
      const result = await db.list({ collection: model })
      log('<<< ModelFactory.list', result)
      return result
    },

    ...models[model].init,
  }
}

function UserModel() {
  return {
    create: function (userObj) {
      log('>>> UserModel.create', userObj)
      const test = validateJson(userObj, models.users.schema)

      if (!test.valid) {
        for (let error of test.errors) {
          console.error(error.stack)
        }
        log('<<< UserModel.create', false)
        return false
      }

      userObj = {
        ...userObj,
        id: new Date().valueOf(),
      }
      log('<<< UserModel.create', userObj.id)
      return userObj.id
    },
  }
}

function ProductModel() {
  return {
    create: function (productObj) {
      log('>>> ProductModel.create', productObj)
      const test = validateJson(productObj, models.products.schema)

      if (!test.valid) {
        for (let error of test.errors) {
          console.error(error.stack)
        }
        log('<<< ProductModel.create', false)
        return false
      }

      productObj = {
        ...productObj,
        id: new Date().valueOf(),
      }
      log('<<< ProductModel.create', productObj.id)
      return productObj.id
    },
  }
}
/* END Model Factories */

/* Client */
const client = async () => {
  const user = new ModelFactory('users')
  const users = await user.list()
  user.create({ name: 'Troy', age: 49 })

  const product = new ModelFactory('products')
  const products = await product.list()
  product.create({ name: 'soap' })
}

client()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment