Как лучше реорганизовать эту кодовую базу таймера / секундомера для лучшего повторного использования кода в соответствии с парадигмой ООП и принципом DRY?

#javascript #oop #refactoring #dry #code-reuse

#javascript #ооп #рефакторинг #dry #повторное использование кода

Вопрос:

У меня возникли проблемы с соблюдением ООП в javascript. Как я могу сделать этот код более объектно-ориентированным и повторно используемым?

Я попытался ознакомиться с концепциями ООП в JS, но не смог найти способ сделать этот код единым. Есть предложения?

PS: это код для создания секундомера

 //Define variables to hold time values
let seconds = 0;
let minutes = 0;
let hours = 0;

//Define variable to  hold "display" value
let interval = null;

//Define variable to hold the clock status
let status = "paused";

//Clock function ( logic to determine when to increment next value, etc.)
function clock() {
  seconds  ;

  //Logic to determine when to increment next value
  if (seconds >= 60) {
    seconds = 0;
    minutes  ;

    if (minutes >= 60) {
      minutes = 0;
      hours  ;
    }
  }

  //Display updated time values to user
  document.getElementById("display").innerHTML =
    //If seconds/minutes/hours are only one digit, add a leading 0 to the value
    `${hours ? (hours > 9 ? hours : `0${hours}`) : "00"}:${minutes ? (minutes > 9 ? minutes : `0${minutes}`) : "00"}:${seconds > 9 ? seconds : `0${seconds}`}`;
}

function startPause() {
  if (status === "paused") {
    //Start the stopwatch (by calling the setInterval() function)
    interval = window.setInterval(clock, 1000);
    document.getElementById("startPause").innerHTML = "Pause";
    status = "started";
  } else {
    window.clearInterval(interval);
    document.getElementById("startPause").innerHTML = "Resume";
    status = "paused";
  }
}

//Function to reset the stopwatch
function reset() {
  seconds = 0;
  minutes = 0;
  hours = 0;
  document.getElementById("display").innerHTML = "00:00:00";
  document.getElementById("startPause").innerHTML = "Start";
  window.clearInterval(interval);
  status = "paused";
}
  

Ответ №1:

Можно было бы использовать подход, основанный на компонентах. Приведенный пример кода довольно прост. Конструктор присваивает экземпляру все необходимые ссылки в качестве общедоступных свойств Stopwatch . Геттеры и сеттеры для чтения и записи части пользовательского интерфейса компонента (/ DOM) реализованы как прототипные методы. Вспомогательные методы (обработка интервалов, вычисления временных измерений) не обязательно должны быть частью самой реализации класса, но будут находиться Stopwatch в области модуля…

 // module scope of e.g. 'Stopwatch.js' file.

const MEASURE_STATE_RUNNING = 'running';
const MEASURE_STATE_STOPPED = 'stopped';

const UPDATE_CYCLE_MINIMUM = 100;   // any value in msec.
const UPDATE_CYCLE_DEFAULT = 200;   //
const UPDATE_CYCLE_MAXIMUM = 1000;  //

function getDisplayNumber(value) {
  return ((String(value).length === 1) amp;amp; `0${ value }`) || value;
}

function getMeasureInMilliseconds(measure) {
  return (((measure.hours * 3600)   (measure.minutes * 60)   measure.seconds) * 1000);
}
function getMeasureFromMilliseconds(value) {
  let hours = (value / 3600000);
  let minutes = ((hours - Math.floor(hours)) * 60);
  let seconds = ((minutes - Math.floor(minutes)) * 60);

  hours = Math.floor(hours);
  minutes = Math.floor(minutes);
  seconds = Math.floor(seconds   0.001);

  seconds = ((seconds < 60) amp;amp; seconds) || 0;
  minutes = ((minutes < 60) amp;amp; minutes) || 0;
  hours = ((hours < 100) amp;amp; hours) || 0;

  return { hours, minutes, seconds };
}

function handleStartStopForBoundStopwatch(/* evt */) {
  const stopwatch = this;
  if (stopwatch.measureState === MEASURE_STATE_STOPPED) {

    stopwatch.startMeasure();
  } else {
    stopwatch.stopMeasure();
  }
}

function updateStopwatchMeasure(stopwatch) {
  const dateNow = Date.now();

  // has at least one second past since the last measure update?
  const isUpdateMeasure = (Math.floor((dateNow - stopwatch.updateTimestamp) / 1000) >= 1);

  if (isUpdateMeasure) {
    stopwatch.updateTimestamp = dateNow;

    // time differences in milliseconds since measuring has been started the last time.
    const timePassed = (dateNow - stopwatch.measureTimestamp);
    const messureValue = (timePassed   stopwatch.lastMeasuredMSecs);

    Object.assign(stopwatch.measure, getMeasureFromMilliseconds(messureValue));

    stopwatch.setComponentMeasure();
  }
}


class Stopwatch {

  constructor(node) {
    this.node = node;

    this.timerId = null;
    this.updateCycle = this.getComponentUpdateCycle();

    // for synchronizing display values of a running measure.
    this.lastMeasuredMSecs = null;
    this.measureTimestamp = null;
    this.updateTimestamp = null;

    this.measure = this.getComponentMeasure();
    this.measureState = this.getComponentMeasureState();

    // synchronize component data initially.
    this.setComponentMeasure();
    this.setComponentMeasureState();

    if (this.measureState === MEASURE_STATE_RUNNING) {
      this.startMeasure();
    }
    this.startStopHandler = handleStartStopForBoundStopwatch.bind(this);

    node.addEventListener('click', this.startStopHandler);
  }
  destroy() {
    if (this.node) {
      this.node.removeEventListener('click', this.startStopHandler);
      this.node.remove();
      this.node = null;
      delete this.node;
    }
    this.timerId = this.updateCycle = this.lastMeasuredMSecs = null;
    this.measureTimestamp = this.updateTimestamp = null;
    this.measure = this.measureState = this.startStopHandler = null;

    delete this.timerId;
    delete this.updateCycle;
    delete this.lastMeasuredMSecs;
    delete this.measureTimestamp;
    delete this.updateTimestamp;
    delete this.measure;
    delete this.measureState;
    delete this.startStopHandler;
  }

  getComponentMeasure() {
    const result =
      (/^(?<hours>d{1,2}):(?<minutes>d{1,2}):(?<seconds>d{1,2})$/)
      .exec(
        this.node.dateTime
      );
    const {
      hours,
      minutes,
      seconds
    } = (result amp;amp; result.groups) || { hours: 0, minutes: 0, seconds: 0 };

    return {
      hours: parseInt(hours, 10),
      minutes: parseInt(minutes, 10),
      seconds: parseInt(seconds, 10)
    };
  }
  setComponentMeasure() {
    const { hours, minutes, seconds } = this.measure;
    const value = [
      getDisplayNumber(hours),
      getDisplayNumber(minutes),
      getDisplayNumber(seconds)
    ].join(':');

    this.node.dateTime = value;
    this.node.innerText = value;
  }

  getComponentMeasureState() {
    return (
      ((this.node.dataset.measureState || '').trim() === 'running')
      amp;amp; MEASURE_STATE_RUNNING
      || MEASURE_STATE_STOPPED
    );
  }
  setComponentMeasureState() {
    this.node.dataset.measureState = this.measureState;

    if (this.measureState === MEASURE_STATE_RUNNING) {

      this.node.classList.add(MEASURE_STATE_RUNNING);
      this.node.classList.remove(MEASURE_STATE_STOPPED);
    } else {
      this.node.classList.add(MEASURE_STATE_STOPPED);
      this.node.classList.remove(MEASURE_STATE_RUNNING);
    }
  }

  getComponentUpdateCycle() {
    let value = parseInt(this.node.dataset.updateCycle, 10);
    value = (Number.isNaN(value) amp;amp; UPDATE_CYCLE_DEFAULT) || value;
    return Math.max(UPDATE_CYCLE_MINIMUM, Math.min(UPDATE_CYCLE_MAXIMUM, value));
  }

  startMeasure() {
    this.measureTimestamp = this.updateTimestamp = Date.now();
    this.lastMeasuredMSecs = getMeasureInMilliseconds(this.measure);

    this.timerId = setInterval(

      updateStopwatchMeasure,
      this.updateCycle,
      this
    );
    this.measureState = MEASURE_STATE_RUNNING;
    this.setComponentMeasureState();
  }
  stopMeasure() {
    clearInterval(this.timerId);

    this.lastMeasuredMSecs = null;
    this.measureTimestamp = null;
    this.updateTimestamp = null;

    this.measureState = MEASURE_STATE_STOPPED;
    this.setComponentMeasureState();
  }/*
  resetMeasure() {
    Object.assign(this.measure, {
      hours: 0,
      minutes: 0,
      seconds: 0
    });
    this.setComponentMeasure();
  }*/

  static initialize(node) {
    return new Stopwatch(node);
  }
}


/*export default*/function initialize() {
  return Array
    .from(document.body.querySelectorAll('.stopwatch-component'))
    .map(Stopwatch.initialize);
}


/**
 *  usage
 */
const timerList = initialize();

// console.log('timerList :', timerList);  
 dd {
  margin-bottom: 6px;
}
.stopwatch-component {
  font-family: monospace;
  font-size: x-large;
  cursor: pointer;
}
.stopwatch-component.stopped {
  text-decoration: line-through solid #999;
}  
 <dl>
  <dt>
    1st time measurement
  </dt>
  <dd>
    <time
      class="stopwatch-component"
      datetime="0:0:0"
      data-update-cycle="100"
      data-measure-state="running">0:0:0</time>
  </dd>
  <dt>
    2nd time measurement
  </dt>
  <dd>
    <time
      class="stopwatch-component"
      datetime="99:59:33"
      data-update-cycle="1000">99:59:33</time>
  </dd>
  <dt>
    3rd time measurement
  </dt>
  <dd>
    <time
      class="stopwatch-component"
      datetime="07:11:55"
      data-update-cycle="500"
      data-measure-state="stopped">7:11:55</time>
  </dd>
</dl>
<p>Click each time measure separately for toggling its pause/proceed state.</p>  

Примечание:

Чтобы сохранить (повторный) рендеринг секунд (минут, часов), которые были переданы синхронно, нужен другой подход к таймеру, чем тот, который предоставляется OP.

Примеры выполняются с различными настраиваемыми циклами обновления, определяемыми значением data-update-cycle атрибута <time class="stopwatch-component"/> элемента. Интервал обновления 500 мсек или всего 1 сек. не подходит для такого рода задач измерения из-за setInterval недостаточной точности. Запущенный пример демонстрирует именно это.

Необходимо синхронизировать отображаемые данные, постоянно сравнивая временную метку с момента запуска процесса измерения с временной меткой из текущего процесса обновления. (Повторный) рендеринг отображаемого значения cause выполняется только в том случае, если с момента последнего (повторного) рендеринга прошло не менее секунды. Но циклы обновления, которые постоянно (но не постоянно) запускают (повторный) рендеринг, должны выполняться с более высокой частотой.

Комментарии:

1. @Roshin … есть ли еще вопрос, на который нет ответа?