Uzabase Tech Blog

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

Smalltalkで『オブジェクト指向設計実践ガイド』の「第3章 依存関係を管理する」をハンズオンしたら快適で楽しかった

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

前回の続きです。趣旨も同じ。

『オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方』のサンプルコードを
Ruby から Smalltalk に翻訳しながら読み進めることで、ただの写経をアクティブな学びにし、 いろいろな道草、発見をしながら楽しもう、というものです。

前回も触れましたが、やはり自分のコードとクラスライブラリの境界が無く、よいお手本がすぐに手に入るのがよいです。
わざわざドキュメントを紐解いたり、ググる必要がほぼないのですね。
Smalltalk 環境で完結します。
シンプルです。

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

今回も、わりと忠実な写経が可能でした

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

Ruby と Smalltalk がすごく似ていることは重々わかりました。
しかし、そこはアクティブ写経なので、気付きや考えたことがありました。
それを書いていきます。

ダック・タイピング良好

「 3.2 疎結合なコードを書く」では、本来的に避けられないオブジェクト間の共同作業の中で、依存関係をいかに管理するか、蜜結合なオブジェクトの巨大団子を作らないための設計テクニックがいくつか紹介されています。

まずは、「依存オブジェクトの注入」です。
コンストラクタで、 Wheel オブジェクトを注入しています。

Class {
    #name : #Gear,
    #superclass : #Object,
    #instVars : [
        'chainring',
        'cog',
        'wheel'
    ],
    #category : #'Example-OOD-gear'
}

{ #category : #private }
Gear >> setChainring: chainringInteger cog: cogInteger rim: rimInteger tire: tireFloat [
    chainring := chainringInteger.
    cog := cogInteger.
    rim := rimInteger.
    tire := tireFloat.
    ^ self
]

{ #category : #calculating }
Gear >> gearInches [
    ^ self ratio * wheel diameter
]
Class {
    #name : #Wheel,
    #superclass : #Object,
    #instVars : [
        'rim',
        'tire'
    ],
    #category : #'Example-OOD-gear'
}

{ #category : #calculating }
Wheel >> diameter [
    ^ rim + (tire * 2)
]

Ruby によるサンプルコードと同様、ダック・タイピングが使えるのでとても簡単でした。
「 diameter というメッセージに答えることが出来る、ある抽象的なもの」への依存を注入しています。

静的型付け言語の場合だと、「抽象的なもの」はインタフェースで表現されると思います。
陽に、インタフェースを定義するには名付けが必要で、そこに難しさがあると思います。
○○Able ? IWheel ? なかなかよい名前が浮かびません。
「 diameter というメッセージに答えうるもの」という概念が抽象的すぎるのでしょうか。
陰に、インタフェースがかたちづくられるダック・タイピングが、名付け問題を先送りに出来ているという意味で、ここでは有効だと思いました。
(インスタンス変数を wheel と名付けしていますが、クラス名、インタフェース名ほど目立つものではありません。)

その代わり、コンパイラが事前に実行時エラー検出してくれるとうメリットを捨てているわけですが、
以下のような心意気でテストを書いていればよいのでは、と考えています。

むしろソフトウェア開発は科学のようなものである。どれだけ最善を尽くしても正しくないことを証明できないことによって、その正しさを明らかにしているのである。
Clean Architecture 達人に学ぶソフトウェアの構造と設計 > 第4章 構造化プログラミング > テスト

上記、ダック・タイピングのWikiページの記述にもあるように、 OCaml などの言語は型推論により上記のいいとこ取りが出来るようなので、近いうちに使ってみたいなと思いました。

その他、依存を管理するテクニック

依存を隔離する」では、Wheel の初期化のためのメソッドを用意して依存をむしろ明示して局所化しています。
インスタンス変数の遅延初期化の書き方が以下のように異なりました。

# Ruby
def gear_inches
  ratio * wheel.diameter
end

def wheel
  @wheel ||= Wheel.new(rim, tire)
end
"Smalltalk"
gearInches [
    ^ self ratio * wheel diameter
]

wheel [
    wheel ifNil: [ wheel := Wheel rim: rim tire: tire ].
    ^ wheel
]

Ruby のほうがシンタックス・シュガー(||=)がある分、コード量は少ないですね。
Smalltalk にはシンタックス・シュガーが無いですが、この側面から見ると Ruby は Easy 、Smalltalk は Simple と言えると考えています。

次に、「引数の順番への依存を取り除く」です。
ここでは、その目的のためにハッシュを引数に渡すこと(デフォルト値にも対応)をしています。
同じことを Smalltalk で書くとこうなります。

setArgs: args [
    chainring := args at: #chainring ifAbsent: 40.
    cog := args at: #cog ifAbsent: 18.
    wheel := args at: #wheel.
    ^ self
]

または、デフォルト値を持つオブジェクトとのマージを使って書くと以下のようになります。

setArgs: args [
    defaults
        ifNil: [ defaults := Dictionary new.
            defaults at: #chainring put: 40.
            defaults at: #cog put: 18 ].
    defaults addAll: args.
    chainring := args at: #chainring.
    cog := args at: #cog.
    wheel := args at: #wheel.
    ^ self
]

Smalltalk ではハッシュ( Dictionary )同士のマージを addAll というメッセージで行います。
Ruby のサンプルコードでは merge というメソッド名になっていますが、
個人的には addAll という名前のほうが、キーが一致した場合に値が後勝ちすることがわかりやすい気がしていて好みです。

「自身より変更されないものに依存しなさい」というマントラ

「依存方向の管理」では、まず Gear と Wheel の依存関係を敢えて逆転させて、依存関係の方向は恣意的に選ぶものだと示します。

OOとは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。
Clean Architecture 達人に学ぶソフトウェアの構造と設計 > 第5章 オブジェクト指向プログラミング > まとめ

Ruby も Smalltalk も OO なので、依存関係を自由に制御できるわけですが、
選択基準として、「自身より変更されないものに依存しなさい」が示されます。
つまり、具象より抽象に依存すべきと言い換えられると思いますが、 さきほど行った Gear が Wheel を注入するようにしたのは、 依存方向は変わっていませんが、「 Wheel 」という具象より、「 diameter というメッセージに答えるもの」という抽象に依存するようにしており、結果的に依存の方向を強調したことになったと思っています。

まとめ

前回より、Ruby と Smalltalk の違いが際立たなかったので、すなおにハンズオンを進めていけました。(TDD遵守)
さらに加速して、第9章までやりきりたいと思います。