На главную

Адаптер

Суть паттерна "Адаптер" и его реализация на TypeScript

Адаптер — паттерн, который позволяет однообразно использовать различные объекты, предоставляющие данные.

Зачем это нужно?

  • Есть уже работающее приложение, сервисы которого работают с одним источником данных и необходимо поддержать новый источник с другим интерфейсом данных.
  • На начальном этапе разработки известно какие данные будут приходить, но неизвестно в каком формате, а начинать работу нужно уже сейчас.
Условия. Вы разработчик внутреннего портала компании. Сервисы портала работают с данными сотрудников.

Задача. Происходит объединение с другой компанией и необходимо интегрировать в существующие сервисы вашего портала работников этой компании. Мигрировать данные сотрудников поглащаемой компании в вашу базу данных не представляется возможным.

Проблема. Ваши сервисы умеют работать с одним форматом данных. Но у новых сотрудников он другой. Что делать?

Код

Общим для приложения является класс работника Employee, который реализует интерфейс EmployeeClass и предоставляет данные в определенном формате.

type Group = 'admin' | 'personnel'
type Status = 'active' | 'suspended' | 'inactive'

interface EmployeeData {
accountStatus: Status
groups: Group[]
departmentId: number
}

interface EmployeeClass {
groups: Group[]
department: number
status: Status
}

class Employee implements EmployeeClass {
private data: EmployeeData

constructor(data: EmployeeData) {
this.data = data
}

get groups() {
return this.data.groups
}

get department() {
return this.data.departmentId
}

get status() {
return this.data.accountStatus
}
}

К примеру, вот так испольует класс работника сервис отпусков, чтобы проверить, может ли текущий сотрудник редактировать записи с отпусками в отделе:

class VacationsService {
private departmentId: number

constructor(id: number) {
this.departmentId = id
}

// .. какие-то другие методы

public isUserCanEditDepartmentVacations(user: EmployeeClass) {
if (user.status !== 'active') return false
if (user.groups.includes('admin')) {
return true
}
return (
user.department === this.departmentId &&
user.groups.includes('personnel')
)
}
}

В реальном приложении подобных сервисов, заточенных под интерфейс EmployeeClass, может быть очень много.

Новый источник данных

При объединении компаний появляется еще один источник данных, с которыми, в свою очередь, работают сервисы, доставшиеся в наследство.

interface AnotherEmployeeData {
isActive: boolean
isAdmin: boolean
isPersonnel: boolean
departmentId: number
}

class AnotherEmployee {
private data: AnotherEmployeeData

constructor(data: AnotherEmployeeData) {
this.data = data
}

get isAdmin() {
return this.data.isAdmin
}

get isPersonnel() {
return this.data.isPersonnel
}

get isActive() {
return this.data.isActive
}

get department() {
return this.data.departmentId
}
}

Пишем Адаптер

Чтобы использовать данные из класса AnotherEmployee в существующих сервисах, напишем адаптер. Он будет принимать в конструктор экземпляр класса AnotherEmployee и используя его "под капотом", реализовывать привычный интерфейс EmployeeClass.

class AnotherEmployeeAdapter implements EmployeeClass {
private anotherSourceEmployee: AnotherEmployee

constructor(anotherSourceEmployee: AnotherEmployee) {
this.anotherSourceEmployee = anotherSourceEmployee
}

get groups() {
const groups: Group[] = []
if (this.anotherSourceEmployee.isAdmin) {
groups.push('admin')
}
if (this.anotherSourceEmployee.isPersonnel) {
groups.push('personnel')
}

return groups
}

get status() {
return this.anotherSourceEmployee.isActive ? 'active' : 'inactive'
}

get department() {
return this.anotherSourceEmployee.department
}
}

Теперь мы можем использовать данные из нового источника, не меняя существующих методов у существующих сервисов.

const baseUser = new Employee({
groups: ['admin'],
departmentId: 42,
accountStatus: 'active',
})

const anotherUser = new AnotherEmployee({
isActive: true,
isAdmin: false,
isPersonnel: true,
departmentId: 44,
})

const vacationsService = new VacationsService(50);

// используем привычный источник данных
const basicUserPermissionResult = vacationsService.isUserCanEditDepartmentVacations(baseUser)

// и так же используем новый источник данных с адаптером
const anotherUserAdapter = new AnotherEmployeeAdapter(anotherUser)
const anotherUserPermissionResult = vacationsService.isUserCanEditDepartmentVacations(anotherUserAdapter)

Комментарии