やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

「夜と霧」を読んで「人はなぜ生きるのか」には答えがないことが分かった

最近は技術書だけではなく、さまざまな書籍を読むようになりました。
今回はヴィクトール・E・フランクル著の「夜と霧」を読みました。今更、自分が紹介するまでもなく名著して語り継がれている書籍です。著者のフランクルが第二次世界大戦渦にて、ユダヤ人してアウシュヴィッツに収容された経験を元に「人間とは何か」というテーマとなっています。

フランクルは精神科医であり、心理学の分野では「ロゴセラピー」という心理療法を確立しました。
簡単にいうと「なぜ生きるのか」という理由を充実させることで、メンタルや心の病の解消を目指すという療法だそうです。フランクルは自身の収容所体験からロゴセラピーの考えに至ったそうで「夜と霧」の中でも、後にロゴセラピーとなったであろう出来事やフランクルの思いが各所に現れます。

人はなぜ生きるのか

わたしたちが生きることからなにを期待するのではなく、むしろひたすら、生きることがわたしたちからなにを期待しているかが問題なのだ

第二段階 収容所生活 「生きる意味を問う」より引用

子供の頃からよく「なぜ人は生きるんだろう」と考えていました。 何をしても意味がない、何の成功もしていない自分が嫌で嫌で、なぜ自分は生きているのかと今も同じように悩む時があります。しかし、フランクルから言わせれば「なぜ人は生きるのか」を考えても、答えはでないので、あらゆる場面で「君はこの状況で、どうする?」と問われていると考えて、とくかく行動して答えを見出すべし...とのことです。

苦しみや困難は自分にしか取り除くことができず、誰かに肩代わりしてもらうことはできません。自分自身が苦しみや困難に向き合うことが重要であり、フランクルはアウシュヴィッツでこの考えなしでは生き抜くことができなかったそうです。

と言われても...

「苦しみや困難に向き合って行動せよ」と言われても「いやいや、そんなの自分には無理だよ...」というのが正直な気持ちです。
こんな自分のような人間は、未来にあなたを待っている何かがあることに気づくことが重要だそうです。それは人でも物でも名誉でも何でも良くて、何かが待っていると気づいた人は自分が生きる意味が強くなり、どんなことにも耐えられるようになるとのことです。

とりあえず「なぜ人は生きるのか」と答えのない疑問を考えることはやめて「自分を待っているものは何か」を考えてみようと思います。

人にとって何が一番辛いのか

https://www.pexels.com/ja-jp/photo/236151/

アウシュヴィッツでの収容所生活において、収容人たちにとって一番辛かったのは「期日が定まっていないこと」だと多くの収容人が口を揃えて言っています。 ある年のこと、クリスマスが終わった後に突然、多くの方が命を落とされたというエピソードが本書で紹介されています。多くの人がクリスマスまでには状況がよくなり、家族と穏やかな時間が過ごせるのではないかと期待をしており、それが叶わないと分かった瞬間に免疫力が低下してしまったそうです。

「いつまでこの日々が続くのか...」という不安が人を弱くするというのには非常に納得できます。
仕事でも辛いことが一発あるよりも「何のためにやってるんだろう...」という期間が長いほうが確かに辛いです。当時のアウシュヴィッツの収容生活と比べて遥かに豊かな生活をしているいるにもかかわらず、自分が仕事を通じて、幸福を感じることができない理由が分かりました。

最後に

いかがでしたでしょうか。
今回はフランクル著の「夜と霧」について書評をまとめました。ちなみに「夜と霧」とタイトルはナチスが「夜の間に霧の如く、ユダヤ人を拘束せよ」という作戦名にちなんだものです。 かなりショッキングな事実が書かれており、これは現実の出来事だったんだろうか...と衝撃を受けました。
しかし、フランクルが記載しているように、ただの体験記に終わらず、そこから心理学の一分野への昇華がされており「人はなぜ生きるのか」という問いに悩んでいる人には、ぜひ読んでほしいと思える一冊です。

少しでも「ええな〜」と思ったらイイネ!・シェア!・はてなブックマークを頂けると励みになります。

mastdonからServiceクラスの作り方を学ぶ

みなさんはRailsの開発にて、サービスクラス(ServiceObject)を使っているでしょうか。
モデルやコントローラーがファットになるのを防ぐために導入される設計方法で、ドメイン知識におけるサービス(eg: 商品の購入処理)の一連の処理をまとめたクラスを作成することで、可用性が高くテストがしやすいコードを書くことが可能です。

techracho.bpsinc.jp

特にRailsではActiveRecordの便利さゆえにコントローラーやモデルに処理をゴリゴリと書いてしまいがち。しかし、何も考えずにサービスクラスを無秩序に作ってしまうと逆に保守性が悪い状態になってしまいます。

自分が書いたクソコード

偉そうに言っていますが、先日、自分も良くないサービスクラスを書いてしまいました。
商品情報の登録・更新・削除を担当するサービスクラスであるUserProductクラスです。このクラスを対応するコントローラーのCRUDのエンドポイントから、対応する関数を呼び出していました。

class Services::UserProduct
  def initialize(user_id)
    raise 'user_id is not specified' if user_id.nil?
    @user_id = user_id
  end

  def regist(name:, need_points:, stock_ammount:)
    product = Product.new(name: name, need_points: need_points, stock_ammount: stock_ammount, user_ids: @user_id)
    { successed: product.save, errors: product.errors.full_messages }
  end

  def update(product_id, name:, need_points:, stock_ammount:)
    product = Product.find(product_id)
    product.update!(name: name, need_points: need_points, stock_ammount: stock_ammount)
  end

  def destroy(product_id)
    product = Product.find(product_id)
    product.destroy!
  end
end

しかしながら、このサービスクラスは無秩序で好き放題書かれています。
サービスクラスを導入したことで、コントローラーとモデルのコードはかなりスッキリして恩恵を感じましたが、このクラスは単一責任の原則に違反しています。サービスクラスにするのであれば、商品情報の登録・更新・削除は別々のクラスにするべきでした。
また、ここでは紹介しませんが他のサービスクラスではexecというメソッドが定義されていたりとメソッドの命名規則も特にありませんでした。

「どのようにサービスクラスを作るのが良いかな」と調べていたところ「有名なオープンソースのmastdonにサービスクラスがあるから参考にしてみると良いよ!」という素晴らしい記事を見つけました。

fuyu.hatenablog.com

ということで、mastdonに実装されたサービスクラスを見ながら、自分のクソコードをリファクタリングしてみようと思います。

mastdonのサービスクラス

github.com

mastdonはサービスクラスをappディレクトリの直下に作成したservicesディレクトリに格納しています。
その中で基本的にはサブディレクトリを作ることはしないようです。どのサービスクラスにも__Serviceというクラス名が宣言されています。

いくつかクラスを見た感じ、どのサービスクラスもBaseServiceクラスを継承しており、外部に公開されるパブリックなメソッドはcallのみのようです。BaseServiceではモジュールの読み込みと、可変長な引数を持つcallメソッドを定義するようにインターフェース的な役割を持たせています。

base_service.rb

# frozen_string_literal: true

class BaseService
  include ActionView::Helpers::TextHelper
  include ActionView::Helpers::SanitizeHelper

  include RoutingHelper

  def call(*)
    raise NotImplementedError
  end
end

mastodon/base_service.rb at main · mastodon/mastodon · GitHub

実際のサービスクラスではcallメソッドを定義しています。
call以外のメソッドはプライベートとなっています。興味深いのはinitializeに引数を渡してインスタンス変数を定義するよくある書き方をせずに、callメソッドが呼び出された際にインスタンス変数を定義しているという点です。initializeメソッドを定義しなくて良いというメリットはありますが、自分的にはインスタンス変数を使いたいのであればinitializeメソッドを定義する方が個人的には好みです。

after_block_service.rb(ブロック後の処理)

# frozen_string_literal: true

class AfterBlockService < BaseService
  def call(account, target_account)
    @account        = account
    @target_account = target_account

    clear_home_feed!
    :
  end

  private

  def clear_home_feed!
    FeedManager.instance.clear_from_home(@account, @target_account)
  end
  :
end

mastodon/after_block_service.rb at main · mastodon/mastodon · GitHub

クソコードのリファクタリング

mastdonの実装を踏まえて僕が書いたクソコードをリファクタリングしてみます。
まずは基底クラスの作成しますが、今回は特に読み込みが必要なモジュールなどはないので、callメソッドを定義しただけのクラスとなりました。

# frozen_string_literal: true

class BaseService
  def call(*)
    raise NotImplementedError
  end
end

次に商品情報の登録・更新・削除の処理を単一責任の原則に従って、それぞれのクラスに分解してcallメソッドのみをパブリックにします。

登録のサービスクラス

# frozen_string_literal: true

class CreateProductService < BaseService
  def call(user_id:, name:, need_points:, stock_ammount:)
    product = Product.new(name: name, need_points: need_points, stock_ammount: stock_ammount, user_ids: user_id)
    { successed: product.save, errors: product.errors.full_messages }
  end
end

コントローラーからは以下のように呼び出します。

def create
  res = CreateProductService.new.call(
    user_id: params[:user_id],
    name: params[:name],
    need_points: params[:need_points],
    stock_ammount: params[:stock_ammount],
  )
  render json: res, status: :ok
end

このやり方でも良いのですが、インスタンス変数を使わないのであればクラスメソッドとして定義してしまった方が良いかもしれません。
現時点ではただ、データを登録するだけなのでサービスクラスの恩恵を感じませんが、今後、メールの送信や合わせて作成しないといけないデータなどが追加された際にはこのサービスクラスのみに手を加えれば良いです。

サービスクラス大量発生?

とはいえ、コントローラーの全てのエンドポイントでサービスクラスを作るのは非常に面倒です。
このままだと大量のサービスクラスが作られていくことになります。mastdonではどうなっているでしょうか。
どうやらmastdonでは全ての箇所でサービスクラスを使っているわけではないようです。

# frozen_string_literal: true

class Api::V1::Statuses::PinsController < Api::BaseController
  include Authorization

  before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
  before_action :require_user!
  before_action :set_status

  # このエンドポイントではサービスクラスを使わず処理がベタ書きされている
  def create
    StatusPin.create!(account: current_account, status: @status)
    distribute_add_activity!
    render json: @status, serializer: REST::StatusSerializer
  end
end

mastodon/pins_controller.rb at main · mastodon/mastodon · GitHub

どういった基準でサービスクラスの作成を検討しているのか分かりませんが、推測するに比較的長い処理、関連する処理が多い箇所ではサービスクラスを作成しているようです。 ただ、こんなサービスクラスもあるので真相は謎のままです。

class AfterUnallowDomainService < BaseService
  def call(domain)
    Account.where(domain: domain).find_each do |account|
      DeleteAccountService.new.call(account, reserve_username: false)
    end
  end
end

mastodon/after_unallow_domain_service.rb at main · mastodon/mastodon · GitHub

トランザクションがあれば安心ではないのでRailsでもロックをかけたい

Railsでトランザクションを扱いたい時はActiveRecordに実装されたtransactionメソッドを呼び出します。

ActiveRecord::Base::transaction do
  :
end

今までRailsでトランザクションのブロックを宣言した時は、他のトランザクションからデータの取得、更新は出来ないと思っていましたが、先日、勘違いであることに気づきました。更新はトランザクションの待機によってすぐには実行されませんが、データの取得は可能です。取得もさせたくないのであれば排他制御を行い明示的にロックの宣言をする必要があります。

トランザクションのブロックを宣言しただけでは、他のトランザクションからデータの取得は可能なため、データの整合性が合わなくなる可能性があります。

発行されるSQL

トランザクションのみ

ただトランザクションが実行されるのみで、ロックはされないため他のトランザクションからデータの取得は可能だが、更新はできない。

ActiveRecord::Base::transaction do
  product = Product.find(product_id)
end

# -- SQL --
# BEGIN;
# SELECT * FROM products WHERE product_id = product_id;
# COMMIT;

トランザクションとロック

.lockを使うことでレコードに対してロックがされます。FOR UPDATESELECT句に付与されていることが分かります。

ActiveRecord::Base::transaction do
  product = Product.lock.find(1)
end

# -- SQL --
# BEGIN;
# SELECT * FROM products WHERE product_id = product_id FOR UPDATE; 
# COMMIT;

ActiveRecord::Locking::Pessimistic

もしくはwith_lockを使うことも可能。ブロック内は1つのトランザクションとして実行されるので、複数のデータにロックをかけたい時には便利そう。

product = Product.find(1)
product.with_lock do
  user = User.lock.find(1)
end

# -- SQL --
# BEGIN;
# SELECT * FROM products WHERE product_id = product_id FOR UPDATE;
# SELECT * FROM users WHERE user_id = user_id FOR UPDATE;
# COMMIT;

ロックをしている場合の挙動

ターミナルAから特定のレコードにロックをかけた状態で、ターミナルBから同じレコードに対して更新処理を行ってみました。結果は以下の通りで、ROLLBACKされていることが分かります。

更新処理ではなく、読み取りであればエラーにならずに実行することができました。

購入処理に再チャレンジ

先ほど、図で示した購入処理をロックを使用して実行してみます。トランザクションAをターミナルA。トランザクションBをターミナルBとして実行しました。ターミナルBでは一度、エラーとなってしまいましたが、再度実行することで最終的には整合性のとれた在庫数になってくれました。

ロック時のタイムアウト値はpostgresql.confにて設定が可能なのですが、0秒になっていたので3秒に変更して再度、実行してみました。先ほどとは違い実行時にタイムアウトエラーが発生していないことが分かります。