Вопрос по dependency inversion principle (SOLID)
Суть данного принципа: классы должны зависеть от абстракций, а не от конкретных деталей (скопировал из википедии) Теперь давайте рассмотрим пример. Есть некий класс ApiService, который умеет обращаться к бэку, полную реализацию писать я думаю смысла нет:
class FetchTransport {
send(url: string, params: any) {
return fetch(...)
}
}
class ApiService {
private transport: FetchTransport
constructor() {
this.transport = new FetchTransport()
}
public getData(filters?: any) {
return this.transport.send('my_url', filters)
}
}
const api = new ApiService()
api.getData()
Здесь я думаю суть понятна. ApiService зависит от FetchTransport и если мы захотим использовать вместо феча что то другое, например сокеты, то будут проблемы. Поэтому лучше использовать интерфейс, а реализацию передать параметром. Вот так будет правильно:
interface Transport {
send(url: string, params: any): Promise<any>
}
class FetchTransport implements Transport {
public send(url: string, params: any): Promise<any> {
return Promise.resolve() // fetch(...)
}
}
class WebSocketTransport implements Transport {
public send(url: string, params: any): Promise<any> {
return Promise.resolve() // using WebSocket WebSocket
}
}
class ApiService {
private transport: FetchTransport
constructor(transport: Transport) {
this.transport = transport
}
public getData(filters?: any) {
return this.transport.send('my_url', filters)
}
}
const api = new ApiService(new FetchTransport())
api.getData()
А теперь давайте рассмотрим другой пример. я здесь тоже полную реализацию рассписывать не буду, опишу только саму суть. Допустим я хочу написать свой драг\дроп, но при этом разбив на сущности. Есть сущность платформа где это будет запускаться, на мобилке или на вебе. И в зависимости от платформы будут идти разные подписки на события. На вебе: mousedown, mousemove, mouseup. На мобилке: touchstart, touchmove, touchend. И естественно будут разные типы нативных событий, которые я хочу обернуть в свой интерфейс, чтобы вне зависимости от платформы у меня были одинаковые координаты для конечного класса Draggable. А в классе Draggable у меня простая логика. При нажатии я подписываюсь на move и up, в move я обновляю положение элемента. а в up я отписываюсь чтобы драг прекратился
interface CustomDragEvent {
x: number
y: number
}
type Callback = (customEvent: CustomDragEvent) => void
interface PlatformEvents {
subscribeMouseDown(subscriber: Callback): void
subscribeMouseMove(subscriber: Callback): void
subscribeMouseUp(subscriber: Callback): void
unsubscribeMouseDown(): void
unsubscribeMouseMove(): void
unsubscribeMouseUp(): void
}
type MouseCallback = (event: MouseEvent) => void
class WebPlatform implements PlatformEvents {
private subscriberMouseDown: MouseCallback | null = null
private subscriberMouseMove: MouseCallback | null = null
private subscriberMouseUp: MouseCallback | null = null
private $node: EventTarget
constructor($node: EventTarget) {
this.$node = $node
}
public subscribeMouseDown(callback: Callback): void {
const subscriberMouseDown: MouseCallback = (event: MouseEvent) => {
const customEvent: CustomDragEvent = { x:0, y: 0 } // ... mutation MouseEvent
callback(customEvent)
}
this.$node.addEventListener('mousedown', subscriberMouseDown as EventListener)
this.subscriberMouseDown = subscriberMouseDown
}
public unsubscribeMouseDown(): void {
if (this.subscriberMouseMove) {
this.$node.removeEventListener('mousedown', this.subscriberMouseDown as EventListener)
}
}
public subscribeMouseMove(callback: Callback): void {
const subscriberMouseUp = (event: MouseEvent) => {
const customEvent: CustomDragEvent = { x:0, y: 0 } // ... mutation : MouseEvent
callback(customEvent)
}
this.$node.addEventListener('mousemove', subscriberMouseUp as EventListener)
this.subscriberMouseUp = subscriberMouseUp
}
public unsubscribeMouseMove(): void {
if (this.subscriberMouseMove) {
this.$node.removeEventListener('mousedown', this.subscriberMouseDown as EventListener)
}
}
public subscribeMouseUp(callback: Callback): void {
this.$node.addEventListener('mouseup', (event) => {
const customEvent: CustomDragEvent = { x:0, y: 0 } // ... mutation TouchEvent
callback(customEvent)
})
}
public unsubscribeMouseUp(): void {
if (this.subscriberMouseMove) {
this.$node.removeEventListener('mousedown', this.subscriberMouseDown as EventListener)
}
}
}
class MobilePlatform implements PlatformEvents {
private $node: EventTarget
constructor($node: EventTarget) {
this.$node = $node
}
public subscribeMouseDown(callback: Callback): void {
// ...
}
public unsubscribeMouseDown(): void {
// ...
}
public subscribeMouseMove(callback: Callback): void {
// ...
}
public unsubscribeMouseMove(): void {
// ...
}
public subscribeMouseUp(callback: Callback): void {
// ...
}
public unsubscribeMouseUp(): void {
// ...
}
}
class Draggable {
constructor($node: HTMLElement, global: PlatformEvents, platform: PlatformEvents) {
const mouseDown = () => {
global.subscribeMouseMove(mouseMove)
global.subscribeMouseMove(mouseUp)
}
const mouseMove = ({ x, y }: CustomDragEvent) => {
$node.style.left = `${x}px`
$node.style.top = `${y}px`
}
const mouseUp = () => {
platform.subscribeMouseDown(mouseDown)
platform.subscribeMouseDown(mouseDown)
}
platform.subscribeMouseDown(mouseDown)
}
}
const platform = true // or false
const $div = document.createElement('div')
const globalPlatform = new WebPlatform(window)
const targetPlatform = new WebPlatform($div)
const draggable = new Draggable($div, globalPlatform, targetPlatform)
Если я правильно понял данный принцип, то я должен каждый раз прокидывать в конструктор параметры платформы, т.к. класс не должен зависеть от реализации, а только от абстракции. Но по факту им пользоваться получается не очень удобно. Потому что мне каждый раз надо вручную создавать экземпляр платформы. Но если я уберу логику детекта платформы внутрь конструктора, то тогда в параметры мне нужно будет только передавать таргет, ноду которую я хочу перетаскивать, но тогда мой Draggable будет зависеть от реализации Platform и нарушу данный принцип. Вопрос в том, что всегда ли полезно использовать DIP? Например как в данном случае? Или нужно ограничиваться какими то правилами когда это действительно полезно, а когда нет
И еще вопрос. Если Platform тоже зависит от какого то другого класса, допустим Platform тоже использует какую то обертку. То тогда нужно сначала создать одни экземпляр класса, его прокинуть в экземпляр класса Platform, а Platform в свою очередь в Draggable? Так получается? И в конечном итоге чтобы создать один Draggable, нужно туда передать всю поднаготную с другими классами?