DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

Pythonエンジニアが感じたRailsの戸惑いポイント

この記事ははじめてのアドベントカレンダー Advent Calendar 2023 21日目の記事です。

初めまして。 2023年10月にDIGGLEにエンジニアとして入社したaki0344です。

DIGGLEではバックエンドにRuby on Railsを利用しています。
そこで今回は、小学生の娘に『ルビィのぼうけん』を買ったぐらいしかRubyとの関わりのなかった私が、Ruby on Rails(以下Rails)を学習する過程で感じた内容について書いてみたいと思います。
また、それだけだとただの感想文になってしまうので、Rails初心者をチームに受け入れるためにこんな用意をしておくとキャッチアップが早まるかもしれませんというポイントも書きたいと思います。

前提

本題に入る前に、私のバックグラウンドについて書いておきたいと思います。
DIGGLE入社前は中小独立系SIerでバックエンドエンジニアとして働いていました。
業務で利用していた言語は直近から順におおよそ以下の通りです。

  • Python(5年)
  • Java(10年)
  • C(1年)
  • C++(2年)

DIGGLEに入社するにあたり、主にProgate、およびRuby on Railsチュートリアルで学習を進めました。
上記のJavaから下に記載した言語は業務から離れて久しいため、今回は直近のPythonのメジャーなフレームワークDjangoと比較した際のRails特有の書き方にフォーカスを当てていきたいと思います。

Railsの便利な点

まずはDjangoと比較して便利だと思った点を書いていきます。

MVCモデル

RailsはMVCモデルでの開発を行うためのフレームワークとなっています。
DjangoもMVCモデルに似たMTVモデルを採用しており、Djangoでの開発経験があれば比較的スムーズにRailsにも入っていけるのではないかと感じました。
また、次の項目にも書く通り、Djangoよりもより厳格な運用となっており、プロジェクトによる差異が起きにくい仕組みとなっています。
そのため、一度Railsによる開発を経験すると、Railsで開発を行っている他プロジェクトに移った場合でもDjangoに比べてキャッチアップが早いのではないかと思います。

ディレクトリ構成に強力な制限がある

Railsはmodels/views/controllers/helpers等、プロジェクトを作成した時点で役割の決まったディレクトリが多く作られます。
それぞれのディレクトリに格納されたファイルが何をするかが明確に決められており、それに従って実装を行うのが最も効率的になるような設計になっていると感じました。
Djangoの場合、プロジェクト作成時にファイルは数個しか作られません。
開発プロジェクト毎にどこに何を置くかを決めて実装を行っていくことになります。
したがって、効率的な開発が行えるかどうかは開発プロジェクトの設計に大きく依存することになります。

書くコード量が少ない

これは実際に開発を行ってから強く実感したことですが、他言語に比べてコード量が少なくなります。
他言語と比べて開発効率の面で優位性があると感じました。

Ruby/Railsの戸惑いポイント

次はPython/Djangoと比較して戸惑ったポイントを書いていきます。

とにかく多い記法

Rubyは大量の組み込みライブラリが用意されています。
PythonとRubyで同じ処理を実装した時の一般的な書き方で比較してみます。

まずはPythonで処理を書いてみます。

import random
random_numbers = [random.randint(1, 1000) for _ in range(10)]

# リストの中身を標準出力
[print(x) for x in random_numbers]

# リストの要素を2倍する
double_numbers = [x * 2 for x in random_numbers]

# リストから偶数のみを取り出す
even_numbers = [x for x in random_numbers if x % 2 == 0]

全てfor文を使って書くのが一般的かと思います。
Pythonのコードを書いたことがなくても、何となくやっていることがわかるのではないかと思います。

一方、同じ処理をRubyで書くと以下のようになります。

random_numbers = (1..1000).to_a.sample(10)

# リストの中身を標準出力
random_numbers.each { |num| p num }

# リストの要素を2倍する
double_numbers = random_numbers.map { |num| num * 2 }

# リストから偶数のみを取り出す
even_numbers = random_numbers.filter(&:even?)

each(要素を取り出して処理), map(処理結果の配列を返却), filter(処理結果が真となる配列を返却)と全て異なるメソッドが登場します。
さらに、偶数のみを取り出す式ではfilterに対して&:even?という、Rubyを知らないとなぜこう書くのか想像するのが難しい値が渡されています。
&:についてはQiitaの@kasei-san(かせいさん)様の記事に詳しく書かれているためここでは割愛しますが、今回の場合はrandom_numbersの要素に対してメソッドeven?で偶数かどうかを判定する、となります。
Rubyの場合はどのような処理をしたいかで最適な文法が変わってくるため、Pythonに比べて覚える内容が多くなります。

あなたは変数?メソッド?

Python、Rubyいずれも以下のような書き方が出来ます。

next_number = previous_number + index

それぞれの名前から前回値にインデックスを加算して次に使用する値とすることが想像できますが、Pythonの場合ここに出てくるのは全て変数となります。
Pythonはメソッドを変数に設定することも可能ですが、実行する際は必ず括弧()が必要となります。

def print_a():
    print("a")

var_print_a = print_a
print("b")                 # => b
var_print_a()              # => a

一方、Rubyの場合メソッドの括弧()を省略可能という仕様があり、ここを見ただけでは変数なのかメソッドなのか判断が出来ません。

def print_a()
  p "a"
end

var_print_a = print_a       # => a
p "b"                       # => b
var_print_a                 # ⇒ a ("var_print_a"という変数に入っている、print_aの戻り値"a"が表示される)

そのため、最初に書いた式はprevious_number, indexという変数を加算したのかもしれないし、メソッドの結果を加算した可能性もあります。
実際にそれぞれが変数なのかメソッドなのかは前後の処理を追って確認する必要があります。

returnを書かない

Pythonはメソッドから値を返却する場合、returnの記載が必須となります。
returnが無いメソッドは戻り値無しとなります。
Rubyの場合、メソッド内で最後に行った処理の結果が戻り値となるため、必ずしもreturnを記述する必要はありません。
途中で値を返却したい場合のためにreturnを書くことは可能ですが、何故か省略可能なところでは省略するのが一般的なようです。
一貫してreturnを明記する言語で開発を行ってきた私にとってはちゃんと想定通り動くんだろうかと漠然とした不安を感じる要素ではあります。

DBモデルが見えない

Djangoのmigration機能を利用している場合、パっと見でモデル構成が理解できるようなコードになります。

from django.db import models
from django.db.models import Q


class User(models.Model):
    # カラム定義
    first_name = models.CharField(128)
    last_name = models.CharField(128)
    age = models.IntegerField
    email = models.CharField(256)
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    created_at = models.DateTimeField()
    updated_at = models.DateTimeField()

    class Meta:
        constraints = [
            # ユニーク制約
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="fullname"
            ),
            # チェック制約
            models.CheckConstraint(
                check=Q(age__gte=18),
                name='age_gte_18'
            )
        ]

実際にDBにmigrateが実行されるとテーブルにはidのカラムが作成されますが、それ以外はモデルを定義したファイル(一般的にはmodels.py)で確認が可能です。

一方、Railsでもデフォルトではschema.rbにDBモデルの構成が保存され、こちらも可読性は高いです。
しかしながら、DIGGLEでは以前プロシージャ導入にあたり、ファイルをstructure.sqlに変更しています。

diggle.engineer

structure.sqlはDBインスタンスを作成するために必要な全てのSQLが記載されており、モデルを把握するためにこのファイルを利用するのは現実的ではありません。
トリガ、シーケンス、プロシージャ等DB固有の機能を利用する場合、Railsの標準機能ではモデルの可読性が下がってしまうというのが問題としてあると感じました。

gemの記法で完全に迷子

Pythonは一部の標準ライブラリを除いて、ライブラリを使用する際はimportが必要になります。
また、処理の中で利用する際はimportで宣言した名前を指定する必要があります。 Djangoのプロジェクトを作成した際に自動生成されるファイルurls.pyでさえ、Djangoの機能を利用するためにDjangoのライブラリをimportしています。

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

ワイルドカード指定でimportすることは可能ですが、非推奨となっています。

from django.db.models import *

したがって、推奨された書き方をするとそのファイルで使用されるライブラリは必ずそのファイル内に同じ名前でimportされることになります。
このルールは一見すると開発効率が落ちるように映るかもしれませんが、そのライブラリの存在を知らない開発者が参画した場合に、自ら調査して解析するための足掛かりになります。
また、最近はIDEの進化によりいきなりライブラリを使用した処理を書き始めても、自動で補完してimportを追加してくれるため、importを書かなくても良いフレームワークと比較した際の開発効率の悪さも解消されつつあります。

一方のRailsでは、Bundlerを使えばbundleコマンドでインストールしたgemを勝手に読み込んでくれるという機能があります。
開発の際には煩わしいrequireを定義する必要が無くなるというメリットがありますが、gemを知らない者が処理を読み進めていると突然良く分からない名前のクラスが出現することになります。

my_password = BCrypt::Password.create("my password")

上記の例ではBCryptという固有の名称があるためまだ検索して確認することが可能ですが、factory_botのようにgemの機能を利用するための定義を書くファイルとそれを利用する処理を書くファイルが分割されている場合、処理を書くファイルから読み始めると唐突に一般的な単語1つが出現します。

user = create(:user)

こうなってしまうと、どう調べれば良いのか見当もつかず完全に行き詰ってしまいます。

キャッチアップを早めるための準備

最後に、Rails初心者がキャッチアップを早めるために、こんなものが用意されていると良いと思うものを書いておきます。

Lintの導入

そもそも導入していない開発プロジェクトの方が少ないかとは思いますが、Rails初心者にとっては記法を覚えるための良き指導者にもなると感じています。
DIGGLEではPR発行時のCIでrubocopによるチェックを行っていますが、今のところ私は毎回rubocop先生に怒られています。

モデル図

DBの構成を把握できるかはシステムを理解するスピードに直結すると私は考えています。
ridgepoleannotate等、モデルの可読性が上がるgemを利用する、プロジェクト自体に手を入れたくない場合はtbls等のツールを利用してモデル図の作成/更新が行われていると新規参画メンバーの理解度が上がるのではないかと思います。

主要なgemの使用例

個人的にはここが最も自己解決の難しい部分ではないかと思います。 使用頻度の高いものや処理を追うだけではgem全体の仕様の把握が難しいものについては、使用例等と合わせて一覧化されていると、解析の一助になるのではないかと思います。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers