【最終章】 リファクタリングを通してチームを強化していく
※ この記事は「Railsのリファクタリングに立ち向かうための教科書」シリーズの最終章になります(5/5)
【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
【第1章】 ModelとServiceを紐解く
【第2章】 ApplicationServiceの導入
【第3章】 マイクロサービスを見越した実装
★【最終章】 リファクタリングを通してチームを強化していく
ここまで読み進めていただき、誠にありがとうございます。最終章となる今回は、まとめとして、簡単にですがリファクタリングを通してどうチームが変わっていくかというところを書きたいと思います。
開発速度があがる、レビューの質があがる
ここまで説明してきた責務を考えながら実装すると、単純に設計で迷う回数が減るので、開発速度があがります。また、レビューの質が向上することもぜひ体感していただきたいです。設計がしっかりしていないと、どこをレビューしてよいのかという軸が定まらないのでレビューが非常にしづらいです。ここまで説明してきたことをチームに落とし込めると、設計方針に沿っているか、責務は正しいかというところにフォーカスできるので、レビュワーもレビュイーも質の高い議論がしやすくなります。
オンボーディングも容易に
設計方針が決まっていると、新しく入ってくる方のキャッチアップコストも低くなります。私に業務委託としてリファクタリングを任していただく際は、ここまで説明してきたことに加えて、wikiでrootになる設計方針の説明書きを用意することをよくします。また、勉強会等で全体に対してプレゼンをすることもよくあります。この辺りの開発の心地よさを是非このシリーズで説明してきたことを実行して味わっていただければと思います。
終わりに
いかがでしたでしょうか。実際に実装を進めていくと、細かいところで迷うこともあるかと思います。その際はご自身やチームでDDDの復習などを進めて理解を深めて言っていただければと思います。また、ご縁があれば仕事の依頼をいただいて私も一緒に進めていけると良いかも知れません。
最後まで読んでいただき、ありがとうございました。それでは!
【第3章】 外部サービスとの連携 - マイクロサービスを見越した実装
※ この記事は「Railsのリファクタリングに立ち向かうための教科書」シリーズの第3章になります(4/5)
【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
【第1章】 ModelとServiceを紐解く
【第2章】 ApplicationServiceの導入
★【第3章】 マイクロサービスを見越した実装
【最終章】 リファクタリングを通してチームを強化していく
前回までで、おおよそ価値のあるリファクタリングが行えるだけの知識は習得できたものと思います。この章ではさらにそれを発展させ、ヘキサゴナルアーキテクチャからport/adapterという概念を用いて、マイクロサービスなどの外部サービスとの連携を行う際の実装手法を見ていきたいと思います。
※ ヘキサゴナルアーキテクチャについてはこちらを参照ください blog.tai2.net
重要なのはやはり責務
例えば、キャッシュ機構を実装することを考えてみましょう。アプリケーションからすれば、キャッシュを保持することがドメインの関心であって、キャッシュの保存先が何であるか、どうやってそこに接続する必要があるかは関心外です。なので、アプリケーションからはインターフェースだけを参照すれば十分というような設計にすることで、そうした外部性を疎結合に実装したくなります。この実現をするための設計がport/adapterです。外部サービスとの連携は、すべてport/adapterを通して行うようにします。そうすることによって、例えば保存先をmemcachedからredisに変えようという際に、アプリケーション側では一切コードを変更する必要がなくなります。他にもテスト時だけadapterを付け替えることが可能になるなどのメリットがあります。基本的にadapterは外部との連携なので、接続自体はテストで担保する必要がなく、mock化することがほとんどなので、大きなメリットを享受できます。
では、実際のディレクトリ構造を見てみましょう。例として下記のようなものが考えられます。
port/adapter/ persistence/ cache_store.rb microservices/ config.rb grpc.rb service/ service_A.rb service_B.rb ...
ここでは永続化用のadapterとmicroserviceへの接続用adapterを例として取り上げています。実装は、汎用的な名前(接続先に依存しないような名前。redisではなくcache_storeのような形)を選ぶようにすれば、あとは単純に接続を実装していけば問題ないかと思います。
port/adapterを実装すると、さらにServiceの責務がきれいになり(おそらくport/adapterがなければServiceに実装することになるので)、また、先述したように外部サービスとの接続周りの実装が扱いやすくなるので、非常におすすめです。
いかがでしたでしょうか?これでコアとなるリファクタリングの説明はおしまいです。次章「【最終章】 リファクタリングを通してチームを強化していく」では、まとめとしてこうしたリファクタリングを通して開発チームがどうなっていくか、ということを書きたいと思います。
それでは!
Railsのリファクタリングに立ち向かうための教科書
今回は、Railsのリファクタリング手法を紹介しようと思います。これまで私自身数仕事として数多くのリファクタリングを手伝わせていただいてきましたが、基本的に方針としては毎回ほとんど同じことを実践しています。ですので一度、どう進めればよいのかということを教科書的にまとめておきたく思います。Railsのアプリケーションのコードが汚い、追加開発が辛い、誰もコードを説明できないなどで困っているという方、アーキテクチャについて真剣に考え始めた方の一助になれますと幸いです。
アジェンダとしては、全5回の章立てで下記のように進めていこうと思います。
【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
【第1章】 ModelとServiceを紐解く
【第2章】 ApplicationServiceの導入
【第3章】 マイクロサービスを見越した実装
【最終章】 リファクタリングを通してチームを強化していく
それでは!
【第2章】 ApplicationServiceの導入
※ この記事は「Railsのリファクタリングに立ち向かうための教科書」シリーズの第2章になります(3/5)
【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
【第1章】 ModelとServiceを紐解く
★【第2章】 ApplicationServiceの導入
【第3章】 マイクロサービスを見越した実装
【最終章】 リファクタリングを通してチームを強化していく
今回はApplicationServiceの導入について説明し、さらにリファクタリングを進めていきたいと思います。おそらく、第1章までを進めていくと、ある疑問にぶつかることになるかと思います。それは、「誰がModelとServiceを使ってユースケースを実現するのだろう?」ということです。Controllerにやらせますか?そうすると再びFatControllerに戻ってしまいます。本来Controllerの責務はroutingにより受け取るリクエストに応じてレスポンスを返すことです。そこでユースケースを組み立てて、Modelを作ってServiceを呼んで・・というのをやっているのは責務上良くないです。この問題点を解決するのがApplicationServiceになります。ApplicationServiceの責務はずばり「ドメインのユースケースを組み立てること」。これがうまくいくと、エンジニアじゃない人がコードを読んでも、そのユースケースで何を行うのかということを理解できるようになります。
例えば下記はあるアプリケーションのapplication_servicesです。
├── bank_setup │ ├── activate.rb │ └── sepa_builder.rb ├── contract_classify │ ├── accept.rb │ ├── cancel.rb │ └── decline.rb ├── payment_charge │ ├── payment │ └── transaction └── signup ├── bank_information ├── complete.rb ├── create_consultation_protocol.rb └── personal_information
契約を結ぶ際に銀行口座を登録したりする必要があり、その後サインアップが完了します。上記のディレクトリ構造を見ると、どういうドメインがあって、それぞれのドメインでどういうユースケースがあるのかというのが明白ではないでしょうか。
では、具体的にこの中から1ファイルを選んで見てみましょう。 signup/complete.rb
を見てみます。これは名前の通り、サインアップが完了した際のユースケースを表現します。
module Signup class Complete def self.call(customer) instance = new(customer) instance.call end ... def call create_consultation_protocol notify_success_signup notify_success_signup_internally update_contract_status end private def notify_success_signup Notifier::Email.call( 'signup_success_notification', customer.attributes ) end ... end
一部省略してありますが、インスタンスメソッドのcallを見ればこのユースケースで何を行うかが明確かと思います。プログラミングは自然言語で実装できますので、ユースケースを的確にメソッド化していくことで、非エンジニアがここを読んでも処理を理解することができるようになります。こうすることでコミュニケーションコストはかなり減りますし、メンテナンス性は格段に上がります。
また、privateメソッドでServiceを呼んでいることにも注目してください。publicメソッドから呼ばれるインスタンスメソッド以外は、Serviceを呼んだりModelをinitializeしたり、queryを発行したりして、ユースケースの実現を担うことになります。こうできることでControllerはApplicationServiceを呼ぶだけでいいということがわかりますね。
なので、(何度も見せますが)下記の図のように、ApplicationServiceはControllerが受けるリクエストに応じて、アプリケーションの実装を使ってユースケースを実現するエントリーポイントになるということがよくわかったかと思います。
いかがでしたでしょうか。ここまででRailsのリファクタリングを進める手立てとして、十分な知識を習得できたものと思います。次回「【第3章】 マイクロサービスを見越した実装」では、少し視点が変わるのですが、ヘキサゴナルアーキテクチャの中で出てくるport/adapterという考え方を用いて、マイクロサービスなどの外部サービスとの連携を見据えたリファクタリング方法をご紹介したいと思います。マイクロサービスで開発をしていなくても、Slack通知など、外部サービスとの連携がある場合は有用な考え方なので、ぜひご一読ください。
それでは!
【第1章】 ModelとServiceを紐解く
※ この記事は「Railsのリファクタリングに立ち向かうための教科書」シリーズの第1章になります(2/5)
【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
★【第1章】 ModelとServiceを紐解く
【第2章】 ApplicationServiceの導入
【第3章】 マイクロサービスを見越した実装
【最終章】 リファクタリングを通してチームを強化していく
今回はModelとServiceを紐解いていき、リファクタリングの方針を定めていきたいと思います。
責務を決める
序章でも書いた通り、まず何よりも大事なのがディレクトリの責務を決めることです。責務が決まっていなければ、どんなクラスやモジュールをどこに配置すれば良いのかがわかりません。常にこの「責務」については頭に入れてリファクタリングを進めていきましょう。それではさっそくServiceとModelについて考えていきます。本題に入る前に再度下記の図を復習して、頭に入れておいてください。
Service
序章にも書かせていただきましたが、ModelがFatになってきたからServiceを作ったというケースがRailsプロダクトには非常に多いです。なのでServiceが実際に何をするべき場所なのかがわからないということが起こることになります。私がおすすめするServiceの実装方針は下記の通りです。
1. ステートレスな関数(関数型プログラミングのような形)
2. 命名は動名詞系
1. ステートレスな関数
ステートレスにすることで、Serviceはオブジェクトとして扱われなくなり、ただのFunctionとして振る舞うようになります。これにより、何をやるのかが明確になります。どういうことなのかは実際のコードを見たほうがわかりやすいと思うので、まずはそちらを示します。例として、売上のトレンドを計算するServiceを考えます。
module SalesTrendCalculator module_function def recent_years(sales) year = Time.now.year amount = sales .where(year: [year-1, year-2, year-3, year-4]) .group(:year).sum('amount') calculate_point(amount) end def calculate_point(amount) # Point Logic end end
上記はmodule化することによって、外部からinitializeすることを不可能にしています。こちらは見た目上もわかりやすいのですが、calculate_point
メソッドが外部から見えてしまうことを嫌うこともあるかと思います。その場合は下記のようにクラス化して対応します。
class SalesTrendCalculator def self.recent_years(sales) instance = new(sales) instance.recent_years end def recent_years year = Time.now.year amount = sales .where(year: [year-1, year-2, year-3, year-4]) .group(:year).sum('amount') calculate_point(amount) end private attr_reader :sales def initialize(sales) @sales = sales end def calculate_point(amount) # Point Logic end end
ステートレスにすることを守ることで、責務が1つで何か特定の機能としてのみ動作するレイヤーという位置づけが担保されやすくなります。
2. 命名は動名詞系
これは明確にServiceクラスのものであること、ステートレスであることがわかるようにするための工夫になります。具体的には xxCreator
, xxUploader
, xxBuilder
などの名前を使うことになります。よくある命名でやめておいたほうがよいのが xxService
という名前です。こちらはApplicationServiceを連想しやすいのでおすすめしません。また、可能であれば services
というディレクトリ名も、 domain_services
などにした方が良いと思います。
Model
さて、アプリケーションにおいて最も重要になのがModelです。Modelはアプリケーションをモデリングするオブジェクトの住処なので、特に気をつけて責務を考える必要があります。Railsで実装している場合、Modelは「何をすべきか」ではなく、「何をすべきでないか」という視点で考えたほうがうまくいくことが多いです。具体的にはServiceがやるべきこと、QueryObjectでやるべきことの2つをModelから剥がすとうまくいきやすいことが多いです。特に後者はActiveRecord継承クラスにおいては非常に有用です。QueryObjectについては下記に詳しいです。
7 Patterns to Refactor Fat ActiveRecord Models - Code Climate
この2つを実行すると、ModelにはDDDでいうEntity(簡単のためActiveRecord継承クラスを指す)やValueObjectのみを残すことができるようになるかと思います。
Entity
Entityはアーキテクチャによって指すところが違ったりしてややこしいのですが、DDDにおいては一意に識別できるものとされています。実際はその限りではないのですが、RailsにおいてはActiveRecord継承クラスとしておくと、一旦の理解としては良いかと思います。ここで、序章で話した単一責務の原則を意識したモデルを作ると、Entityとして相応しいものに近しくなっていくかと思います。「そのオブジェクトがそのメソッドないし操作を知っていていいのか?」ということを自問しながら実装を進めると良いでしょう。
ValueObject(値オブジェクト)
ValueObjectは一意性が不要なものをモデリングする際に有用です。例えば電話番号、氏名などはよくこの対象になるのではないかと思います。 ※ サンプルについては検索すればいくらでも見つかると思いますので省略します
Tips
最初はEntityとValueObjectの2つを抑えておいてリファクタリングを進めることをおすすめします。だんだん慣れてきてより複雑なモデリングが必要になったタイミングでaggregateなどを導入するのが良いでしょう。また、 concern
や継承は思い切って使わないという方針を取ってみるのもおすすめします。考えられた上で実装されていれば強力な武器になりますが、大規模なリファクタが必要となるプロダクトではそうではないケースが多く、そうした思い切りも簡潔さのためには非常に有効でしょう。
いかがでしたでしょうか。今回はモデリングのコアになるModelの実装方法と、それを進めるために役立つServiceについても見てきました。次回「【第2章】 ApplicationServiceの導入」では、さらにApplicationServiceの導入について考えたいと思います。ApplicationServiceの導入までできると、かなりReadabilityとメンテナンス性の高いアプリケーションが実装できるものと思います。
それでは!
【序章】Railsのリファクタリングに立ち向かうための教科書
※ この記事は「Railsのリファクタリングに立ち向かうための教科書」シリーズの序章になります(1/5)
★【序章】 問題はどうして起こるのか ~ 方針とアーキテクチャについて
【第1章】 ModelとServiceを紐解く
【第2章】 ApplicationServiceの導入
【第3章】 マイクロサービスを見越した実装
【最終章】 リファクタリングを通してチームを強化していく
問題はどうして起こるのか
私の経験上、多かれ少なかれ違いはあるものの、どのような過程を踏んでRailsのコードがカオスになってくるかには、ある程度傾向が見られます。まず、自分の関わっているプロジェクトの現状がどうなのかを判断するために下記の質問に答えてみてください。
Q. app/以下のディレクトリはそれぞれどういう役割を持っていますか
これに答える自信がなければ、リファクタリングを要する匂いが既にしてきているのかもしれません。ぜひ引き続きこの記事と私のブログを読み進めていってみてください。
さて、ではどのようにしてRailsのコードがカオスになってくるのでしょう?もう何度も見てきたRailsがカオスになるパターンが下記の通りです。
- Fat Controllerになる
- Modelに移す
- Fat Modelになる
- Serviceを作ってServiceに移す
- Serviceがカオスになる
- どうしていいかわからない...
耳が痛いという方もいらっしゃるのではないでしょうか?これは本当によくあるパターンです。こうなってくると、先程の質問に対して「Service」が何をしているのか答えられなくなります。また、Modelに対しても同じようになってしまっているかもしれません。でも安心してください。これからこの問題を解決していきましょう。
方針とアーキテクチャ
では、早速どのようにリファクタリングを進めていくのかという方針を決めていきます。私の提案は下記の3つです。
1. どのレイヤーに何を置き、どう書くべきかを明確にすることをゴールとする
2. 単一責任の原則を厳守する
3. アーキテクチャの方針をよく理解し、取り込む
ひとつずつ見ていきましょう。
1. どのレイヤーに何を置き、どう書くべきかを明確にすることをゴールとする
1は最終的に一番重要になります。これができていると、なにが嬉しいのかというと
- 新しい人が入ってきてもすぐにコード方針を理解出来る
- レビューの視点がクリアになる。何をレビューすればいいかわかる
- 新機能を追加するときもどこに何を書くかで迷うことがなくなる
1は3と重なる部分もあるのですが、ここがゴールだとメンバーみんなが理解することが非常に重要です。
2. 単一責任の原則を厳守する
これはよく聞く話なので当たり前だと思う方も多いのではないでしょうか。ただし、Railsで見過ごされがちなのもこの視点であるのは間違いありません。とくにActiveRecord継承クラスとServiceクラスでこの原則を無視したコードが数多く見られます。ActiveRecordは便利なORMですが、付き合い方を存分に気をつける必要があります。そして、この単一責任の原則を守るだけで、現状のコードでもかなりの部分でReadabilityが向上することに驚かれることになると思います。ちなみに、私がチームにジョインする時は、まずこの原則に基づいたリファクタリングをメンバーにみせ、これを続けていけばかなり良くなっていくのではという期待感を持っていただいてから1と3を進めていくという手順を取ります。
3. アーキテクチャの方針をよく理解し、取り込む
最後になりますが、アーキテクチャを決めます。要はMVCの限界をどう乗り切るかというところです。私がここで用いるのはRailsに合わせたDDD(Domain Driven Design)です。DDDというと難解で尻込みする方が多いのもわかりますが、それほど難しいものではありません。特にRailsの上でDDDの長所を取り込むにはどうすれば良いかという視点で開発するので、話も少し簡単になります。これは後の記事で詳しく解説していきます。まずは全体像がどのような形になるかをここで見ておきましょう。この図はこの後何度か登場することになるかと思います。
いかがでしたでしょうか。少しでも関心をお持ちいただき、次回を楽しみにしていただけると、私としても非常にありがたいです。
次回は「【第1章】 ModelとServiceを紐解く」を書いていこうと思います。
それでは!
aliasを使ったJS開発 & VSCodeでaliasエラーになる時の解決法
下記の環境での話をしますが、特に全てに当てはまらなくても解決できると思います
- VSCode
- webpack
- babel
- typescript
- React
alias(エイリアス)について
簡単にaliasについて説明します。JSを書いてると、importして他のコンポーネントを使うということがよくあると思いますが、その際に相対パスで指定すると、なかなか大変ですよね。特に階層が深くなってくると、いくつ戻っていくつ進むんだ・・・という事になってきます。aliasを使うと「ある場所からのpath」という指定ができるようになるので、そのあたりが簡単になります。
aliasなしの場合の例
import ArticleComponent from '../../../base/articles/ArticleComponent'
aliasあり(例として上記のbaseのところを起点としています)
import ArticleComponent from '@/articles/ArticleComponent'
alias設定方法
まずは webpack.config.js
内に下記のように書きます。
module.exports = { entry: { .. }, ... resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'], modules: [...resolvedPaths, 'node_modules'], alias: { '@': resolve(__dirname, 'src/base/') } }, }
次にtypescript用に tsconfig.json
を編集します。
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/base/*"] },
最後に babel.config.js
も変更しておきます。
[ require('babel-plugin-module-resolver').default, { root: ['./'], extensions: [ '*', '.webpack.js', '.web.js', '.js', '.jsx', '.ts', '.tsx', '.scss', '.json' ], alias: { '@': ['./src/base'] } } ],
これでおおよその場合、エイリアスを使えるようになっていると思います。
それでもVSCodeでエラーになる
compileは通っていて開発も普通にできるけど、VSCodeのエラーが解消されない。。。 Cannot find module '@/xxx'
というエラーが出る、という方は tsconfig.json
のincludeが邪魔していないか確認してみてください。私はこれが原因で数時間ロスしました。
ついでにJestでalias(エイリアス)を使いたい
上記までの作業でaliasが使えるようになったら当然jestなどのテストでも使いたくなってくると思うのですが、そのままでは使えません。
jest.config.js
に、下記のように付け足してaliasの設定をしましょう。
moduleNameMapper: { '^@(.*)$': '<rootDir>/src/base/$1' }
aliasを使うとだいぶ効率上がりますね。