sky’s 雑記

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

2019年が始まり、30歳を手前にして10年ぶりくらいのとてつもない悔しさを感じているので、今年の抱負をまとめる

転職活動してるときに選考で落ちた会社の記事を見て久々に超悔しい状態。

この2年もっと有効に使えたかもしれない、会社のブランドに胡座をかいていたんだろう。 仕事において当事者意識を持っていないとなんとなく働くことになって全然成長しない。 この2年間はチーム規模やステークホルダーの多さで、明らかに携わるプロダクトが他人事だったしそれじゃだめだと思って転職することにした、 けど場所を変えただけで結局プロダクトの規模が大きくなったらぶち当たる課題。 この5年くらいは自分にできるやり方で長所を伸ばすことをしてきたけどこれからはそうじゃない方法も取っていこう、会社という後ろ盾もなくなるし自分から学びに行かないと置いていかれるだろう。

2019年の抱負

  • これから携わるプロダクト、組織がどんなものであっても自分ごととして捉える
  • 規模が大きくなったとき、それをどう自分ごととして捉えられるか解決策を考える
  • リアルでのアウトプットにこだわる
  • エンジニアとしてもっと成長する動機を考える(これがプロダクト、会社の利益に直結していると良い)
  • あいつらには負けねええええええええええええええ

以上

若干の後悔を残しつつ辞める

退職に関するところでピンポイントで言うと、もっと人を信用して話してみればよかった。

もうちょっと広い話だと、選択は必ず自分でしないといけない、けど自分が必ず正しい選択をできるとは限らない、 それでもなるべく正しい選択をするために、視座をあげて多くの可能性を認識している必要がある。

結論、 もっと人の話を聞こうと思う。 1人がそうだといって納得できなくてもさすがに100人に言われたら納得するかもしれない。

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することがまずは重要かなと思う。