Смешанные ответственности (операции) разных уровней


Исходный код

scrollEvent(e, element) {
    const elementRect = element.getBoundingClientRect();
    const elemenParenttRect = element.parentElement.getBoundingClientRect();

    if (document.documentElement.scrollTop <= elementRect.top) return;

    if (this.point < document.documentElement.scrollTop) {
      if (-1 * elemenParenttRect.top + window.innerHeight >= elemenParenttRect.height) {
        this.hookTop = true;
        element.style.position = 'absolute';
        element.style.transform = `translateY(${-1 * elemenParenttRect.top - -1 * elementRect.top}px)`;
        element.style.left = `0px`;

        return;
      }

      if (
        (element.getBoundingClientRect().top - window.innerHeight) * -1 > element.getBoundingClientRect().height + 30 &&
        element.getBoundingClientRect().height > window.innerHeight
      ) {
        element.style.left = `${elementRect.x}px`;
        element.style.transform = `translateY(${elementRect.top}px)`;
        element.style.position = 'fixed';

        this.hookTop = false;
      }

      if (element.getBoundingClientRect().height < window.innerHeight && elemenParenttRect.top <= 0) {
        element.style.position = 'fixed';
        element.style.transform = `translateY(0px)`;
        element.style.left = `${elementRect.x}px`;
      }

      if (this.hookTop && element.getBoundingClientRect().height > window.innerHeight) {
        element.style.left = `0px`;
        element.style.position = 'absolute';
        element.style.transform = `translateY(${-1 * elemenParenttRect.top - -1 * elementRect.top}px)`;
      }
    } elseif (!this.wasTop) {
      element.style.position = 'absolute';
      element.style.transform = `translateY(${-1 * elemenParenttRect.top - -1 * elementRect.top}px)`;
      element.style.left = `0px`;

      if (elementRect.top >= 0) {
        this.hookTop = true;
        element.style.position = 'fixed';
        element.style.transform = `translateY(0px)`;
        element.style.left = `${elementRect.x}px`;
      }

      if (elemenParenttRect.top >= 0) {
        element.style.position = 'static';
        element.style.transform = `translateY(none)`;
        element.style.left = `${elementRect.x}px`;
        this.hookTop = false;
      }
    }

    this.point = document.documentElement.scrollTop;
  }

Исходный код предназначен для управления плавающим элементом на веб-странице: при определённых условиях он «заливает», при других — скроллится вместе с контентом страницы.

Что не так в исходном коде

Главный недостаток исходного кода — смешение ответственностей (знаний) разного уровня в одном методе. Тут и вычисление положения, в котором должен оказаться блок при каждом положении скроллинга, и все детали позиционирования.

Как можно сделать лучше

В результате рефакторинга были не только разделены ответственности, но и заметно упрощён алгоритм:

onScroll() {
    const windowHeight = this.windowHeight = window.innerHeight;

    const interRect = this.interTarget.getBoundingClientRect();

    const interTop = interRect.top;
    const interBottom = interRect.top + interRect.height;

    if (interTop > windowHeight + 500) {
      this.hide();
    }
    else if (interBottom < -500) {
      this.hide();
    }
    else {
      this.setAsFloatingWithInter(interTop);
    }
  }

  hide() {
    this.stickyTarget.style = 'visibility: hidden;';
  }

  setAsFloatingWithInter(stickyTop) {
    stickyTop = Math.min(stickyTop, 0);
    stickyTop = Math.max(stickyTop, this.windowHeight - this.stickyHeight);
    this.stickyTarget.style = `visibility: visible; position: fixed; top: ${ stickyTop }px`;
  }

Самое важное изменение — появились новые методы, которые «знают», как правильно позиционировать плавающий блок в каждой ситуации, а исходный метод занимается только определением того, какая именно ситуация сейчас имеет место быть.