Шаблоны проектирования и приёмы рефакторинга
Следовать принципу разделения интерфейса помогают такие шаблоны проектирования как Адаптер, а также приёмы выделения интерфейса и множественного наследования.
Адаптер
Адаптер — шаблон проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.
С точки зрения ISP этот шаблон помогает держать интерфейсы чистыми и понятными, а при необходимости совместить несовместимые модули через специальную прослойку (адаптер).
Приложение Курсовик показывает курс доллара к рублю. Оно берёт данные курсов с сайта Центрального банка России. Центробанк отдаёт их в формате XML, а Курсовик работает с JSON.
Адаптер помогает «подружить» модули, которые работают с XML, и модули, которые работают с JSON.
const ApiClient = {
async getXml(url: string): Promise<XmlString> {
const response = await fetch(url)
const data = await response.text()
return data
}
}
const xmlJsonAdapter = (xml: XmlString): JsonString => {
// Конвертируем xml в json:
return json
}
const parseCourse = (data: JsonString): CourseDict => {
// ...
return course
}
(async () => {
const data = await ApiClient.getXml('api_endpoint')
const course = parseCourse(xmlJsonAdapter(data))
})()
Также адаптер помогает справляться со «сломанным» API от бекенда и преобразованием одних структур данных в другие.
Из минусов можно назвать:
- добавление ещё одной абстракции в кодовую базу проекта;
- при создании нового адаптера нужно найти все места, где требуется его использовать.
Выделение интерфейса
Выделение интерфейса — это приём, при котором одинаковые методы и поля выносят в отдельный интерфейс.
В качестве примера можно вернуться к Койну из прошлого раздела. Интерфейс Record
— это выделенный общий интерфейс, который включает в себя общие для траты и дохода поля.
Выделение интерфейса тесно связано не только с ISP, но и с LSP. Например, оно используется при поиске корня композиции и как вспомогательный инструмент для выделения суперкласса.
Множественное наследование
Множественное наследование используется, например, чтобы реализовать функциональность нескольких интерфейсов:
class Horse implements Animal, Transport {/*...*/}
В TypeScript такое наследование реализуется через миксины.
До этих пор в книге для простоты повествования мы пропускали специальную функцию applyMixins
, которая копирует функциональность из родительских классов:
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name))
})
})
}
Чтобы пример с классом Horse
выше сработал, нам необходимо использовать applyMixins
следующим образом:
applyMixins(Horse, [Animal, Transport])
Тогда множественное наследование будет работать, как мы ожидаем.