Техника рефакторинга ООП

Я учусь объектно-ориентированному программированию на JavaScript и просматриваю этот небольшой код P5 из CodingTrain № 78, который имеет дело с летающими частицами на холсте в качестве материала.

Полный код ниже:

// Daniel Shiffman
// http://codingtra.in

// Simple Particle System
// https://youtu.be/UcdigVaIYAk

const particles = [];

function setup() {
  createCanvas(600, 400);
}

function draw() {
  background(0);
  for (let i = 0; i < 5; i++) {
    let p = new Particle();
    particles.push(p);
  }
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    particles[i].show();
    if (particles[i].finished()) {
      // remove this particle
      particles.splice(i, 1);
    }
  }
}

class Particle {

  constructor() {
    this.x = 300;
    this.y = 380;
    this.vx = random(-1, 1);
    this.vy = random(-5, -1);
    this.alpha = 255;
  }

  finished() {
    return this.alpha < 0;
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.alpha -= 5;
  }

  show() {
    noStroke();
    //stroke(255);
    fill(255, this.alpha);
    ellipse(this.x, this.y, 16);
  }

}

Желая тренировать свои навыки ООП, я пытаюсь преобразовать этот код в более сложный с точки зрения ООП. Поэтому я реорганизовал его, добавив Particles_Manipulation класс и перенос процесса, написанного на draw функция в Particles_Manipulation класс как action метод. Код ниже:

// Daniel Shiffman
// http://codingtra.in

// Simple Particle System
// https://youtu.be/UcdigVaIYAk

class Particle {

  constructor() {
    this.x = 300;
    this.y = 380;
    this.vx = random(-1, 1);
    this.vy = random(-5, -1);
    this.alpha = 255;
  }

  finished() {
    return this.alpha < 0;
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;
    this.alpha -= 5;
  }

  show() {
    noStroke();
    //stroke(255);
    fill(255, this.alpha);
    ellipse(this.x, this.y, 16);
  }

}

class Particles_Manipulation{
  constructor(){
    this.particles = [];
  }
  
  push_particles(_n){
    for (let i = 0; i < _n; i++) {
      let p = new Particle();
      this.particles.push(p);
    }
  }
  
  action(){
    for (let i = this.particles.length - 1; i >= 0; i--) {
      this.particles[i].update();
      this.particles[i].show();
      if (this.particles[i].finished()) {
        // remove this particle
        this.particles.splice(i, 1);
      }
    }
  }
}

const my_Particles_Manipulation = new Particles_Manipulation();

function setup() {
  createCanvas(600, 400);
}

function draw() {
  background(0);
  my_Particles_Manipulation.push_particles(5);
  my_Particles_Manipulation.action();
}

Не могли бы вы оценить, насколько хорош мой рефакторинг?

2 ответа
2

Из небольшого обзора;

  • В ООП предполагается, что ваши объекты можно использовать повторно
    • Я бы взял x,y из параметра
    • Я бы взял vx, vy из параметра
    • Я бы даже взял alpha из параметра
  • Приятнее позвонить finished -> isFinished, таким образом читатель ожидает, что логическое значение вернет
  • В update, имеет смысл обновить местоположение с помощью скорости, но я ожидал, что частица примет ‘updateFunctionwhich would reduce theальфа` на 5
  • В show, 16 должно быть либо константой с красивым именем, либо чем-то, что может быть установлено пользователем частицы. Это нарушает в настоящее время правило магических чисел и делает класс менее пригодным для повторного использования.
  • Particles_Manipulation -> ParticlesManipulation
  • Не поклонник подчеркивания, _n -> n или даже particleCount

    Магические числа

    Избегайте магических чисел. Магические числа — это просто числа в вашем коде, которые представляют некоторое абстрактное значение данных. Проблема в том, что они распространяются внутри кода. Чтобы внести изменения в эти значения, необходимо выполнить поиск в коде, и часто одни и те же значения повторяются, что еще больше затрудняет задачу.

    Определение магических чисел как именованных констант упрощает внесение изменений. Смотри переписать

    Использование собственных API

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

    Именование

    Есть много проблем с вашим стилем именования.

    Соглашение об именах JS

    Соглашение об именах для JavaScript заключается в использовании lowerCamelCase для всех имен, за исключением объектов, созданных с новым токеном, который использует UpperCamelCase (статические объекты и фабрики объектов также могут использовать UpperCamelCase). Для констант вы можете использовать UPPER_SNAKE_CASE

    _underscoredNames

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

    Использование подчеркивания должно быть только самым последним вариантом и никогда не должно использоваться, если в этом нет необходимости. Например, аргумент _n в push_particles(_n) { должно быть pushParticles(n)

    Избегайте избыточности

    Легко добавить слишком много к имени. Делайте имена короткими и используйте как можно меньше слов. Имена никогда не создаются изолированно и, как и естественный язык, требует, чтобы контекст имел значение.

    Например

    • Particles_Manipulation это перебор, когда Particles все что нужно

    • my_Particles_Manipulation Никогда не использовать my (за исключением инструктивного кода) Это имя может быть просто particles, или же smoke

    • push_particles (не повторяйте имена) Что делает эта функция? «Добавляет частицы». push becomes добавлять. What does it add particles to? "The Particles object", thus частицыcan be inferred. The nameдобавлятьis all that is needed. If you were adding monkeys then maybeaddMonkeys`

    Избегайте повторяющихся длинных ссылок

    Избегайте использования одного и того же длинного пути ссылки и индексации массива, сохраняя ссылку на объект в переменной.

          this.particles[i].update();
          this.particles[i].show();
          if (this.particles[i].finished()) {
            // remove this particle
            this.particles.splice(i, 1);
          }
    

    Становится

          const p = this.particles[i];
          p.update();
          p.show();
          if (p.finished()) {        
              this.particles.splice(i, 1);
          }
    

    Следите за логикой

    Система рендеринга уменьшает альфа частицы на каждом шаге. Вы используете значение альфа, чтобы проверить, когда удалять частицы alpha < 0. Однако вы уменьшаете альфу, затем визуализируете, а затем удаляете. Это означает, что не один раз, просматривая каждый кадр, вы пытаетесь визуализировать 5 невидимых частиц. Удалите перед рендерингом.

    JS и память

    Javascript — управляемый язык. Это означает, что он управляет памятью за вас.

    В большинстве случаев это замечательно, но это серьезно снижает производительность в таких приложениях, как системы частиц, где часто создаются и уничтожаются многие небольшие независимые объекты.

    Пул объектов

    Чтобы избежать накладных расходов на управление памятью, мы используем пулы объектов.

    Пул объектов — это просто массив объектов, которые не используются. Когда объект больше не нужен, мы удаляем его из основного массива и помещаем в пул. Когда нужен новый объект, мы проверяем пул, если есть какой-либо объект в пуле, мы используем его, а не создаем новый.

    Это означает, что объекту частицы потребуется функция, которая его сбрасывает.

    Смотри переписать

    Обратите внимание, что вы также можете объединить частицы в один массив, используя пузырьковый итератор, чтобы избежать дорогостоящего @ MDN.JS.OBJ.Array.slice @. Пузырь с мертвыми частицами на одном конце массива. Это даже быстрее, но значительно усложняет внутренний цикл обновления. Вы могли бы сделать это, только если вы используете 10K + частиц.

    ОО дизайн на JavaScript

    Многие считают, что с помощью class как написать хороший объектно-ориентированный JavaScript. К сожалению, это не так, и на самом деле это худший способ написания объектно-ориентированного JavaScript. Синтаксис класса не обеспечивает хорошей модели инкапсуляции, его единственный плюс — легкое наследование, которое является своего рода избыточным вместо полиморфизма проигрыша в JavaScript.

    В этом случае лучше всего подходят фабричные функции (функция, которая создает Object AKA Object factory). Поскольку мы используем пул объектов, когда даже не нужно беспокоиться о назначении prototytpe для функций частиц.

    Некоторые преимущества заводских функций

    • Это избавляет от необходимости использовать this и new.

    • Позволяет определять частные свойства без необходимости #hack имена

    • Позволяет создавать объекты до завершения первого прохода выполнения (переместите инициализацию и настройки наверх, где они находятся)

    Некоторые недостатки заводских функций

    • Требуется хорошее понимание закрытие и Объем и то, и другое является трудным для понимания предметом.

    • Не работает с прототипами объектов.

    Переписать

    Перезапись использует CanvasRenderingContext2D API Чтобы избежать накладных расходов P5, некоторые функции замены находятся внизу.

    Обратите внимание, что при определении констант всегда рекомендуется включать типы единиц, диапазоны или описание в качестве комментария.

    animate();
    ;(() => {
        const CANVAS_WIDTH = 600;       // in CSS pixels
        const CANVAS_HEIGHT = 400;
        const PARTICLE_SPREAD = 1;      // max x speed in canvas pixels
        const PARTICLE_POWER = 3;       // max y speed in canvas  pixels
        const PARTICLE_RADIUS = 12;     // max y speed in canvas pixels
        const MAX_PARTICLES = 1000;     // Approx max particle count
        const PARTICLES_RATE = 3;       // new particle per frame
        const SMOKE_PARTICLES_PATH = new Path2D;  // circle used to render particle
        const PARTICLE_COLOR = "#FFF";  // any valid CSS color value
        const START_ALPHA = 1;          // particle start alpha max
        const START_ALPHA_MIN = 0.5;    // particle start alpha
    
        // Rate particle alpha decays to match MAX_PARTICLES. The life time in seconds 
        // can be calculates as  Math.ceil(1 / ALPHA_DECAY_SPEED) * 60
        const ALPHA_DECAY_SPEED = 1 / (MAX_PARTICLES / PARTICLES_RATE);    
    
        SMOKE_PARTICLES_PATH.arc(0, 0, PARTICLE_RADIUS, 0, Math.PI * 2);
        const canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
        const smoke = new Particles(canvas);
        animate.draw = function() {
            canvas.ctx.setTransform(1, 0, 0, 1, 0, 0);
            canvas.ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
            smoke.add(PARTICLES_RATE);
            smoke.update();
        }
    
        function Particle() { 
            var x, y, vx, vy, alpha;
            const PARTICLE = {
                init() {
                    x = CANVAS_WIDTH / 2;
                    y = CANVAS_HEIGHT;
                    vx = random(-PARTICLE_SPREAD, PARTICLE_SPREAD);
                    vy = random(-PARTICLE_POWER, -PARTICLE_POWER);
                    alpha = random(START_ALPHA_MIN, START_ALPHA);
                    return PARTICLE;
                },
                update() {
                    x += vx;
                    y += vy;
                    alpha -= ALPHA_DECAY_SPEED;
                    return alpha < 0;
                },
                draw(ctx) {
                    ctx.globalAlpha = alpha;
                    ctx.setTransform(1, 0, 0, 1, x, y);
                    ctx.fill(SMOKE_PARTICLES_PATH);
                },
            };
            return Object.freeze(PARTICLE.init());
        }
        function Particles(canvas) {
            const particles = [];
            const pool = [];
            const ctx = canvas.ctx;
            return Object.freeze({
                add(n) {
                    while (n-- > 0) {
                       pool.length ?
                            particles.push(pool.pop().init()) :
                            particles.push(Particle());
                    }
                },
                update() {
                    var i = 0;
                    ctx.fillStyle = PARTICLE_COLOR;
                    while (i < particles.length) {
                        const p = particles[i];
                        p.update() ? 
                            pool.push(particles.splice(i--, 1)[0]) : 
                            p.draw(ctx);
                        i++;
                    }
                }
            });
        }
    })();
    
    
    // functions to replace P5
    function createCanvas(width, height) {
        const canvas = Object.assign(document.createElement("canvas"), {width, height, className: "renderCanvas"});
        document.body.appendChild(canvas);
        canvas.ctx = canvas.getContext("2d");
        return canvas;
    }
    function random(min, max) { return Math.random() * (max - min) + min }
    
    function animate() {
        animate.draw?.();
        requestAnimationFrame(animate);
    }
    .renderCanvas {
        background: black;
    }

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *