Одним з найскладніших для розуміня в мене був паттерн специфікація. Саме через те що не дуже зрозуміло як його використовувати і навіщо. Сьогодні я спробую вам пояснити для чого він. І запропонувати свою версію реалізації цього паттерну, яку як на мене можно доволі ефективно використовувати
Що це таке?
Специфікація - це патерн проектування який дозволяє комбінувати бізнес-правила за допомогою булевої логіки. Складно? От і мені так здалось на перший погляд. Насправді найпростіша реалізація цього патерну виглядає ось так
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 переваги.
Збільшена читаємість
Можливість перевикористання
Тобто купка незрозумілих параметрів в квері стають осмисленими данними. Наприклад от так можно зробити специфікацію для пошука географічних данних
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. Мою реалізацію можно використовувати на повну силу для того щоб робити пошук по базі. Як це наприклад робиться в С#. Буду радий почути коментарі або критику такого підходу.