Logo

dev-resources.site

for different kinds of informations.

Utilizing Adapter pattern for hiding external libraries

Published at
9/25/2024
Categories
typescript
enterprisepatterns
domaindrivendesign
Author
rshriyan
Author
8 person written this
rshriyan
open
Utilizing Adapter pattern for hiding external libraries

When working on a project, integrating external libraries is often necessary. However, tightly coupling these libraries with the code can make it difficult to swap them out or refactor without risking regressions. That’s why I started using the Adapter Pattern—it hides external dependencies, allowing seamless library replacement without breaking the rest of the application.

My application code dealt directly with momentjs and was tightly coupled to it. Since momentjs was deprecated, I wanted to switch to another library: dayjs. Switching to a new library meant, not only finding and updating the references but also retesting every functionality the refactor impacts. Instead of replacing momentjs constructs with dayjs everywhere, I decided to abstract it behind an adapter. This allowed me to keep the rest of my codebase library-agnostic while having the flexibility to swap in another library if necessary—without fear of introducing bugs or needing to retest every component.

Let’s break down the key parts of this implementation:

  • Private Constructor:
    I made the constructor private to enforce the abstraction. This way, only the DateTimeAdapter class manages the dayjs instance internally, ensuring no external code relies on dayjs directly.

  • Returning New Instances:
    Methods like add() return a new DateTimeAdapter instance. This ensures immutability, which is crucial when working with dates. Mutating the current instance would lead to unexpected behavior in the rest of the system.

  • Immutability
    Returning New Instances: When operations like add, subtract, set, or clone are performed, you return a new instance of DateTimeAdapter instead of modifying the existing one. This practice ensures immutability, which is vital for entities like dates, where accidental mutations could lead to unexpected behavior across the application.

One of the key principles here is “writing against a contract.” In this case, the contract is the DateTimeAdapter class, which defines the interface my application code interacts with. The external library (dayjs, in this example) becomes a detail that can be swapped or replaced. This practice of coding to an interface, not an implementation, keeps the system flexible and easier to maintain.

// Abstract an external library for dealing with dates, datetime.
import dayjs, { OpUnitType, QUnitType, UnitType } from 'dayjs';

import timezone from 'dayjs/plugin/timezone';
import weekday from 'dayjs/plugin/weekday';
import utc from 'dayjs/plugin/utc';

import { isNil } from '@utils';
import { LocalDateTimeString, LocalTimeString } from '../utils/local_date_time';

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(weekday);

export enum Timezone {
  UTC = 'UTC',
  PST = 'America/Los_Angeles',
  CST = 'America/Chicago',
  EST = 'America/New_York',
}
// Mimics the types accepted by Dayjs, but purposely removes Dayjs as one of the accepted types.
export type DateTimeAdapterTypes = string | number | Date | null | undefined;

export class DateTimeAdapter {
  // Holds the library core instance, this can be swapped with another library like moment
  private instance: dayjs.Dayjs;

  // Purposely made private for internal use, we cannot expose this outside since that
  // leaks the library specific type(dayjs.Dayjs) to the caller. This will break abstraction.
  private constructor(dayjs: dayjs.Dayjs) {
    this.instance = dayjs;
  }

  static create(value: DateTimeAdapterTypes = undefined): DateTimeAdapter {
    if (isNil(value)) {
      return new DateTimeAdapter(dayjs());
    }
    return new DateTimeAdapter(dayjs(value));
  }

  format(template?: string): string {
    return this.instance.format(template);
  }

  isBefore(value?: DateTimeAdapterTypes | DateTimeAdapter): boolean {
    if (value instanceof DateTimeAdapter) {
      return this.instance.isBefore(value.toDate());
    } else {
      return this.instance.isBefore(value);
    }
  }

  isAfter(value?: DateTimeAdapterTypes | DateTimeAdapter): boolean {
    if (value instanceof DateTimeAdapter) {
      return this.instance.isAfter(value.toDate());
    } else {
      return this.instance.isAfter(value);
    }
  }

  // https://day.js.org/docs/en/display/difference
  diff(value: DateTimeAdapterTypes | DateTimeAdapter, unit?: string): number {
    if (value instanceof DateTimeAdapter) {
      return this.instance.diff(value.toDate(), <QUnitType | OpUnitType>unit);
    } else {
      return this.instance.diff(value, <QUnitType | OpUnitType>unit);
    }
  }

  // Important to not mutate the current instance, modifications should always return new instance
  add(value: number, unit: string): DateTimeAdapter {
    // returns a clone i.e. a new dayJs, so the current instance should not be updated.
    return new DateTimeAdapter(this.instance.add(value, unit));
  }

  toDate(): Date {
    return this.instance.toDate();
  }

  valueOf(): number {
    return this.instance.valueOf();
  }

  isValid(): boolean {
    return this.instance.isValid();
  }

  subtract(number: number, unit: string): DateTimeAdapter {
    return new DateTimeAdapter(this.instance.subtract(number, unit));
  }

  set(hour: string, number: number): DateTimeAdapter {
    return DateTimeAdapter.create(
      this.instance.set(<UnitType>hour, number).toDate(),
    );
  }

  date(): number {
    return this.instance.date();
  }

  clone(): DateTimeAdapter {
    return new DateTimeAdapter(this.instance.clone());
  }

  isSame(value: DateTimeAdapter): boolean {
    return this.instance.isSame(value.toDate());
  }

  toLocalTimeString(): LocalTimeString {
    return this.instance.format('HH:mm');
  }

  setTimezone(zone: Timezone): DateTimeAdapter {
    return new DateTimeAdapter(this.instance.tz(zone));
  }

  weekday(): number {
    return this.instance.weekday();
  }

  hour(): number {
    return this.instance.hour();
  }
}
Enter fullscreen mode Exit fullscreen mode

Github link

Looking back, implementing the Adapter Pattern has saved me a lot of headaches. It’s allowed me to keep my codebase flexible and protected from the quirks of external libraries. Whether it’s changing libraries or just needing to adapt to new project requirements, this pattern ensures I’m not locked into one solution. Going forward, I plan to apply this in other parts of my code where external dependencies exist—it’s a small investment that pays off big.

domaindrivendesign Article's
30 articles in total
Favicon
Domain-Driven Design as a Software Design Approach
Favicon
The best way of implementing Domain-driven design, Clean Architecture, and CQRS
Favicon
Utilizing Adapter pattern for hiding external libraries
Favicon
Stop Wasting Working Software
Favicon
Understanding Domain Events in TypeScript: Making Events Work for You
Favicon
Understanding Clean Architecture Principles
Favicon
Heroes of DDD: Software Developer == business partner?
Favicon
Domain-Driven Design Core Principles and Challenges
Favicon
Heroes of DDD: BEHAVING perspective. What do I do?
Favicon
Tutorial: Defining the Domain entities
Favicon
Navigating the gRPC Galaxy: A Different view into Efficient 'api to api' Communication
Favicon
Heroes of DDD: Is a "good" domain model the Holy Grail?
Favicon
What I Learned from Domain Modeling in a Team
Favicon
Introduction to Domain Driven Design: Bridging the Gap Between Complex Systems and Software
Favicon
Domain-Driven Design Estratégico: Extraindo Sub-domínios com EventStorming
Favicon
Domain-Driven Design Estratégico: Destilando o domínio
Favicon
Domain-Driven Design Estratégico: O Início
Favicon
How We Reorganised Engineering Teams at Coolblue for Better Ownership and Business Alignment
Favicon
Value Objects in .NET (DDD Fundamentals)
Favicon
Recording: A domain driven approach to design and implement microservice REST APIs for Cumulocity IoT
Favicon
Evolving the Conversation: Embracing Ubiquitous Language
Favicon
Unraveling the Mysteries of Domain-Driven Design: An Introduction
Favicon
Can a VO make API calls? - Exploring the possibilities of integrating Value Objects with API calls
Favicon
Monolith vs Microservices
Favicon
A Journey Through Anti-Patterns and Code Smells
Favicon
From Chaos to Clarity: Discovering Domain Boundaries using Event Storming
Favicon
DDD e Sociologia: o que tĂŞm em comum?!
Favicon
Tell, don't ask: Domain-driven code refactoring
Favicon
What is domain-driven design?
Favicon
The value of value objects

Featured ones: