Паттерн “Специфікація“ в JS чи потрібен він?

Одним з найскладніших для розуміня в мене був паттерн специфікація. Саме через те що не дуже зрозуміло як його використовувати і навіщо. Сьогодні я спробую вам пояснити для чого він. І запропонувати свою версію реалізації цього паттерну, яку як на мене можно доволі ефективно використовувати

Що це таке?

Специфікація - це патерн проектування який дозволяє комбінувати бізнес-правила за допомогою булевої логіки. Складно? От і мені так здалось на перший погляд. Насправді найпростіша реалізація цього патерну виглядає ось так

class ByIdSpecification {
  constructor(private id: string) {}

  isSatisfiedBy(candidate: any): boolean {
    return candidate.id === this.id
  }

}

Як це використовувати?

const array = [{ id: 1, name: "Viktor" }, {id: 2, name: "Oleg"}]

const specification = new ByIdSpecification(1)

const result = array.find(specification.isSatisfiedBy)

Тепер виглядає простіше. Але для чого це? Виглядає як оверінженірінг. Бо цю саму задачу можно зробити за допомогою набагато меншого коду.

Що ж подивимось туди де це використовується найчастіше С#. Тут цей паттерн використовують для того щоб уніфікувати запити до бази данних. Так як фреймворк Entity спілкується з базою за допомогою функцій подібно функції filter або find в JS

Але в JS а конкретно в Node.js коли мі спілкуємось с базою за допомогою query обєктів. Наприклад в одній з найпопулярніших баз данний MongoDB звичайний запит до моделі виглядає от так

model.find({ something: 2 })

Я пропоную свою реалізацію

Я не претендую на безальтернативний варіант, але в мене є одна реалізація яка допомагає використовувати цей паттерн навіть в наших умовах

class ByIdSpecification {
  constructor(private id: string) {}

  isSatisfiedBy() {
    return {
      id: this.id
    }
  }

}

model.find(new ByIdSpecification(1).isSatisfiedBy())

Розумію що все ще виглядає як оверінженірінг. Але навіть зараз ми вже маємо 2 переваги.

  1. Збільшена читаємість

  2. Можливість перевикористання

Тобто купка незрозумілих параметрів в квері стають осмисленими данними. Наприклад от так можно зробити специфікацію для пошука географічних данних

class GeoSpecification {
  constructor(private lat: number, private lng: number){}

  isSatisfiedBy() {
    return {
      lat: this.lat,
      lng: this.lng
    }
  }

}

Але головна фішка ціього паттерну попереду.

Булева логіка в кущах

Доки я повністю не зрозумів цю частину, я не бу наскільки закоханим у цей паттерн. Специфікації можно комбінувати. В визначенні патерна фігурує фраза про булеву логіку. Це найголовніша перевага цього патерну. Подивимось на реалізацію цього в JS. На прикладі одніє булевої функції AND

interface ISpecification {
  isSatifiedBy()
  and(specification: ISpecification): ISpecification
}

class CompositeSpecification implements ISpecification {
  
  isSatisfiedBy() {
    throw NotImplementError()
  }

  and(specification: ISpecification): ISpecification {
    return new AndSpecification(this, specification)
  }

}


class AndSpecification extends CompositeSpecification {
  constructor(left: ISpecification, right specification) {
    super()
    this.left = left;
    this.right = right;
  }

  isSatisfiedBy(): {
    return this.left.isSatisfiedBy() && this.right.isSatifiedBy()
  
  }

}


class ByIdSpecification extends CompositeSpecification {
  constructor(private id: string) {}
  
  isSatisfiedBy() {
    return {
      id: this.id
  } 
}

class GeoSpecification extends CompositeSpecification {
  constructor(private lat: number, private lng: number) {}
  
  isSatisfiedBy() {
    return {
      lat: this.lat,
      lng: this.lng
    }
  }

}

const query = new ByIdSpecification(1)
  .and(new GeoSpecification(123, 321))
  .isSatisfiedBy()

model.find(query)

Тепер ми можемо використовувати комбінації цих специфікації що дозволить нам покращити перевикористовування. Та зменшити повторення. Також можно реалізовувати і інші булеві функції наприклад or але є одне обмеження. Туди пролазять деталі реалізації бази. Наприклад

class OrSpecification extends CompositeSpecification {
  constructor(left: ISpecification, right: ISpecificaition) {
    super()
    this.left = left;
    this.reight = right
  }

  isSatisfiedBy() {
    return {
      $or: [this.left.isSatifiedBy(), this.right.isSatifiedBy()]
    }
  }

}

Але є і хороша частина в цьому. Якщо вам захочеться змінити базу посеред проекту(це буває рідко, а ле хто вас бешкетників знає). То ви зможете змінити спосіб комбінації лише в одному місці

Висновки

Стандартну реалізацію цього патерно можно використовувати наприклад для фронтенда з редаксом. Або будь де де в нас є масив inmemory який ми можемо перебирати за допомогою функцій filter або find. Мою реалізацію можно використовувати на повну силу для того щоб робити пошук по базі. Як це наприклад робиться в С#. Буду радий почути коментарі або критику такого підходу.

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Владислав Лєсной
Владислав Лєсной@unnsin

554Прочитань
4Автори
17Читачі
На Друкарні з 15 квітня

Більше від автора

  • Clear Architecture для бекенда на JS

    Кожен хто займається розробкою певний час приходить до слова архітектура. Сьогодні ми подивимось на одну з парадигм проектування архітектури через призму типового JS бекенду.

    Теми цього довгочиту:

    Clean Architecture
  • Паттерн “Репозиторій“ в JS

    В попередній статті ми розібрались з паттерном Специфікація Сьогодні ми поговоримо про логічне продовження паттерн репозиторій.

    Теми цього довгочиту:

    Js

Вам також сподобається

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Вам також сподобається