sky’s 雑記

露頭に迷うエンジニア雑記

javaとkotlinの世界のstaticとsingleton

javaでよくあるユーティリティクラスをkotlinに移植しようと思ったときに気づきがあったのでまとめておく。

javaユーティリティクラスのkotlin convert

事の発端はjavaで記述された以下のユーティリティクラス(staticなメンバーのみをもつクラス)をkotlinにconvertしたとき、object SampleUtil で定義されていたこと。kotlinでobjectを定義するとsingletonになるはずで、本来インスタンスを生成しないはずのものがsingletonに変換されていることを不思議に思った。

例えばaws-android-sdkにあったこのSampleUtil、 これをkotlinにconvertすると、

public class SampleUtil {
    private static final String PropertyFile = "aws-iot-sdk-samples.properties";

    public static class KeyStorePasswordPair {
        public KeyStore keyStore;
        public String keyPassword;

        public KeyStorePasswordPair(KeyStore keyStore, String keyPassword) {
            this.keyStore = keyStore;
            this.keyPassword = keyPassword;
        }
    }
...
}

SampleUtilはobjectで定義されていて、staticだったものがsingletonに変換される。

object SampleUtil {
    private val PropertyFile = "aws-iot-sdk-samples.properties"

    class KeyStorePasswordPair(var keyStore: KeyStore, var keyPassword: String)

    fun getConfig(name: String): String? {
        val prop = Properties()
        val resource = SampleUtil::class.java.getResource(PropertyFile) ?: return null
        try {
            resource.openStream().use { stream -> prop.load(stream) }
        } catch (e: IOException) {
            return null
        }

        val value = prop.getProperty(name)
        return if (value == null || value.trim { it <= ' ' }.length == 0) {
            null
        } else {
            value
        }
    }
...
}

kotlinでobjectで定義するsingletonになることは知識としては前から知っていたので、javaにおけるユーティリティクラス(staticメンバのみを持つクラス)をkotlinにconvertすると class SampleUtil内のcompanion object にメンバーを定義する形になると想像していたが結果は違った。また多くのピュアkotlinで書かれたプロジェクトのユーティリティクラスも同じようにobjectでシングルトンを生成していたことも混乱する原因となった。

objectとcompanion objectのおさらい

詳細は以下にまとまっているが、改めてobjectcompanion objectのおさらいをする。 Object Expressions, Object Declarations and Companion Objects - Kotlin Programming Language

objectで定義したDataProviderManagerはシングルトンで生成され、

object DataProviderManager {
  fun registerDataProvider(provider: DataProvider) {
    // ...
  }

  val allDataProviders: Collection<DataProvider>
    get() = // ...
}

DataProviderManager.registerDataProvider(...) のように使える。

companion object についても同じように、

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create() のように使える。

objectがsingletonということはcompanion objectはstaticだろうと思っていたのが勘違いの始まりでちゃんとドキュメントを読むと以下のようにstaticに見えるメンバーもオブジェクトインスタンスのメンバーであるという記述がある。

Note that, even though the members of companion objects look like static members in other languages, at runtime those are still instance members of real objects, and can, for example, implement interfaces:

objectとcompanion objectはjavaでどう解釈されるか

公式ドキュメントによればobjectcompanion objectも非staticでありインスタンスは生成されているということだが、実際にjavaではどのように解釈されるのか、 理解を深めるために実際に以下のktクラスをdecompileした。

object
object SampleUtil1 {
    fun test() {
        Log.e("SampleUtil1", "test")
    }
}
@Metadata(
   mv = {1, 1, 11},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\bÆ\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
   d2 = {"Lcom/hoge/util/asset/SampleUtil1;", "", "()V", "test", "", "production sources for module app"}
)
public final class SampleUtil1 {
   public static final SampleUtil1 INSTANCE;

   public final void test() {
      Log.e("SampleUtil2", "test");
   }

   static {
      SampleUtil1 var0 = new SampleUtil1();
      INSTANCE = var0;
   }
}
companion object
class SampleUtil2 {
    companion object {
        fun test() {
            Log.e("SampleUtil2", "test")
        }
    }
}
@Metadata(
   mv = {1, 1, 11},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\u0018\u0000 \u00032\u00020\u0001:\u0001\u0003B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0004"},
   d2 = {"Lcom/hoge/util/asset/SampleUtil2;", "", "()V", "Companion", "production sources for module app"}
)
public final class SampleUtil2 {
   public static final SampleUtil2.Companion Companion = new SampleUtil2.Companion((DefaultConstructorMarker)null);

   @Metadata(
      mv = {1, 1, 11},
      bv = {1, 0, 2},
      k = 1,
      d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
      d2 = {"Lcom/hoge/util/asset/SampleUtil2$Companion;", "", "()V", "test", "", "production sources for module app"}
   )
   public static final class Companion {
      public final void test() {
         Log.e("SampleUtil2", "test");
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

metadataとかがあって弱化読みにくいが、SampleUtil1については想定どおりでstaticイニシャライザでSampleUtil1をシングルトンで生成している。 一方companion object のほうはというと、companion object 内で定義したメンバーはsitaticインナークラスで定義されたCompanionクラス内に定義されている。 SampleUtil2自体はインスタンス化されていないが内部のCompanionクラスがシングルトンで生成されるのでやっていることとしてはシングルトンインスタンスを作るということでobject定義のほうと大きな差はないように思う。

結論

  • kotlinではstaticを許容しない
  • companion objectobject もシングルトンインスタンスを生成する

以下参考にした記事など

yyyank.blogspot.com

Kotlin - インスタンス化しないようにする[kotlin]|teratail

www.kaitoy.xyz

futurismo.biz

hydrakecat.hatenablog.jp

クラス継承とデータベース設計

一応下記の続き的な立ち位置

iwsksky.hatenablog.com

オブジェクト指向言語でリレーショナルデータベースとモデルをつなぐオブジェクトマッピングで、 どのように継承関係を表すかというのは結構難しい話題なのだが、 それについてちょっと書いてみたいと思う。

継承とデータベース設計

アプリケーションの実装でクラス間に継承関係があった場合に、それをデータベース側で表現する方法として、 PofEAAに以下3つが示されている。

  • 単一テーブル継承

単一テーブル継承

  • クラステーブル継承

クラステーブル継承

  • 具象テーブル継承

具象テーブル継承

ちなみに今回はRoRの実装をしていて感じたことなのでRoR前提で記す。

ActiveRecordの難しさ

RoRActiveRecordはその名の由来にもなっているように、 データベースとアプリケーションをつなぐアーキテクチャのうち、 アクティブレコードパターンで実装されている。

そしてアクティブレコードパターンは他のパターンに比べてアプリケーションとテーブルが密結合である。 (これについては同じくPofEAAの10章参照) ActiveRecord

実装しているうちは難しさを感じるものの、それを言語化できなかったのだが、 アクティブレコードを使った実装の難しさは密結合であるがゆえに、 アプリケーションとテーブルどちらを優先して考えたらいいかわからなくなるということだと思う。

具体的にそれぞれを優先した場合のpros/consを書いてみたいと思う。

アプリケーション優先

フレームワークORMによって違うと思うが、 RoRでは継承の表現として単一テーブル継承のみサポートしている。 単一テーブル継承は上述のとおり1テーブルで継承クラスを表現する、 具体的にはtypeカラムをテーブルに持たせて各クラスの差分についてはnullになるというもの。 概念としては異なるがテーブル自体はポリモーフィック関連と似たものになると思う。

そしてRoRに限定した場合、アプリケーションを優先させることはSTIを採用することと言っていいと思う。

pros
  • RoRでサポートされているのでアプリケーション側の実装が容易である
  • 単一テーブルなのでテーブル間での参照が生じずシンプルなクエリ発行で済む
cons
  • 完全新規の実装でない場合データベース側のスキーマの変更が生じる (user:個人とcompany:法人というテーブルがもともと存在し、その両者に課金機能をもたせたい場合、これらテーブルをマージしてtypeカラムを追加する必要がある)
  • 外部キー制約がかけられないので外部テーブルとの連携に弱い
  • nullカラムが増えるのでテーブルがスパースになる
  • typeカラムがカーディナリティ低くなりがちなのでクラスごとに絞り込みを行うことにつらみがある
  • (なんとなく)STIを直感的に気持ち悪いと感じる開発者が多い

データベース優先

STI/CTI/CCIどれが優れたパターンかという話ではなく、 アプリケーション側の事情を考慮せずデータベース設計を行うという話。 STIを用いない場合ある程度自力での実装が必要になるがその場合はアプリケーション側で吸収する。

pros
  • データベース側に変更が生じないので大掛かりになりにくい
  • (なんとなく直感的に綺麗だと感じる)CTI/CCIが選択できる
cons
  • 共通カラムを参照するような基本的なレコードであってもジョインが必要になる
  • スーパークラスを継承するクラスが増えたときにアプリケーションのコードが煩雑になりやすい
RoRでの実装

STIの実装例はググればいくらでも出てくるので、参考までに継承を使った場合の実装を書いておく。 気をつけなければならないのは、親クラスが外部に持つアソシエーションを子クラスで呼び出した場合に、 子クラスの外部キーが用いられること。 なので親クラスに定義したアソシエーションを子クラスで呼び出した場合は子クラスのidが外部キーとして使用される。 親クラスにdelegateして user.subscriptions した場合も customer.subscriptions が呼ばれるようにするなど対応が必要だと思う。

class Customer < ApplicationRecord
  has_many :subscriptions
  has_one :user
  has_one :company
  
end
class User < Customer
  belongs_to :customer
  delegate :subscriptions, to: :customer
end
class Company < Customer
  belongs_to :customer
  delegate :subscriptions, to: :customer
end

結論

どっち優先で考えるかは実装と可用性のバランスをどう取るかかなと思った。 ことRoRにおいてはCTI/CCIを選択したとしてもそれほどつらみを感じないので選択肢としてはありなのかなと思う。 また実装者が違和感を感じるか、というのは結構重要な観点で、STIに違和感を感じる実装者がいるのもまた事実、実際のところとくに問題のないアーキテクチャだとしても違和感を潰すこととか違和感を感じないものを選択することもあわせて重要なんだろうなと思う。

ref

この辺も参考になると思う。 github.com

有名なGemのソースコードを読んで仕組みを理解する Pundit編

rails向けの権限管理GemにPunditっていうものがあるんですが、 いい感じに権限周りの記述を1ファイルにまとめられて見通しの良いGemだなと感じたので、 ソースコードを読んでみました。

pundit (2.0.0) github.com

簡単な説明

使い方の説明記事ではないのでざっと書くと、 各リソースに対してPolicyというテンプレートファイルを内で権限をメソッド単位で定義できるというもの。

たとえばHoge.rbの権限を設定したかったらApplicationControllerでPunditをincludeしたあとに、

class ApplicationController < ActionController::Base
  include Pundit
end

Policyファイルを作って

class HogePolicy
  def show?
    #ここにHogeリソースの閲覧権限を書く。
  end

  .
  .
  .
end

使うときは

class HogeController
  def show
    authorize @hoge
  end
end

みたいな感じに、 if current_user hoge みたいな条件分岐記述が分散せず1箇所にまとめられて見通しがよくなる。

Gemの中身の概要

中身は結構シンプルでコアは以下の2つのファイル

pundit/pundit.rb at master · varvet/pundit · GitHub

pundit/policy_finder.rb at master · varvet/pundit · GitHub

アプリケーションとGemのインターフェースは先述の通り authorize メソッドで、 これはPunditモジュールに定義されているのでアプリケーション側でincludeすると利用できるようになる。

リソースとポリシーの紐づけはauthorizeメソッドとPolicyFinderクラス内のfindメソッドで行っている。シンプルなアプリケーションであれば権限管理したいリソースのオブジェクトのみをauthorizeの引数として渡すことが多いと思うが、query つまり権限設定に利用するメソッドと policy_class 権限設定が記述されているクラスをカスタマイズできる。デフォルトでは query"#{action_name}?" であり、 policy"#{klass}#{SUFFIX}" となる。

https://github.com/varvet/pundit/blob/master/lib/pundit.rb#L202

def authorize(record, query = nil, policy_class: nil)
    query ||= "#{action_name}?"

    @_pundit_policy_authorized = true

    policy = policy_class ? policy_class.new(pundit_user, record) : policy(record)

    raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)

    record
  end

pundit/policy_finder.rb at master · varvet/pundit · GitHub

def find(subject)
    if subject.is_a?(Array)
      modules = subject.dup
      last = modules.pop
      context = modules.map { |x| find_class_name(x) }.join("::")
      [context, find(last)].join("::")
    elsif subject.respond_to?(:policy_class)
      subject.policy_class
    elsif subject.class.respond_to?(:policy_class)
      subject.class.policy_class
    else
      klass = find_class_name(subject)
      "#{klass}#{SUFFIX}"
    end
end

以上でリソースとポリシーの紐づけが済むので、

raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)

で、HogePolicyの該当アクションを実行することで権限がfalseであれば NotAuthorizedErrorを流せるというわけだ。

非常にシンプルである。

ちなみに

本題とは若干それるが、スコープごとに権限管理を行う対象(user)やポリシーファイル自体もカスタマイズできるようで policy, pundit_policy_scope, pundit_user のオーバーライドが可能なようだ。このあたりActiveSupport::Concern とかhelper_methodで動的に実現しているのもrubyらしいのかなと思いなかなか興味深かった(普段java/kotlinみたいな静的な言語を書くことが多いので尚更)

included do
  helper Helper if respond_to?(:helper)
  if respond_to?(:helper_method)
    helper_method :policy
    helper_method :pundit_policy_scope
    helper_method :pundit_user
  end
end

課金周りのテーブル設計について

とあるサービス開発に携わっていて課金周りの実装に悩ましさを抱えている。

具体的には 課金ユーザーが持つcustomer_idをどのようにもたせるかということだ。 toCのサービスであればユーザー=課金ユーザーなのでuserテーブルにcustomer_idを付与すればいいと思うんだが、 今回はユーザー単位の場合もあれば会社単位の場合もある、といった具合。

脳死状態で実装するとすればcompanyテーブルにcustomer_idがあり、userテーブルにもcustomer_idがあるみたいな感じになるんだが、 これはcustomer_idが点在してるのが微妙だなーと思う。

設計的には中間テーブルでcustomerテーブルとつなぐのが丸い。 companies - company_customers - customers users - user_customers - customers 的な具合。

設計的にはこれでいいんだが実はまだ問題はあって、 userがcompanyに属する場合だ。 この場合以下のようなことを検討する必要があるかなと思う。

  • userとcompanyが同じものを課金する可能性があるか
  • userとcompanyの二重課金を許可するか
  • 課金側は何をもって課金ユーザーを一意に特定するか(たとえばemailで一意に特定する場合、サービス側でuserとcompanyが同じemailを持っていたらどうなるかなど)

なにはともあれ外部の課金サービスでAPI叩いている場合などは、 自分のサービス側で課金情報を適切に保持してしっかりとsyncすることがまずは重要かなと思う。

頼まれごとを断る勇気と決意

人にお願いされると断るのが苦手で損をすることを何度か経験してきた。

だから頼まれたら断れないけど損もしたくないということで、 そもそも土俵に乗らない、つまり頼まれない状況を意図的に作ってきた。

だがこれって逆を返すとこっちからのお願いもやりにくくなるということでもある。 ビジネスはgive&takeかつ交渉であり相手の要求と自分の要求をすり合わせる必要があるからだ。

ということで相手と交渉をすることをこれからは頑張っていくことにした、 自分の要求を通すために相手の要求を飲むし必要であれば交渉するし断る。

結構勇気いるのよねこれって。。。

だが俺は自分のキャリアのために一歩踏み出す。

【Ethereum】Dapps開発で前提となるEthereumネットワークについて

f:id:iwsksky:20180814025638p:plain

中央集権型のアプリケーション同様に、 分散型アプリケーションであるDappsにもデプロイという作業が存在します。

中央集権型のアプリケーションを開発したことがある方であれば、 開発はlocalhostにサーバーを立てて行い、 本番であればクラウドにサーバーを立ててデプロイを行うといった工程が想像できると思います。

ですがDappsをどのようにEthereumネットワークにデプロイするか想像がつかないのではないでしょうか。

この記事ではDapps開発をする上で知る必要があるEthereumネットワークの種類について解説します。

1. プライベートネットワーク

gethのcliやganacheで起動されるネットワークはこのプライベートネットワークにあたります。 一個人により管理されるEthereumネットワークであり、中央集権型のネットワークと言い換えることができるかもしれません。 自由にEtherの発掘を行うことができ閉じたネットワークであるためDappsのテストといった開発用とに適したネットワークと言えます。

2. テストネットワーク

プライベートネットワークとは異なり複数のノードにより管理されるネットワークです。 主要なネットワークとしては以下のようなものがあります。

Ropsten

TESTNET Ropsten (ETH) BlockChain Explorer

もっともEthereumネットワークに近いテストネットワークと言われています。 合意形成アルゴリズムはPoWです。

Ropstenネットワークは2016年運用開始でにストックホルムの駅名から名付けられました。

2017年にはDoS攻撃を受けてGas代が上がるといったこともあったようですが、 現在ではもっとも有力なテストネットワークといって良いと思います。

Ethereum Test network – Coinmonks – Medium

Ropsten testnet is under kind of attack? What can we do? - Ethereum Stack Exchange

ropsten/revival.md at master · ethereum/ropsten · GitHub

# Pros
  • メインネットと同様の環境でありDappsのテスト環境としては最有力
  • geth/parity両方とも利用可
  • Etherの発掘可

Ethernet Faucet

# Cons
  • 前述の通りDosのようなスパム攻撃を受ける可能性がある。
Kovan

Parityチームによりサポートされるテストネットワークです。 こちらは2017年4月運用開始でシンガポールの駅名から名付けられました。

# Pros
  • Etherのサプライが信頼できる機関により管理されているため、スパム攻撃に強い
# Cons
  • geth利用不可
  • Etherの発掘不可
  • 合意形成アルゴリズムがPoSでありメインネットと環境が異なる
Rinkeby

Gethチームによりサポートされるテストネットワークです。 こちらも2017年4月運用開始でストックホルムの地下鉄の駅名から名付けられました。

# Pros
  • Etherのサプライが信頼できる機関により管理されているため、スパム攻撃に強い
# Cons
  • gethのみ利用可能
  • Etherの発掘不可
  • 合意形成アルゴリズムがPoSでありメインネットと環境が異なる

3. メインネットワーク

開発者目線では本番環境のネットワークであり、Ethereumや一般公開されているDappsはメインネットワーク上で動いています。 2015年7月より運用開始。

メインネットに接続する · Ethereum入門

【Ethereum/solidity】ゼロから始めるDapps開発

ブロックチェーン周りの技術は日進月歩で発展しているので、 1年前の情報でも既に使い物にならないということも多い。

この記事ではブロックチェーン未経験のエンジニアが、 Ethereumを利用したアプリケーション(Dapps)を開発するとき、 何をすればよいかまとめていく。

ちなみに私は業務ではRailsウェブアプリケーション、kotlinでandroidアプリなどを開発している、 エンジニア歴としては5年程度なのでマイルストーンとして参考にしてほしい。

1. CryptoZombies

cryptozombies.io

Dapps開発にはsolidityというjavascriptライクな言語を用いる。 このsolidityの文法からクライアント側との連携まで網羅的に学ぶことができるのがCryptoZombiesである。 講座はLesson1から6まで存在しており、はじめは初歩的なsolidity文法から学ぶことになる、 若干退屈感は否めないがところどころ中央集権型のアプリケーションには存在しない、 ブロックチェーン特有の言語概念が存在するので是非CryptoZombiesに取り組むことをおすすめする。

ちなみにCryptoZombiesもまたLoomNetworkを利用したDappsである。

2. Pet Shop Tutorial

Truffle Suite | Tutorials | Ethereum Pet Shop -- Your First Dapp

solidityに慣れDappsの概要がつかめるようになったら、 より詳細なDapps開発の生態系を知りたくなるはずだ。 だがCryptoZombiesを終えただけでは、 Dappsを開発しようと思ったときどうしてよいかわからないということになると思う。

Pet Shop Tutorialでは、 ローカルでブロックチェーンを利用できるGanacheやDappsフレームワークであるtruffleを用いて、 実際にペットをethで購入できるアプリケーションの開発を行う。

Pet Shop Tutorialを終えると自分の作りたいDappsがあったとき何すればよいのか、 具体的な道筋が頭の中に思い描けるレベルになっているはずだ。

終わりに

簡単にDapps開発が行えるようになるまでにやることを紹介したが、 私もまだ初心者なので逐一記事の更新を行っていこうと思う。