Uzabase Tech Blog

SPEEDA, NewsPicks, FORCASなどを開発するユーザベースの技術チームブログです。

Smalltalkで『オブジェクト指向設計実践ガイド』の「第2章 単一責任のクラスを設計する」をハンズオンしたら快適で楽しかった

今日は。 SPEEDA を開発している濱口です。

前回はテスト駆動開発(TDD)を習得するためのハンズオンを行いましたが、今回はオブジェクト指向設計(OOD)です。

SPEEDA は複雑なビジネスロジックから成っており、開発では複雑で多くのコミュニケーションが発生します。
開発チームでは gauge などのツールを使い e2e テストが仕様を語るようにしたり、ペアプロを徹底したり、すべてが流れるようにしたり...等々の様々な工夫を行っています。
コードや設計もコミュニケーションのひとつだと捉えると、実際に開発で発生するコニュニケーションの比重の中では大きな割合を占めるものだということを感じています。
SPEEDA 開発者は優れた OOD をすることが必要となっていると思います。

有意義なハンズオンを、より有意義にしたい

今回も前回と同様、 Smalltalk でやります。
環境も Pharo (イメージのテンプレートは Pharo 7.0 )です。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』は、副題にもある通りコードは Ruby で書かれていますが、プログラミング言語翻訳プロセスを介することで、ハンズオンがアクティブ・ラーニング化することを狙っています。

古くてあたらしい言語(環境)、 Smalltalk にダイブしたい

また、 Smalltalk では自分のコードと既存のコードは全く区別無く閲覧、編集が可能であるため、よいお手本の宝庫としてのクラスライブラリが本当に身近にあります。
シンプルで完成度の高い直感的な IDE により、クラスライブラリを快適に回遊しながら、その中でアプリケーションをつくることができます。
探索的プログラミングを助けるツールとしては、 System Browser 、 Finder 、 Playground をよく使いました。
また、コードは Iceberg でバージョン管理します。(プロジェクト作成時、リポジトリ一覧で「No Project Found」というステータスに出くわした場合、『Manage Your Code with Iceberg』の「1.4 Add a new project to Iceberg」以降の手順で解決できました。)

f:id:yhamaro:20191209054645p:plain
Let's 写経!

Smalltalkでは、やはりわりと忠実な写経が可能でした

わりと忠実な写経は以下です。(プログラムの進化の過程が見えるようにコミットを分けました)

「やはり」と書いたのは、言語として近いことはなんとなく認識していて、書籍の表現でもメソッドの実行を「メッセージを送る」と表現していたためです。
ただ、もちろん違いはありました。 途中、特に気になった差異をいくつか挙げていきます。

全てのインスタンス変数を隠蔽せよ、ということ

本書の「2.3 変更を歓迎するコードを書く」の"データではなく、振る舞いに依存する"という部分の結論ではそうなっています。

変数はそれらを定義しているクラスからでさえも隠蔽しましょう。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』

この点において、Smalltalk開発者では意見が分かれているようです。

Smalltalk開発者の中には、アクセサを通してのみインスタンス変数にアクセスすることを好む人もいます。このプラクティスは価値あるものですが、クラスのインターフェースを乱雑にする側面があり、下手をすると外部の世界にプライベートな状態を晒すことにもなります。
Pharo by Example(日本語版)

『ケント・ベックのSmalltalkベストプラクティス・パターン』(SBPP)でも以下の相反するパターンが取り上げられています。

  • Direct Variable Access パターン
  • Indirect Variable Access パターン

一方が読みやすさを、一方が柔軟性を実現するとしています。
この問題が一筋縄ではいかないことを示すエピソードも添えられています。

初めて Smalltalk を始めた時(テクトロニクス社にいた80年代半ばのことですが)、状態のアクセスについて熱い議論を戦わせていました。両サイドに派閥ができ、自分勝手な主張を繰り広げたものです(研究所はあまり好きではないですか?)。

たいていは Indirect Variable Access を支持するグループが、 Smalltalk コミュニティの心をつかんでいました。なぜなら、大手のトレーニング会社が間接アクセスを教えていたからだと思います。この問題は「直接アクセスは悪で、間接アクセスは善である」といった具合に、非常に単純化されてしまっているきらいがあります。

...(中略)...
私は間接アクセスを主張するクライアントと仕事をしました。プロのプログラマはどんなスタイルでも思いのままにかけるものです。私は、良き一兵卒となって Getting Method と Setting Method をひたすら書きました。

そのクライアントとの仕事が終わったあと、家でちょっとしたコードを書いてみました。前の仕事と比べてそのコードがなんと読みやすかったことでしょう。スタイルの違いはまさに、直接アクセスか間接アクセスかということにあったのです。

『ケント・ベックのSmalltalkベストプラクティス・パターン』

ただ、ひとつの基準も示されています。

もし、継承のニーズがある場合には、 Indirect Variable Access を用いるべきでしょう。
次に使う人はきっと感謝します。

『ケント・ベックのSmalltalkベストプラクティス・パターン』

結論として、今やっているのはハンズオンなので読みやすさを重視すべきですし、未だ継承の概念が出てきていないため、一旦 Direct Variable Access パターンを採用しています。

Smalltalk には Ruby の Struct クラスにあたるものがない

Gear クラスをつくっている最中に Wheel の概念が必要になるくだりがあります。
そこでの想定は以下となっています。

もし Wheel クラスを別につくれる状況にあるならば、おそらくつくるべきでしょう。
しかしいまは、一時的でなくずっと使い続けるような、アプリケーション全体で利用可能な新しいクラスはつくらないと選択したとしましょう。
何らかの制約が課されているのかもしれませんし、もしくは、どこに向かっているのかまったくわからなく、考えが変わる恐れがあるので、 だれかが依存をつくってしまいかねない新しいクラスはつくりたくない、といった状況が考えられます。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』

Ruby では上記の想定のなかで、 Struct クラスを用います。

A Struct is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class.
(明示的にクラスを書くことなく、いくつもの属性を1カ所に束ねるための便利な方法。アクセサメソッドが用いられる。)
Rubyの公式ドキュメント

これは上記の想定に完璧に合った用途のもので、さらに、

Wheel を Gear 内に埋め込むことで、 Wheel は Gear のコンテキストにおいてのみ存在すると設計者が想定していることがわかります。
『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』

というように設計意図を伝えることもします。
私は上記の想定のなかで同様の構造で設計意図を伝えられる方法を探しましたが、結論から言えば見つけることが出来ませんでした。
私の試した方法は以下です。

Dictionary を使う

これが一番、 Struct での実現方法に近いと思います。

{ #category : #calculating }
RevealingReferences >> diameters [
    ^ wheels
        collect: [ :wheel | (wheel at: #rim) + ((wheel at: #tire) * 2) ]
]

{ #category : #private }
RevealingReferences >> setData: aData [
    wheels := aData
        collect: [ :cell | 
            {(#rim -> (cell at: 1)).
            (#tire -> (cell at: 2))} asDictionary ]
]

Struct の機能である「明示的にクラスを書くことなく、いくつもの属性を1カ所に束ねる」ことを実現しています。 一方、「アクセサメソッドが用いられる」ことに関しては diameters メソッドの中でアクセサでダイレクトにではなく、キーアクセスになってしてしまっているので微妙です。(ブロッククロージャを用いたやり方もできそうですが、これと同じ問題に遭遇すると考え試していません。)

Wheel クラスをつくっちゃう

元も子もないですが。。

{ #category : #calculating }
RevealingReferences >> diameters [
    ^ wheels collect: [ :wheel | wheel diameter ]
]

{ #category : #private }
RevealingReferences >> setData: aData [
    wheels := aData
        collect: [ :cell | Wheel rim: (cell at: 1) tire: (cell at: 2) ]
]

しかし、 diameters メソッドが書籍のコードと同じかたちになるので、ハンズオンでは一旦この方法を取っています。

まとめ

前回同様、違いはありましたが、このハンズオンが意図する本筋から外れるものはなく、ステップバイステップで進めていけました。(ちなみにちゃんと TDD でやっています。)
個人的には Pharo が手に馴染んで、クラスライブラリに関する知識も少しずつ増えてきた感触があります。 この調子で第9章までやりきりたいと思います。