Вопрос по 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, нужно туда передать всю поднаготную с другими классами?


Ответы (0 шт):