Path抽象化
GUIなどで良く見られるが、
親から子に変換なりイベントなりを受け渡す構造がある。
for(let child of this.Children){
child.update(this.なんか変換);
}
この構造は子が子を成していくと最終なにやってるかよく分からなくなるデメリットがある。
別のやり口として、変換側がループを回すとすると
for(let target of this.Targets){
変換(target)
}
のようになる。
これは例えば平行移動、スケール、回転が各々ループを回すと考えると、効率は大変悪い。
一度ループを用いないという前提で考えてみよう。
各々のオブジェクトなり変換は単一の参照を持つ。
class Transform{
target;
}
この変換はオブジェクトと一対一で対応する。
この実装で実現可能なことは、targetsをループで回しても実現できる。
が、意図は明瞭になる。
例えば
class Transform{
target;
}
この変換がインスタンス化され、主流のループに2回pushされていれば、targetに対して2回変換が重なるのだと分かる。一方、
for(let target of this.Targets){
変換(target)
}
この変換がインスタンス化され、主流のループに2回pushされていた場合、微妙に分かりずらい。同じことはできるし、それ以上もできるが、話が込み入る。
パスの抽象化
この辺のことを好きに記述することを考えよう。
オブジェクトなりインスタンスなりノードなりには各々いろんな関係があって、ときどきの構造をなす。それは主従かもしれないし親子かもしれないし対等かもしれないし一方通行かもしれない。
ここではオブジェクトなりインスタンスなりをノードと呼び、
各々のノードの関係をパスと称する。
各々のノードは、他の全てのノードと繋がりうる。
例えばこう。
class Node{
constructor(){
this.Nodes=[];
}
}
各々のノードは、自身と接続する他のノードに対して、例えば以下のような処理をする。
function NodeUpdate(master, nodes) {
let destroyCandidates = [];
for (let node of nodes) {
switch (node.type) {
case NodeType.Children:
//ChildrenProcess(node, this.Transformer);
break;
case NodeType.Resource:
ResourceProcess(master, node);
break;
case NodeType.Gadget:
GadgetProcess(master, node);
break;
case NodeType.Satisfier:
SatisfierProcess(master, node);
break;
case NodeType.Reactor:
ReactorProcess(master, node);
break;
case NodeType.Importer:
ImporterProcess(master, node);
break;
// 他のNodeTypeの処理が必要な場合は、ここに追加します。
// 例:
// case NodeType.AnotherType:
// AnotherProcess(master, node);
// break;
default:
// 一致するnode.typeがない場合の処理(必要に応じて)
break;
}
// 削除フラグが立っている場合、削除候補に追加
if (node.destroyFlag) {
destroyCandidates.push(node);
}
}//for
// 削除候補のノードを一斉に削除
removeNodes(master, destroyCandidates);
}
// 指定されたノードをNode配列から削除するメソッド
function removeNodes(master, nodesToRemove) {
master.Node = master.Node.filter(node => !nodesToRemove.includes(node));
}
つまりあるノードは、接続された他のノードを引っ張ってきて、初めてそいつとの関係を調査する。
この例の実装だと、ノードはあらかじめ自身の役割を承知している為、他のノードとの関係によって役割が変化することがない。
もっと誠実に抽象化するなら、ノードは他のノードと比較され、初めてその関係が決定されるべきである。
例えば、今の実装だと子ノードは他の全てのノードに対して子ノードである。今の実装だと、あるノードに対しては子であるが、あるノードに対しては親である、という関係はつくれない。
この処理は、
全てのノードが互いに接続されていることが保証される場合、ルートのノードから始めれば他の全てのノードが処理される。この場合、ノードツリーは一つである。
そうでない場合、主流のループを回して、少なくともそこに乗っているノードは処理することを保証する。この場合、ノードツリーは複数あっても良い。
主流のループに乗っているにも関わらず、他のノードからも接続されている場合、2回処理される。
循環参照無限ループは常にありうる。
Importer
つべこべ言わずに例を見てみよう。
以下に適当なノードを作る。
class DotObj{
constructor(v, r){
this.v = new Vector2D(v.x,v.y);
this.r = r;
//this.fillStyle="white";
//this.strokeStyle="black";
this.Node=[];
this.Transformer=[];
this.Node.push(new StrokeStyleImporter("black"));
}
update(){
NodeUpdate(this, this.Node);
}
//#region get/set
get x(){
return this.v.x;
}
set x(x){
this.v.x=x;
}
get y(){
return this.v.y;
}
set y(y){
this.v.y=y;
}
//#endregion
draw(ctx) {
if(this.fillStyle){
ctx.fillStyle = this.fillStyle;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.fill();
}
if(this.strokeStyle){
ctx.strokeStyle=this.strokeStyle;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
ctx.stroke();
}
}
}
fillStyleなりstrokeStyleなりは、それを描画したいのであればどんな環境下であれ似たようなもんを設定する必要がある。
なんらかのオブジェクト構造を作ろうという時、こうした設定が次々と増えて言って最終邪魔になる。
this.Node.push(new StrokeStyleImporter("black"));
この行はStrokeStyleを設定するためのノードである。
あるJavaScriptオブジェクトであるノードにStrokeStyleとそのゲッターセッターを設定し、役目を終えたら消える。
//Objectにプロパティを追加するNode
class StrokeStyleImporter{
constructor(style="black", width=1){
this.type = NodeType.Importer;
this._strokeStyle = style;
this._lineWidth = width;
this.destroyFlag = false;
}
import(base){
Object.defineProperty(base, '_strokeStyle', {
value: this._strokeStyle
});
Object.defineProperty(base, 'strokeStyle', {
get: function() {
return this._strokeStyle;
},
set: function(value){
this._strokeStyle=value;
}
});
Object.defineProperty(base, '_lineWidth', {
value: this._lineWidth
});
Object.defineProperty(base, 'lineWidth', {
get: function() {
return this._lineWidth;
},
set: function(value){
this._lineWidth=value;
}
});
this.destroyFlag=true;
}
}
消去の処理の実装はいろいろあろうが、フラグを立てておいて後で上流のループで消す処理としてある。
この消去処理すら、ノード化できんこともない。
こんなような存在は、例えば親子関係にあるオブジェクト構造でも実現しようと思ったらできなくもないが、しっくりこない。常にしっくりこない。
それは結局、他の人が作った概念に自分が勝手に作った概念をむりくり当てはめるからである。であるがゆえに、我々は自分が作った構造には自分で適当な名前を付けたい。そしてその適当に作った構造を一括して扱いたい。それがこのページの趣旨である。
また大前提として、この構造は複雑化すると各々のノードがどの時点でどの能力を獲得するのか追跡するのが困難になる可能性がある。
とりわけゲッターセッターがある時にはあって、ある時にはないような魔界のソースを書くことが可能である。
捕捉
このImporterなる、あるノードに対して一方的な関係を持つノードは、インポート対象に対する参照を保持していないため、インポート対象のパス(ここではあるノードが関係を持っているノードのリストの意味)に格納する必要がある。
このImporterなるノードが対象に対する参照を持つのなら、このノードはどこか主流のループに適当に突っ込めばよい。
Gadget
あるオブジェクトに気まぐれに時計をくっ付けてみたり、エルチカさせてみたりしてみたい。そしてそれらを好きな時に外したりしてみたい。
こうした構造はオブジェクトに親子関係を持たせても可能である。また、オブジェクトがGadgetsなるリストをもってそれをループすれば済む話である。
が、実際にこんな機能はそこまで使うことがない。使い所がない。そのくせ、へんにループばっかり回す。であれば、普段は取り外しておいて眺めておく程度で良い。
//Gadget
class DotMotor{
constructor(target, direction, val){
this.type=NodeType.Gadget;
this._target=target;
this.direction=direction;
this.val=val;
this.Node=[];
}
get target(){
return this._target;
}
get velocityVec(){
return VectorMethod.mult(this.direction, this.val);
}
update(){
NodeUpdate(this, this.Node);
}
exe(){
NodeUpdate(this, this.Node);
this.target.v.x+=this.velocityVec.x;
this.target.v.y+=this.velocityVec.y;
//this._target.v = VectorMethod.add(this.target.v, this.velocityVec);
}
makeReactor(){
return new MotorReactor(this);
}
makeReaction(){
return ()=>{this.val=0;}
}
setupFuel(){
let fuel = new AttachedFuel(this, 100);
let satisfier = fuel.makeStisfier();
let reactor = this.makeReactor();
satisfier.reactor=reactor;
this.Node.push(fuel);
this.Node.push(satisfier);
}
}
このDotMotorノードはDotObjノードの座標をexe()実行毎に移動させる。
変化対象ノードをtargetとして持つため、DotMotorノードはメインのループに放り込んでも良いし、接続先であるDotObjノードのループに放り込んでも良い。両方に放り込むと2回動く。
ここでまた欲が出て、実行毎に向きや速度を変えたり、時間に応じて変化を加えてみたり、燃料なる概念を追加して運動を停止させてみたくなる。
Satisfier
このノードはif文を抽象化したものである。考え方としては、抽象構文木も結局木構造であるのだから、我々のつくった適当なノード関係に組み込むことも可能であろうということである。
//left, rightは値でもfuncでもなんでもいい
class ThresholdStisfier{
constructor(left, right, operator){
this.type=NodeType.Satisfier;
this.left=left;
this.right=right;
this.operator=operator;
this.Node=[];
this.reactor=null;
}
satisfy(){
const leftValue = typeof this.left === 'function' ? this.left() : this.left;
const rightValue = typeof this.right === 'function' ? this.right() : this.right;
return SatisfierMethod.exe(leftValue, rightValue, this.operator);
}
}
const Operator = {
EQUAL: '===',
NOT_EQUAL: '!==',
GREATER_THAN: '>',
LESS_THAN: '<',
GREATER_THAN_OR_EQUAL: '>=',
LESS_THAN_OR_EQUAL: '<=',
};
class SatisfierMethod {
static exe(left, right, operator) {
switch(operator) {
case Operator.EQUAL:
return this.equal(left, right);
case Operator.NOT_EQUAL:
return this.notEqual(left, right);
case Operator.GREATER_THAN:
return this.gt(left, right);
case Operator.LESS_THAN:
return this.lt(left, right);
case Operator.GREATER_THAN_OR_EQUAL:
return this.gte(left, right);
case Operator.LESS_THAN_OR_EQUAL:
return this.lte(left, right);
default:
throw new Error("Unsupported operator: " + operator);
}
}
static equal(left, right) {
return left === right;
}
static notEqual(left, right) {
return left !== right;
}
static gt(left, right) {
return left > right;
}
static lt(left, right) {
return left < right;
}
static gte(left, right) {
return left >= right;
}
static lte(left, right) {
return left <= right;
}
}
このノードはsatisfy()なる関数を持ち、左オペランドと右オペランド、ならびに比較演算子を受け取って評価する。
現状ブールを返す仕様になっているが、別にフラグだけ立てて別のノードがそれを回収しても良い。Reactorなるノードはsatisfyがtrue時に分岐するノードである。
これは結局、上流ノード(今の場合、モーターガジェットノード)における設計の問題であって、処理1と処理2をSatisfierノードによって分岐しても良いし、条件を満たした場合処理1を実行するとしても良い。
SatisfierノードがReactorノードを所持するということは、条件を満たした場合処理を実行するという動作を適当に実現する1個のやりかたというだけである。
Resource
Satisfierは値だけ受け取ってもあんまり意味を成さない。
ちゃんとオペランドにノードを受けてようやく意味を成す。
そのやり方として、ここではbind(this)を使った。
ゲッターをそのまま渡してみたり、ラムダをもちいてなんとかできんかとやってみたが、できんかった。
//power source
//resource
//hasされるか外部にもつか
class AttachedFuel{
constructor(master, val){
this.type=NodeType.Resource;
this._master = master;
this._val = val;
}
//#region get/set
get master(){
return this._master;
}
set master(value){
this._master=value;
}
get val(){
return this._val;
}
set val(value){
this._val=value;
}
//#endregion
// update(){
// this.consume();
// }
//これがありうる
// update(master){
// this.consume(master);
// }
consume(){
this._val-=1;
}
//これはmotorが持ってても良い
makeStisfier(){
let bindVal = function() {
return this.val;
}.bind(this);
let satisfier = new ThresholdStisfier(bindVal, 0, Operator.LESS_THAN);
return satisfier;
}
}
以下の部分で、SatisfierにAttachedFuelの現在のval値を取得する関数を渡している。
let bindVal = function() {
return this.val;
}.bind(this);
let satisfier = new ThresholdStisfier(bindVal, 0, Operator.LESS_THAN);
Satisfier側は、左右のオペランドが関数なら出力値を取得し、最初から値ならそのまま使う。
satisfy(){
const leftValue = typeof this.left === 'function' ? this.left() : this.left;
const rightValue = typeof this.right === 'function' ? this.right() : this.right;
return SatisfierMethod.exe(leftValue, rightValue, this.operator);
}
Reactorノードはsatisfy()が発火したら実行するという処理に組み込む。
この処理はReactorノードに組み込んでも良いし、satisfyフラグを回収するノードを作っても良い。
function SatisfierProcess(base, node){
if(node.satisfy()){
if(node.reactor){
node.reactor.action();
}
}
}
Reactor
このノードは、対象となるSatisfierを保持、あるいは監視して、そのフラグに応じて自身を実行しても良い。そうでないなら、やることは少ない。
class MotorReactor{
constructor(master){
this.type = NodeType.Reactor;
this._master = master;
}
action(){
this._master.val=0;
}
}
この記事が気に入ったらサポートをしてみませんか?