Applying factory method pattern: Vuejs with firebase and supabase use case
Tags: vue, typescript, frontend, designpatterns
Published: 2023-08-26 17:52:10

Imagine this scenario.
You are implementing a web application for several clients. 
Each client wants to use a different backend service : _firebase_ and _supabase_.
What would you do?

Implementing two versions of the same app, one to deal with firebase, the other with supabase?

What if a client changes his mind and wants to use another backend service: AWS amplify? Would you change the codebase one more time to fit this new requirement? 
It does not make any sense.

Since this is an issue, our codebase should not rely on any specific backend service. Instead, the codebase must handle whatever backend service we need and, most importantly, be out of proportion to the number of backend services we may use. 
That is to say, whenever we add or update a new backend service, it needs to be easy to integrate into our application with regard to time and complexity. This is achievable only when our implementation is based on abstraction rather than concretion.

Here is where design patterns and SOLID principles come in handy.

In this post, we will try to resolve this issue by applying the factory method pattern. It is not a _unique_ solution, nor maybe a "must do" solution; it is rather an exploratory operation to discover its benefits in regard to this specific issue.
<hr/>
##1-Factory Method pattern: 
&nbsp;
The factory method is a creational pattern, its primary task is to create objects with respect to a specific design. The method responsible for that is the _"factory method"_, which belongs to a _"factory class"_. Picking out this pattern is the best option when we don't know the object we want to instantiate until runtime.

For example, an object `"Animal"` won't be instantiated directly inside the client code instead, this will be delegated to the factory method :

`createAnimal(){return new Animal()}
`
let's deep dive into this pattern. Below a UML class diagram for the factory method pattern: 


![UML class diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5h6ke05ymvpk4d2a3mwy.jpg)
<figcaption>UML class diagram of factory method pattern</figcaption>
&nbsp;

**Creator**: It is an _abstract_ class representing a factory class signature.

**Creator1**: A _concrete_ implementation of the factory class contains a _concrete_ factory method.

**Product**: In the factory method pattern, the product is our object to be instantiated. It is an interface representing our object signature. 

**Product1**: A _concrete_ implementation of the object.

You may notice that we can have multiple factories and products.

**_Then, how could this design be applied to my web application?_**
<hr/>

##2-Factory method pattern in typescript:
&nbsp;
In brief, my app _(find your coach)_ is made to help users connect with coaches and send them mentorship requests.

I want my application to handle several backend services. Until now, I needed Supabase and Firebase.

Backend service choice will be done inside the `.env` variable _VITE_APP_DB_. According to this value, one of the backend services will be instantiated.

These services contain data about _coaches_ and _requests_. My main tasks will be getting a _list of coach_, getting a _list of requests_ and _adding a request_. 
   

#### Design our specific use case

Below is a UML class diagram of the factory method pattern applied to our case.

![findcoach-uml-class](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vhw12mobso5vrgyzfy4.png)
<figcaption>UML class diagram of factory method pattern: find your coach application</figcaption>

&nbsp;

At this level, we transformed a general factory method pattern design into a more specific case: _find your coach_ app

**Creator** _-->_ dbCreator

**Creator1** _-->_ concreteDbCreator

**Product** _-->_ IDatabase

**Product1** _-->_ FirebaseDb

**Product2** _-->_ SupabaseDb
&nbsp;


#### Code implementation:
 
Let's take one more step. Now we are going to implement our design!

**_IDatabase:_**

Signature to the required backend operations.

```ts
import type { Request } from '@/types/Request'

export interface IDataBase {
  getRequests(email: string): Promise<any>
  addRequest(data: Request): Promise<any>
  getCoaches(): Promise<any>
}

```

**_dbCreator:_**

Our factory class signature. A concrete creator should implement this class. 

```ts
import type { IDataBase } from './IDataBase'
export abstract class DbCreator {
  //factory method
  public abstract createDb(dbType: string): IDataBase
}

```

**_FirebaseDb:_** _(full code:[FirebaseDb](https://github.com/hssanbzlm/find-coach/blob/master/src/db/FirebaseDb.ts))_

```ts
import{Database,getDatabase,ref,equalTo,query,get,
orderByChild
,set} from 'firebase/database'
import type { IDataBase } from './IDataBase'
import type { Coach } from '@/types/Coach'
import type { Request } from '@/types/Request'
import { initializeApp } from 'firebase/app'
const firebaseConfig = {}
export class FirebaseDb implements IDataBase {
  private db: Database
  constructor() {
    initializeApp(firebaseConfig)
    this.db = getDatabase()
  }
  getRequests(email: string) {//}
  addRequest(data: Request) {
    return set(ref(this.db, 'requests/' + data.time), data)
  }
  getCoaches() {}
```
**_SupabaseDb:_** _(full code:[SupabaseDb](https://github.com/hssanbzlm/find-coach/blob/master/src/db/SupabaseDb.ts))_

```ts
import { createClient } from '@supabase/supabase-js'
import type { IDataBase } from './IDataBase'
import type { Request } from '@/types/Request'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseKey = import.meta.env.VITE_SUPABASE_KEY
export class SupabaseDb implements IDataBase {
  private supaBase
  constructor() {
    this.supaBase = createClient(supabaseUrl, supabaseKey)
  }
  getRequests(email: string) {}
  addRequest(data: Request) {  }
  getCoaches() {   }
```
Supabase and Firebase must implement IDatabase.

**_ConcreteDbCreator:_**

This is the core of the factory method pattern. Based on the `dbType` value, a particular backend service will be instantiated through `if else` statements.

```ts
import { DbCreator } from './DbCreator'
import { FirebaseDb } from './FirebaseDb'
import { SupabaseDb } from './SupabaseDb'

export class ConcreteDbCreator extends DbCreator {
  public createDb(dbType: string) {
    if (dbType === 'firebase') return new FirebaseDb()
    else return new SupabaseDb()
  }
}

```
**_Client code (main.ts):_**

In this context, client code is where our object is instantiated.
As mentioned above, the factory method is called with the `dbType` param obtained from `.env` file.
  

```ts
//
const appDB = import.meta.env.VITE_APP_DB
const appDataBase = new ConcreteDbCreator().createDb(appDB)
//
```
&nbsp;

---


##3-Inject a database instance into Vue:

So far, we have designed and implemented our pattern in typescript. The next step is to inject the database instance into vue to make it accessible across the application.

_Vuejs_ comes up with a _dependency injection_ mechanism to pass data from a parent component to a child: _Provide/Inject_.

We will use this mechanism to inject our database instance. 
First _provide()_

```ts
app.provide('appDataBase', appDataBase)
```

Then _inject_ it wherever we want.
```ts
  const appDataBase: IDataBase = inject('appDataBase')!
```
&nbsp;

#### Dependency injection with Pinia


In _find your coach_ app, I use _Pinia_ for state management. Unfortunately, the _Provide/Inject_ mechanism is not accepted in _pinia store_. Therefore, I need a different way to inject my database instance.

Yet, _Pinia_ provides another way to use external properties by adding an attribute to pinia store object containing the required properties. This object must be wrapped inside `markRaw()`.

```ts
pinia.use(({ store }) => {
  store.appDataBase = markRaw(appDataBase)
})
```

Then our database instance will be available through `this` inside the store.

```ts
import { defineStore } from 'pinia'
import type { Coach } from '@/types/Coach'

export const useCoachesStore = defineStore('coaches', {
  state: () => ({
    coaches: [] as Coach[],
  })
  actions: {
    fetchCoaches() {
      this.appDataBase
        .getCoaches()
        .then((data: Coach[]) => {
          data.forEach((coach: Coach) => {
            this.coaches.push(coach)
          })
        })
    },
  },
})

```
 

---

##4-Notes:
 
You may wonder, why we don't use simply an interface and several services implementations, and instantiate the service we need since the database service will remain the same at runtime.

Applying the factory method pattern has mainly two benefits:

 
  **-Separation of concern:** Choosing adequate service is the factory method job. The client code will remain unchangeable, whatever service we decide to use.

  **-Limit code modification and mistakes:** Adding a new service requires only changing the _.env variable_, adding a _concrete class_ and adding a _new conditional statement_. It is a _standardized_ task. 


---

##5-Conclusion:

Throughout this post, we successfully implemented the factory method pattern to meet our application need for _flexibility_ in choosing backend service. 

This is done through the `.env` variable which specifies the backend type, and the _factory method_.

Now, we have the freedom to choose between _firebase_ and _supabase_ by just changing one variable!

Anytime we want to use another alternative, we have to follow these steps :
   

- Create a _concrete_ class of specified service. This concrete class must _implement_ IDatabase interface

- Add one more _conditional statement_ in the _factory method_ to instantiate this service according to the `dbType` param.

- On any occasion we want to use this service, we just need to update the `.env` variable: _VITE_APP_DB_.

THANK you for taking the time to read this article.

[Full project code](https://github.com/hssanbzlm/find-coach)