javascriptに静的初期化フィールドがあることに今更ながら気づいた
なぜ今まで気づかなかったのだろう
これは私が探し求めていたものだった
どう探し求めてたのかをとくに整理せずにつらつらと話したい
in Ruby
例えば、rubyでこんなモデルがあったとする
class Asset
def initialize(asset)
@asset = asset
end
def id
@asset[:id]
end
def name
@asset[:name]
end
def path
@asset[:path]
end
def size
@asset[:size]
end
def self.find_by_id(id)
all.find{ it.id == id }
end
def self.find_by_path(path)
all.find{ it.path == path }
end
def self.select_by_name(name)
all.select { it.name == name }
end
def self.all
[
{id: 1, name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{id: 2, name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
{id: 3, name: 'hoge.zip', path: '/path/to2/hoge.zip', size: 300},
].map{ new(it) }
end
end
rubyは備えつきで色々便利なメソッドあったり、active_supportいれたらactive_modelとかも使えるんだけど、一旦それらを知らない世界線だったとしてこれをリファクタリングしていく
Assetと似たような設計のモデルが多くあることを想定して既定クラスを作る
class Record
def initialize(attributes)
@attributes = attributes
end
end
class Asset < Record
def id
@attributes[:id]
end
def name
@attributes[:name]
end
def path
@attributes[:path]
end
def size
@attributes[:size]
end
def self.find_by_id(id)
all.find{ it.id == id }
end
def self.find_by_path(path)
all.find{ it.path == path }
end
def self.select_by_name(name)
all.select { it.name == name }
end
def self.all
[
{id: 1, name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{id: 2, name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
{id: 3, name: 'hoge.zip', path: '/path/to2/hoge.zip', size: 300},
].map{ new(it) }
end
end
ゲッターを作るのがめんどくさいのでよしなにする
class Record
def initialize(attributes)
@attributes = attributes
end
def self.attributes(*attrs)
attrs.each do |attr|
define_method(attr) do
@attributes[attr]
end
end
end
end
class Asset < Record
attributes :name, :path, :size
def self.find_by_id(id)
all.find{ it.id == id }
end
def self.find_by_path(path)
all.find{ it.path == path }
end
def self.select_by_name(name)
all.select { it.name == name }
end
def self.all
[
{name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
].map{ new(it) }
end
end
find,select系の処理も動的に作っちゃう
class Record
def initialize(attributes)
@attributes = attributes
end
def self.attributes(*attrs)
attrs.each do |attr|
define_method(attr) do
@attributes[attr]
end
end
end
def self.selectable_by(*attrs)
attrs.each do |attr|
define_singleton_method("select_by_#{attr}") do |id|
all.select { it.send(attr) == id }
end
end
end
def self.findable_by(*attrs)
attrs.each do |attr|
define_singleton_method("find_by_#{attr}") do |id|
all.find { it.send(attr) == id }
end
end
end
end
class Asset < Record
attributes :name, :path, :size
findable_by :id
selectable_by :path, :name
def self.all
[
{name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
].map{ new(it) }
end
end
allも元となるデータの取得方法だけ用意させれば抽象化できそうだ
class Record
def initialize(attributes)
@attributes = attributes
end
def self.attributes(*attrs)
attrs.each do |attr|
define_method(attr) do
@attributes[attr]
end
end
end
def self.selectable_by(*attrs)
attrs.each do |attr|
define_singleton_method("select_by_#{attr}") do |id|
all.select { it.send(attr) == id }
end
end
end
def self.findable_by(*attrs)
attrs.each do |attr|
define_singleton_method("find_by_#{attr}") do |id|
all.find { it.send(attr) == id }
end
end
end
def self.resource(&block)
define_singleton_method(:all) do
block.call.map{ new(it) }
end
end
end
class Asset < Record
resource {
[
{name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
]
}
attributes :name, :path, :size
findable_by :id
selectable_by :path, :name
end
となる。
いいなーと思う。 これをJSで再現したい
in JS
まずはAssetモデル単品
class Asset {
constructor(asset) {
this.asset = asset
}
get id() {
return this.asset['id']
}
get name() {
return this.asset['name']
}
get path() {
return this.asset['path']
}
get size() {
return this.asset['size']
}
static findById(id) {
return this.all().find(it => it.id === id)
}
static findByPath(path) {
return this.all().find(it => it.path === path)
}
static selectByName(name) {
return this.all().filter(it => it.name === name )
}
static all() {
return [
{id: 1, name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{id: 2, name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
{id: 3, name: 'hoge.zip', path: '/path/to2/hoge.zip', size: 300},
].map(record => new this(record))
}
}
で、次はコンストラクタの共通化とgetterの動的生成
rubyと違ってclass bodyに処理を直接かけないのでコンストラクタで生やす
class Record {
constructor(attributes) {
this.attributes = attributes
for (let attr of this.constructor.attributes || []) {
Object.defineProperty(this, attr, {
get() {
return this.attributes[attr]
}
})
}
}
}
class Asset extends Record {
static attributes = ['id', 'name', 'path', 'size'];
static findById(id) {
return this.all().find(it => it.id === id)
}
static findByPath(path) {
return this.all().find(it => it.path === path)
}
static selectByName(name) {
return this.all().filter(it => it.name === name )
}
static all() {
return [
{id: 1, name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{id: 2, name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
{id: 3, name: 'hoge.zip', path: '/path/to2/hoge.zip', size: 300},
].map(record => new this(record))
}
}
さて、次はstaticの動的生成...
と行きたいところだが今回作るのはクラス自身のメソッドなのでコンストラクタでは少々タイミングが遅い
Recordクラスでstaticメソッドをきって、中でthisに直接これらを生やすメソッドを作ったとしてもそれらをコールする処理はクラス外になってしまい収まりが悪い
ずっとこれがいやだな〜〜〜〜〜いやだな〜〜〜〜って思ってたんだけどひょんなタイミングで冒頭の静的初期化ブロックを見つけた
これはrubyのクラスボディで静的メソッドを呼び出すやつとほぼ同じことができるじゃん!!となって感動した
これを使ってRecordとAssetを書き直すとこんな風にかける
String.prototype.upperCammelCase = function () {
return this.replace(/^.|_./g, match => match.replace(/_(.)/, '$1').toUpperCase());
}
class Record {
constructor(attributes) {
this.attributes = attributes;
}
static attributes(...attributes) {
for (let attr of attributes) {
Object.defineProperty(this.prototype, attr, {
get() {
return this.attributes[attr];
}
})
}
}
static findableBy(...attributes) {
for (let attr of attributes) {
this[`findBy${attr.upperCammelCase()}`] = function (anyId) {
return this.all().find(record => record[attr] === anyId);
}
}
}
static selectableBy(...attributes) {
for (let attr of attributes) {
this[`selectBy${attr.upperCammelCase()}`] = function (anyId) {
return this.all().filter(record => record[attr] === anyId);
}
}
}
static resource(fn) {
this.all = function () {
return fn().map(record => new this(record))
}
}
}
class Asset extends Record {
static {
this.resource(() => ([
{id: 1, name: 'hoge.zip', path: '/path/to/hoge.zip', size: 100},
{id: 2, name: 'piyo.zip', path: '/path/to/piyo.zip', size: 150},
{id: 3, name: 'hoge.zip', path: '/path/to2/hoge.zip', size: 300},
]))
this.attributes('id', 'name', 'path', 'size')
this.findableBy('id')
this.selectableBy('path', 'name')
}
}
となる。 文法上のやぼったさが全くないわけじゃないがかなりrubyライクに書くことができる
最後にこれを使って簡単にメールのツリーを構築するコードを簡単に書いてみよう
See the Pen Untitled by Takuya Nakajima (@Takuya-Nakajima) on CodePen.
うーん、いいですな〜