4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

静的初期化ブロックを知らなかった昨日までのワイへ

Last updated at Posted at 2025-10-30

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.

うーん、いいですな〜

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?