BETA

「どのクラスでnewするか」をダックタイプする

投稿日:2020-04-15
最終更新:2020-04-15

※この記事はQiitaから移植した物です。

#0.前置き

##0-1.タイトルが意味不明なので説明すると
オブジェクト指向を勉強すると、「ダックタイピング」という概念に出会うと思います。
dogクラスでpochiインスタンスを作ってpochi.barkってやるとワンワン鳴いて、
catクラスでtamaインスタンスを作ってtama.barkってやるとニャーニャー鳴くあれですね。
(catはbarkしないってツッコミはノーマナー。)

しかしこれ、「クラスを指定すると、自動でメソッドを選んでくれる」という仕組み。
クラスは自動で選んでくれないので、pochiインスタンスをcatクラスで作るとポチがニャーニャー鳴いてしまいます。
仮にもエンジニアなら誰もそんなミスはしないでしょう。しかし、ユーザーだったら?

「pochi」「koro」と入力したらワンワン鳴かせ、「tama」「tora」と入力したらニャーニャー鳴かせ、
「mouse」「rat」と入力したら無視したい。
でもヒューマンエラーの原因になるので、ユーザーにはクラスの指定をさせたくない。
つまり「引数を指定すると、自動でクラスを選んでくれる」という変則ダックタイピングが目標です。

ダックタイピングじゃない正式名を知ってる人がいたらコメントください。こんな意味不明タイトルはワシも嫌じゃ。

##0-2.やりたいこと
ワンニャー方式で説明してきましたが、ここからは国と元首で説明します。

class NationSuper  
# 前回はLandだったがNationに変更  
end  

class RepublicSub < NationSuper  
    NATION_AND_HEAD = { usa: "Trump",  
                        russia: "Putin",  
                        france: "Macron",  
    }.freeze  
end  

class MonarchySub < NationSuper  
    NATION_AND_HEAD = { uk: "George",  
                        japan: "Naruhito",  
    }.freeze  
    # イギリスは今は女王なので、先代のジョージ六世。  
    # 中国は大統領でも君主でもないので、代わりに日本。  
end  

def head_of(nation)  
end  

# head_of("usa")=>president Trump  
# head_of("japan")=>king Naruhito  
# head_of("val_verde")=>nil  
# バルベルデはハリウッド映画に出てくる架空の国  

##0-3.縛り

###0-3-1.if禁止
当たり前ですがダックタイピングなので、ifとかunless系は全部禁止です。

###0-3-2.政体名、国名を書いて良いのは一回だけ
2回以上書くと、
「君主制と共和制だけじゃなくて、貴族制のサブクラスを追加したい」
「スウェーデンのカール16世グスタフを追加したい」
ってなった時に2ヶ所以上修正する羽目になりますからね。

##0-4.前提知識
ちょっと珍しい前提知識一覧、
ググればわかりやすい記事が出てくる情報ばかりなので、詳細は割愛します。

###0-4-1.サブクラス一覧を配列にして取得できる
https://halmat.qrunch.io/entries/RuVSkk15T3JsFZHf
前回記事です。今回はコメントのやり方を採用します。
記事では「クラス変数」「再代入」と、素人がやっちゃいけない事を二重にやってますからね。

###0-4-2.スーパークラスのメソッドは、サブクラスの定数を参照できる
https://easyramble.com/access-subclass-constant-from-superclass.html
http://takitake.hatenablog.com/entry/2014/10/15/215007
selfを使うと、サブクラスの定数を参照します。
クラスを省略するとスーパークラスの定数を見てしまうらしいので注意。

###0-4-3.変数はクラスオブジェクトを格納できる
「rubyでは全てがオブジェクト」というのはよく言われますね。
整数はIntegerクラスのインスタンスオブジェクト、文字列はStringクラスのインスタンスオブジェクト。
そしてクラスはクラスオブジェクト。
整数や文字列を変数に代入できるのと同様、クラスも変数に代入できます。

buf = Array  
p buf.new(5) # =>[nil, nil, nil, nil, nil]  

今回は使いませんが、実は文字列を使ってインスタンスを作ることもできます。
http://hiroyukim.hatenablog.jp/entry/2013/05/02/171412

###0-4-4.「レシーバーがあるなら実行、ないならnil」の分岐は一文字で実装できる
https://qiita.com/takaram/items/02dc68bf370cc3d8babb
Safe Navigation Operatorと言うらしいです。
nilではなく別の処理をしたい時は、respond_to?でif分岐すると良いかも。
https://ref.xaio.jp/ruby/classes/object/respond_to

#1.サブクラス一覧を配列にして取得
前提知識0-4-1を使います。

class NationSuper  
end  

class RepublicSub < NationSuper  
    NATION_AND_HEAD = { usa: "Trump",  
                        russia: "Putin",  
                        france: "Macron",  
    }.freeze  
end  

class MonarchySub < NationSuper  
    NATION_AND_HEAD = { uk: "George",  
                        japan: "Naruhito",  
    }.freeze  
end  

def head_of(nation)  
end  

polities = ObjectSpace.each_object(Class).select { |c| c.superclass == NationSuper }  
p polities # =>[MonarchySub, RepublicSub]  

#2.スーパークラスからサブクラスの定数を確認するメソッドを実装

class NationSuper  
    def self.is_polity_of(arg_string)  
        self::NATION_AND_HEAD.has_key?(arg_string.to_sym)  
    end  
end  

class RepublicSub < NationSuper  
    NATION_AND_HEAD = { usa: "Trump",  
                        russia: "Putin",  
                        france: "Macron",  
    }.freeze  
end  

class MonarchySub < NationSuper  
    NATION_AND_HEAD = { uk: "George",  
                        japan: "Naruhito",  
    }.freeze  
end  

def head_of(nation)  
end  

polities = ObjectSpace.each_object(Class).select { |c| c.superclass == NationSuper }  

p MonarchySub.is_polity_of("uk") # => true  
p RepublicSub.is_polity_of("uk") # => false  

前提知識0-4-2。MonarchySub.polity_of("uk")を説明すると、
1.「MonarchySub」に「self.polity_of」って無いな。
2.スーパークラス辿ってみたら「NationSuper」にあったからこれ使うか。
3.「self::NATION_AND_HEAD」? これはMonarchySubのNATION_AND_HEADの事ね。
4.arg_string.to_symっていうのは引数をシンボルにすると。
5.よし、MonarchySubのNATION_AND_HEADのキーに:ukがあるか確認。あったからtrue

#3.1で取得した配列を2で作ったメソッドで検索
皆さんご存知findメソッド。
https://ref.xaio.jp/ruby/classes/enumerable/find

class NationSuper  
    def self.is_polity_of(arg_string)  
        self::NATION_AND_HEAD.has_key?(arg_string.to_sym)  
    end  
end  

class RepublicSub < NationSuper  
    NATION_AND_HEAD = { usa: "Trump",  
                        russia: "Putin",  
                        france: "Macron",  
    }.freeze  
end  

class MonarchySub < NationSuper  
    NATION_AND_HEAD = { uk: "George",  
                        japan: "Naruhito",  
    }.freeze  
end  

def head_of(nation)  
end  

polities = ObjectSpace.each_object(Class).select { |c| c.superclass == NationSuper }  
polity1 = polities.find { |c| c.is_polity_of("france") }  
p polity1 # => RepublicSub  
polity2 = polities.find { |c| c.is_polity_of("val_verde") }  
p polity2 # => nil  

前提知識0-4-3が使われ、polityにRepublicSubクラスオブジェクトが代入されています。
全要素をブロック処理してもtrueが見つからない場合、nilが返り値になります。バルベルデはnilが返ってますね。

#4.3で取得したクラスで新しいインスタンスを作成

class NationSuper  
    def initialize(arg_name)  
        # 正直作り忘れてた  
        @nation_name = arg_name  
    end  

    def self.is_polity_of(arg_string)  
        self::NATION_AND_HEAD.has_key?(arg_string.to_sym)  
    end  
end  

class RepublicSub < NationSuper  
    NATION_AND_HEAD = { usa: "Trump",  
                        russia: "Putin",  
                        france: "Macron",  
    }.freeze  
end  

class MonarchySub < NationSuper  
    NATION_AND_HEAD = { uk: "George",  
                        japan: "Naruhito",  
    }.freeze  
end  

def head_of(nation)  
end  

def tmp(nation)  
    # テスト用に一時的に作ったメソッド。最終的にhead_ofに移植する  
    polities = ObjectSpace.each_object(Class).select { |c| c.superclass == NationSuper }  
    polity = polities.find { |c| c.is_polity_of(nation) }  
    polity&.new(nation)  
end  

p tmp("russia").object_id # => 40933700  
p tmp("val_verde").nil? # => true  

newする時に&が入っているのは前提知識0-4-4。
これにより「polityにRepublicSubが入っていてnewできるから、tmp("russia")はインスタンス作成」
「polityがnilでnewできないから、tmp("val_verde")はnil」となります。
if禁止縛りをこんな形ですり抜けちゃっていいのかなあ。

#5.別のサブクラスを使えているか確認
本当に別クラスでインスタンス化できてるか、本来の意味のダックタイピングで確認します。
これくらいの機能だったら、2.と同様にサブクラスから定数持っていって
スーパークラスのメソッドで処理しても良いんですけどね。

class NationSuper  
    attr_reader :nation_name  
    def initialize(arg_name)  
        @nation_name = arg_name  
    end  

    def self.is_polity_of(arg_string)  
        self::NATION_AND_HEAD.has_key?(arg_string.to_sym)  
    end  
end  

class RepublicSub < NationSuper  
    NATION_AND_HEAD = { usa: "Trump",  
                        russia: "Putin",  
                        france: "Macron",  
    }.freeze  

    def show_head  
        "president #{NATION_AND_HEAD[nation_name.to_sym]}"  
    end  
end  

class MonarchySub < NationSuper  
    NATION_AND_HEAD = { uk: "George",  
                        japan: "Naruhito",  
    }.freeze  

    def show_head  
        "king #{NATION_AND_HEAD[nation_name.to_sym]}"  
    end  
end  

def head_of(nation)  
    polities = ObjectSpace.each_object(Class).select { |c| c.superclass == NationSuper }  
    polity = polities.find { |c| c.is_polity_of(nation) }  
    polity&.new(nation)&.show_head  
end  

p head_of("usa") # => "president Trump"  
p head_of("japan") # => "king Naruhito"  
p head_of("val_verde") # => nil  

usaを入れたらRepublicSubの方のshow_headが、
japanを入れたらMonarchySubの方のshow_headが呼ばれていますね。
誤った文字列が入ったときもエラーを発生させずにnilを返せています。
完璧。「引数を指定すると、自動でクラスを選んでくれる」という目標はクリアです。

#6.この方法の弱点
この方法では国名/元首リストを複数のハッシュに分けて管理しているため、
ハッシュキーの一意性を確保できません。
さらにfindメソッドの性質上、複数のクラスでヒットしても最初の一個で処理してしまいます。
仮にどこかの国の政治が混乱して、王と大統領が並立したとしても
findメソッドは最初にtrueを見つけた時点で処理をやめてしまうため、
[MonarchySub, RepublicSub]の1つ目であるMonarchySubを使ってインスタンスを作ってしまうわけです。
思わぬバグを起こさないよう、メンテナンスの時は「絶対に別の国名で登録しろ」というルールを明確化するとか、
one?メソッドとnone?メソッドでif分岐を入れ、それ以外だった場合はエラーを返すようにしても良いかもしれません。
(今回は縛りの関係でできませんでしたが。)


最後まで読んでくださってありがとうございました。
もっといい方法があったらコメントお願いします。

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

例の件でQiitaから移行します。

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう