Я учусь объектно-ориентированному программированию на 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 ответа
Из небольшого обзора;
- В ООП предполагается, что ваши объекты можно использовать повторно
- Я бы взял
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 maybe
addMonkeys`
Избегайте повторяющихся длинных ссылок
Избегайте использования одного и того же длинного пути ссылки и индексации массива, сохраняя ссылку на объект в переменной.
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;
}