
シェア:
ベンはセカンドキャリアの開発者で、以前は成人教育、コミュニティ組織化、非営利団体運営の分野で10年を過ごした。彼はVonageの開発者支援者として働いていた。コミュニティ開発とテクノロジーの交差点について定期的に執筆している。南カリフォルニア出身で、長年ニューヨークに住んでいたが、現在はイスラエルのテルアビブ近郊に在住。
Rubyに静的型チェックを取り入れることによる洞察
Nexmoは、多様なAPIを提供する開発者コミュニティをサポートするために、様々な言語でSDKを提供しています。開発者がカスタムビルドしたHTTPコールを通じて、各REST APIと直接やりとりすることは十分に可能です。しかし、SDKを活用することで、開発者はより早く、より少ないオーバーヘッドで目標を達成することができます。
そのため、各SDKを作成し、反復する作業は、チームとして非常に真剣に取り組んでいます。小さな趣味のプロジェクトから多国籍のビジネスインフラまで、世界中のユーザーから毎月何千万ものAPIコールがSDKを通じて行われています。
その結果、各SDKで開発者のエクスペリエンスを向上させる方法を継続的に模索しています。各SDKは サーバー・ライブラリ仕様各SDKは、各言語固有の制約を考慮しながら、サーバー・ライブラリ仕様で規定された目標を達成するよう努めています。
一般原則 一般原則仕様書に概説されている一般原則のひとつに、次のようなものがある:
私たちの図書館は明確であるべきだ。
これは、理想的にはすべてのクラス、メソッド、定数などが定義され、その値とパラメータがSDKに依存する開発者に知られていなければならないことを意味する。明示的なコードはコードを組み込みやすく、ひいてはデバッグしやすく、避けられない問題が発生したときに解決しやすいコードにつながります。
その目標に向かって Ruby SDKを使用してコードベースに静的型チェックを組み込み始めました。 Sorbet型チェッカーgemを使用しています。そのため v6.3.0リリースSDKのv6.3.0リリースには、gemのインストールと初期化、SMSクラスのメソッドシグネチャが含まれています。
その過程で、勉強になる瞬間はありましたか?常に伝統的に動的型付けされてきた言語で、動的型付けされたコードベースをリファクタリングすることで、共有する価値のある観察がいくつか得られた。v6.3.0リリースのための作業中に、私たちは以下の2つの宝石を発掘した:
インターフェイスを通して考える
Ruby SDK は privateと protectedキーワードを利用して、ライブラリのアーキテクチャの異なるコンポーネントを区別しています。前述の2つのキーワードで定義されていないコードは、パブリック・インターフェースの一部です。
SDKのユーザーとして、それは現実的に何を意味するのでしょうか?いくつかの理由から、これらの違いを明確にすることは重要です。
つまり publicリファクタリングは、ユーザーへの適切な通知やセマンティックなバージョン変更なしに、そのコードに破壊的な変更をもたらすべきではありません。SDKの publicSDKのインターフェイス内にレイアウトされたクラスとメソッドは、ユースケースで作業を行うために直接依存するメカニズムです。これは、メソッド呼び出しの中で名前を付けて呼び出すことによって、直接対話するコードです。 client.sms.send.
の使い方を検証してみよう。 privateと protectedキーワードの使い方を検討するとき、どのような場合にどちらを使うべきかを理解しなければならない。
古典的には、Rubyistたちはこのような区別に頭を悩ませることはあまりありませんでした。実際、多くのRubyistにとって、これらの使い分けは「良い習慣」であり、Javaのような他の言語のように「やらなければならないこと」ではなかった。結局のところ #sendメソッドを使うことで、開発者はインターフェイスの定義を回避し、そこで定義されたメソッドに直接アクセスすることができる。しかし、Rubyに静的型付けを統合し始めると、これらのインターフェイス定義はより重要になり、その適用においてより正確さが要求されるようになる。
Rubyでは privateと protectedの違いは、メソッドが定義されたクラスのスコープ外でアクセスできるかどうかです。キーワードを使った例を見てみよう。 privateキーワードを使った例を見てみましょう:
class MyExample
def public_method
puts "This is public"
end
private
def private_method
puts "This is private"
end
end上記の例では #private_methodを呼び出すことができます。 MyExampleを継承した別のクラスがあった場合、そのクラスではこのメソッドを利用できない。 MyExampleを継承した別のクラスがあった場合、そのクラスはそのメソッドを使えない。例えば、次のように定義されたクラスがあったとする:
class MySecondExample < MyExample
end
プライベート・メソッド MyExample.private_methodクラスのスコープからは MySecondExampleクラスのスコープからはアクセスできない。これは、2番目のクラスがそのクラスのサブクラスであっても同じです。 MyExampleクラスのサブクラスであっても同様です。
一方 protectedキーワードで定義されたメソッドには、親クラスを継承したサブクラスからアクセスできます。したがって privateキーワードを protectedクラス・スコープでアクセスできるようになります。 MySecondExampleクラスのスコープでアクセスできるようになります。
メソッドが protectedまたは privateスコープ内であろうとなかろうと、SDKを使う開発者へのメッセージは、これらのメソッドは外部にあまり通知することなく変更される可能性があるということです。これらのメソッドへのいかなる変更も、アプリケーションの一般的な動作に影響を及ぼすべきではない。もしそうであれば、このメソッドが本当に非公開インターフェイスに属するものなのかどうか、疑問が生じます。
Sorbet gemを通じて静的型チェックを統合し始めたとき、最初に遭遇した問題のひとつは、型チェッカーがメソッドに到達できないというエラーを報告することだった。
例えば SMSクラスは、SDKの他の多くのクラスと同様に、APIにリクエストを送信するために Nexmo::Namespace#requestメソッドを利用してAPIにリクエストを送信します。Rubyのインターフェイス定義の厳密さには本質的な柔軟性があるため、このメソッドが privateキーワードで定義され、サブクラスで使用されているという事実は、このメソッドが設計されたとおりに実際に実行されることを妨げるものではありませんでした。しかし、このメソッドは暗黙のうちにサブクラスで使用されていたため、インターフェース設計の最良の慣例では、このメソッドは protectedキーワードの内部で定義されるべきである。そのため、インターフェイスを protectedに再定義する前に、型チェッカーは次のようなエラーを報告した:
lib/nexmo/sms.rb:109: Method request does not exist on Nexmo::SMS https://srb.help/7003ソルベの便利な機能のひとつは、各エラーにそのエラーコードのドキュメントを参照するURLが付加されていることだ。この場合、エラー7003のドキュメントにはこう書かれている: This error indicates a call to a method we believe does not exist (a la Ruby’s NoMethodError exception).ドキュメントには、エラーについての詳細な説明とともに、問題のコード例とその対処方法が記載されています。私たちの場合、Sorbetがこのエラーを投げる2つ目の理由が私たちのコードに当てはまると思います:
実行時にメソッドが存在していても、Sorbetはエラーを報告するかもしれない。
インターフェイスの内部で定義されたメソッドは privateインターフェイスの内部で定義されたメソッドは、そのメソッドが定義されたクラスのスコープ外からは見えない。Rubyの柔軟性を利用してメソッドを呼び出すことは可能かもしれないが、それでも本質的な不可視性は改善されない。そのため、Sorbetは、呼び出された場所でコードが見えるようにすることにこだわっている。これにより、明示的なコードベースが保証される。
それぞれのメソッドを最後まで貫く
ソルベをコードベースに導入する過程で発見した2つ目の宝石は、コード内で呼び出され使用される各メソッドの意味を深く考え抜いたことだ。
しばしば、アプリケーションがうまく設計されていても、いくつかの項目が私たちの焦点から外れてし まうことがあります。アプリケーションの成功経路と失敗経路の両方をテストすることは、真剣に試みられます。コードは、発生する可能性のある一般的なエッジケースのほとんどを扱えるように構築されていますが、それにも かかわらず、意図しない結果が発生する可能性があります。
この問題が表面化したのは、パラメーター・ハッシュからオブジェクトを取り出す際のデフォルトの戻り値だ。このコードでは #unicode?これは、オブジェクトの値がUnicode形式かどうかをチェックする小さなメソッドである:
if unicode?(params[:text]) && params[:type] != 'unicode'
...
private
def unicode?(text)
!GSM7.encoded?(text)
end
この #unicode?メソッドはパラメーターの値に応じてブール値を返す。しかし :textオブジェクトがない場合はどうなるでしょうか?その可能性は、実装の段階では信じられないほど低いのですが、それでも、コードの観点からは、それは #unicode?メソッドの返り値に影響します。
に何も値を入れずにこのアクションをシミュレートしてみよう。 params[:text]に値を入れずにそのアクションをシミュレートしてみよう:
params[:text]
=> nil
Rubyは nilを返す。従って、このメソッドが型チェックされたときにSorbetがエラーを返した理由もこれで説明できる。この #unicode?メソッドのシグネチャには
sig { params(text: String).returns(T::Boolean) }上記のシグネチャは、メソッドが String型の入力を受け入れ Boolean型の値を返すことを宣言しています。しかし、パラメータが nilの場合、入力は nilとなり String.
この時点で、いくつかの選択肢がある。ひとつは、メソッドのシグネチャを書き換えて、メソッドの入力として Nilableパラメーターをメソッドの入力として使えるようにする。そうすれば技術的な問題はなくなる。しかし、ソルベが明らかにした根本的なアーキテクチャの問題は解消されない。
このコードでは、入力が nilという状況を望んでいない。パラメータが nilである場合、何かが間違っている。その場合、コードは通常通り機能し続けるのではなく、実際にユーザーにエラーを発生させる必要があります。このエラーは、SDKを使用して開発する人たちが、コード内のバグをより早く、反復プロセスの早い段階で発見、診断、治療するのに役立つため、SDKの使用経験を向上させます。
ここでの目標は、症状ではなく、アーキテクチャの根本的な問題に対処することなので、解決策は、値が提供されないときに nilを返さないメソッドを利用することです。パラメータデータへの呼び出しは、次のようにリファクタリングされる:
params.fetch(:text)で説明した #fetchメソッドは メソッドはメソッドは KeyError例外が発生します:
KeyError (key not found: :text)その例外がユーザーに返されると、有益な情報となり、開発の初期段階でコードを改善する指針になる。
次のステップ
静的型チェックをRuby SDKに取り入れるプロセスに着手する前に、私たちはそのメリットとデメリットについて多くの議論を交わしました。SDKの開発に具体的なメリットがあるのかどうか、そしてそれが何なのかを知ることです。現時点では、その未知の質問に対する解答は、明確に肯定的なイエスです。
私たちのRubyコードベースに静的型付けを導入することで、SDKの開発者として、あらゆる設計上の選択、メソッドの利用などの意味について深く考えることに集中できるようになりました。私たちのチームでは、すべてのプルリクエストの徹底したレビュープロセスを維持しています。自動化された統合テストの実行を活用し、成功ルートと失敗ルートをカバーするテストを構築しています。静的型付けの追加は、コードの品質と開発者のポジティブな経験を保証する新しいレイヤーです。
Rubyや他の動的型付け言語における静的型付けは、コードの書き方にパラダイムシフトをもたらす。以前はもっと柔軟性があったところに、標準化が強制されるのだ。この点については、Rubyのコミュニティでも議論の的となっている。どのようなアプローチが望ましいのか。おそらくその論争の答えは、両極端の中間にあるということだろう。ある程度の柔軟性はRubyのマジックを維持する一方で、標準化と慣習を増やすことで、バグやこれまで発見されなかったエッジケースがプロセスのずっと後になって発見される可能性を減らすことができます。
Nexmo Ruby SDKに関しては、今後数ヶ月の間にコードベースに型を徐々に実装していく予定です。目標は100%型付けされたコードベースを達成することであり、それを段階的に進めていくことです。
Nexmo Rubyはオープンソースです!参加したい方は GitHub