2023/03/03、エンジニア文化祭2023の登壇資料です。 『分岐を低減するinterface設計と発想の転換』 https://forkwell.connpass.com/event/272596/
分岐を低減するinterface設計と発想の転換Mar. 3, 2023 @ エンジニア文化祭 2023READYFOR株式会社 ミノ駆動
View Slide
みんなー 元気ー?
まずは元気にアイサツしてみよう
#Forkwell文化祭設計なんもわからん
本資料のリンクは #Forkwell文化祭でツイート済みです資料後半が詳細かつ重厚なのでお手元で資料を見ながらのご視聴を推奨します
こんにちは ミノ駆動です
ここで働いています
最近ニュースになったクラウドファンディング
職歴とか今やってる仕事とか大手電機メーカーなどで十数年組込み系、2019年にWeb系へ移籍、2021年4月にREADYFORにジョインアプリケーションアーキテクトとして、レガシーシステムのリファクタリングや拡張性向上設計など、システム設計に従事ミノ駆動@MinoDriven
著作紹介『良いコード/悪いコードで学ぶ設計入門』技術的負債を作り込まない、変更容易性の高い設計を学ぶ、初級〜中級向け入門書。新卒エンジニアに最適です。輪読会も多数開催されております。発売10ヶ月で8刷重版発行部数3万部 超え達成3万部記念で帯が付きました
ITエンジニア本大賞2023 大賞受賞!!
【本日のお話】分岐低減に寄与するinterfaceの設計に必要な考え方や発想の転換
分岐が複雑化して困っていませんか?
プログラミング言語には、抽象的に扱う仕組みとしてinterface(またはinterfaceに類似する仕組み)があります。クラス クラス クラスinterfaceinterfaceを使うと分岐が劇的に低減し、コードがシンプルになります。
でも……
interface上手く使えてます??
【架空事例】防犯システムで考えるinterface設計
【架空事例】ホームセキュリティシステムの開発製品コンセプト:家全体の防犯システムを統括的に制御するアプリケーション初期の仕様:※あくまでinterface設計を論点とする架空事例です。このセキュリティシステム自体の細かな仕様や制御技術についてあれこれ言われても困ってしまいますので、論点と異なる事柄についてはご容赦願います。不審者の侵入検知 赤外線センサー侵入検知時の通知方法 家人のスマホへSMSで通知
その後……「もっといろんな仕組みに対応できるようにしてほしい」という市場要望が上がってきました。要望例:窓ガラスの破損を検知できるようにしてほしい。監視カメラに対応してほしい。メッセージアプリでも通知を受け取れるようにしてほしい。
さらに、顧客によって仕組みのニーズがばらばらでした。(ある顧客は「防犯カメラ」と「SMS通知」がいいと言ったり、また別の顧客は「赤外線センサー」と「メッセージアプリ通知」の組合せを!と言ってきたり)こうした各顧客の詳細なニーズに対応したシステムを、個別にオーダーメイドで開発するのは莫大な工数がかかり、非現実的です。従って、元のアプリケーションに仕様追加する形で顧客ニーズの吸収を試みました。
if (赤外線センサー.有効() &&赤外線センサー.検知()) {if (SMS通知.有効()) {SMS通知.実行();}if (メッセージアプリ通知.有効()) {メッセージアプリ通知.実行();}} else if (窓ガラス破損.有効() &&窓ガラス破損.検知()) {if (SMS通知.有効()) {SMS通知.実行();}if (メッセージアプリ通知.有効()) {メッセージアプリ通知.実行();….左記のように分岐が複雑な作りになってしまいました。その後さらに仕様追加されるにつれ、分岐は爆発的に複雑化していきました……
このような事例と同じ経験はありませんか?ウッてなりませんでしたか??
一方、interfaceが分岐低減につながる、となんとなく知ってはいるものの、実際の開発で上手く活用できず、結局既存ロジックに分岐をネジ込んでしまっていませんか?
interfaceを上手く活用するには大幅な発想の転換が必要です
interface設計が上達するものの見方、考え方を解説します
interface設計の要点2本柱要点1:目的単位で抽象化すること要点2:「作る」と「使う」を分けること本日の理解目標
要点1:目的単位で抽象化すること
システムは目的達成手段
システムは目的達成手段移動手段の一例二足歩行も立派なシステム「〜〜したい」という目的を叶えるための目的達成手段がシステムである上記例は「目的地へ移動したい」という目的を叶えるための移動手段システムには必ず目的がある
【目的】商品を売買したい【売買手段】ECサイト当然ソフトウェアも目的達成手段【目的】遊びたい【娯楽手段】ゲームソフト
【目的達成手段】システム【目的達成手段】クラス【目的達成手段】クラス【目的達成手段】クラスシステムは目的達成手段であるからその構成要素たるクラスもまた目的達成手段
問題領域と解決領域
目的地に移動したい自動車飛行機電車問題領域解決領域問題領域に対し、それを解決するさまざまな解決領域が世の中にはある
目的の具体化(ツリー構造化)
目的 課題 対策
● 目的が決まると課題が浮き彫りになる● 課題に対処するための解決手段が決まる● 目的が違うと課題も解決手段も違ってくる
【問題領域】 【解決領域】【目的達成手段】地図【目的】山登りしたい【目的/課題】遭難したくない【目的/課題】虫刺され怖い【目的達成手段】虫除けスプレー【目的達成手段】山登り課題抽出(目的の具体化)手段策定
【問題領域】目的/課題目的/課題 目的/課題目的/課題 目的/課題【解決領域】達成手段達成手段 達成手段達成手段 達成手段目的/課題と達成手段は、ツリー構造、かつ鏡合わせの関係になる
目的達成手段とinterfaceの関係
【問題領域】 【解決領域】自動車 電車 飛行機手段の具体的な特徴や性質は異なるが、全て移動目的に対応する手段【目的】目的地に移動したい
【問題領域】 【解決領域】【目的】目的地に移動したい自動車 電車 飛行機<< interface >>移動手段移動する()目的達成手段の抽象化同一目的に対する達成手段のバリエーション
自動車 電車 飛行機<< interface >>移動手段移動する()ゴーヤ 船どう考えても移動目的には使えない移動目的として使用可能
防犯システムの設計に応用する
仕様1 赤外線センサーで侵入検知する。仕様2 侵入検知したことを家人のスマホへSMS通知する。要望1 メッセージアプリでも通知してほしい。要望2 窓ガラスの破損を検知できるようにしてほしい。要望3 監視カメラに対応してほしい。もちろん録画して後から見れるようにしてほしい。要望4 顔で家人かどうか判別はできないものか。要望5 動き方の特徴で家人かどうか判別できませんか。要望6 物を壊すなど、異常な行動を検知できないものか。初期の仕様:追加要望:
仕様1 赤外線センサーで侵入検知する。 侵入検知目的仕様2 侵入検知したことを家人のスマホへSMS通知する。 通知目的要望1 メッセージアプリでも通知してほしい。 通知目的要望2 窓ガラスの破損を検知できるようにしてほしい。 侵入検知目的要望3 監視カメラに対応してほしい。もちろん録画して後から見れるようにしてほしい。侵入検知目的要望4 顔で家人かどうか判別はできないものか。 映像分析目的要望5 動き方の特徴で家人かどうか判別できませんか。 映像分析目的要望6 物を壊すなど、異常な行動を検知できないものか。 映像分析目的初期の仕様:追加要望:
【問題領域】 【解決領域】【目的】通知したい<< class >>SMS通知<< class >>アプリ通知<< interface >>通知手段通知する()【目的】SMSで通知したい【目的】アプリで通知したい通知目的で抽象化可能
【問題領域】 【解決領域】【目的】侵入検知したい<< class >>赤外線センサー<< class >>ガラス破壊センサー<< interface >>侵入検知手段検知する()【目的】動きで検知したい【目的】ガラス破損を検知したい検知目的で抽象化可能【目的】映像で検知したい<< class >>監視カメラ※ここでの「赤外線センサー」や「ガラス破壊センサー」は物理的なデバイスではなく、各デバイスの制御用クラスをイメージしております
【目的】映像を分析して検知できるようにしたい【目的】顔で家人かどうかを判別したい【目的】動き方の特徴で家人かどうかを判別したい【目的】異常行動を検知したい【目的】映像で検知したい【目的】映像を後から見れるようにしてほしい
<< class >>顔検出<< class >>歩行パターン検知<< class >>異常行動検知<< class >>監視カメラ<< class >>映像記録<< interface >>パターン検知手段検知する()
顔検出歩行パターン検知異常行動検知監視カメラ映像記録<< interface >>パターン検知手段検知する()ガラス破壊センサー赤外線センサー<< interface >>侵入検知手段検知する()<< interface >>通知手段通知する()SMS通知 アプリ通知防犯システム
要点2:「作る」と「使う」を分けること
if (赤外線センサー.有効() &&赤外線センサー.検知()) {if (SMS通知.有効()) {SMS通知.実行();}if (メッセージアプリ通知.有効()) {メッセージアプリ通知.実行();}} else if (窓ガラス破損.有効() &&窓ガラス破損.検知()) {if (SMS通知.有効()) {SMS通知.実行();}if (メッセージアプリ通知.有効()) {メッセージアプリ通知.実行();….良くないロジックは、何を使うかを判断する分岐と実行するロジックが混在し、あみだくじ状態になっています。これは自動車に例えたら、自動車を走らせながら自動車を組み立てているのと同じこと。現実ではあり得ない珍妙なことですが、ソフトウェアではできてしまいます。
作る 使う「作る」と「使う」を分離する作って完成されたものを使う、という発想が重要
【作る】● どの部品(interface実装クラス)を使用するかの判断は、FactoryやDIコンテナ(※ここでは「作る側」と呼称)に任せる。● 部品選択と組み立て(interface実装クラスの選択判断とインスタンス化)は作る側にカプセル化する。【使う】● 使う側ではどの部品(interface実装クラス)が使われているかは一切気にもせず判断もしない。● 渡されたインスタンスをただ実行するだけに徹する。
全体構造※あくまでinterface設計を論点とするクラス図と疑似コードです。「イベント駆動にすべき」「もっと複雑な制御が必要なはず」といった論点と異なる指摘についてはご容赦願います。
映像分析侵入検知通知防犯システム
通知
使う側作る側
通知手段SMS通知 アプリ通知
class AlarmDestinations {private final List alarmDestinations;AlarmDestinations() {alarmDestinations = new ArrayList<>();}void add(Alarmable alarmDestination) {alarmDestinations.add(alarmDestination);}void alarm() {alarmDestinations.forEach(Alarmable::alarm);}}通知interfaceのファーストクラスコレクションinterface実装クラスが何であるか知らないが、ただ実行するだけ使う側interface実装クラスのインスタンスを渡してリストに追加
class AlarmDestinationsFactory {static AlarmDestinations create() {AlarmDestinations alarmDestinations = new AlarmDestinations();if (SmsAlarm.isEnabled()) {alarmDestinations.add(new SmsAlarm());}if (SmartphoneAppAlarm.isEnabled()) {alarmDestinations.add(new SmartphoneAppAlarm());}return alarmDestinations;}}作る側どのinterface実装クラスを使うか判断して組み込む。このfactory内で必要なものを全て組み上げる。
侵入検知
使う側作る側赤外線センサーガラス破壊センサー監視カメラ
public class TrespassDetectors {private final List detectors;TrespassDetectors() {detectors = new ArrayList<>();}void add(TrespassDetectable detector) {detectors.add(detector);}boolean detect() {return detectors.stream().anyMatch(TrespassDetectable::detect);}}侵入検知interfaceのファーストクラスコレクションinterface実装クラスが何であるか知らないが、ただ実行するだけ使う側Factoryは通知用Factoryとほぼ同じ構造だが、詳しくは後述interface実装クラスのインスタンスを渡してリストに追加
映像分析
使う側作る側顔検出歩行パターン検知 異常行動検知パターン検知手段
class TrespassDetectorsFactory {static TrespassDetectors create() {TrespassDetectors trespassDetectors = new TrespassDetectors();if (InfraredSensor.isEnabled()) {trespassDetectors.add(new InfraredSensor());}if (GlassDestructionSensor.isEnabled()) {trespassDetectors.add(new GlassDestructionSensor());}if (MonitorCamera.isEnabled()) {PatternDetectors patternDetectors = PatternDetectorsFactory.create();trespassDetectors.add(new MonitorCamera(patternDetectors));}return trespassDetectors;}作る側Factory内でFactoryをcallどのinterface実装クラスを使うか判断して組み込む。このfactory内で必要なものを全て組み上げる。
class MonitorCamera implements TrespassDetectable {private final VideoRecorder videoRecorder;private final PatternDetectors patternDetectors;MonitorCamera(PatternDetectors patternDetectors) {videoRecorder = new VideoRecorder();this.patternDetectors = patternDetectors;}public boolean detect() {return patternDetectors.detect();} 監視カメラクラスは具体的にパターン検知機能を持っているかは知らない。ただ検知を実行するだけ。
TrespassDetectors trespassDetectors = TrespassDetectorsFactory.create();AlarmDestinations alarmDestinations = AlarmDestinationsFactory.create();HomeSecurityService homeSecurityService =new HomeSecurityService(trespassDetectors,alarmDestinations);通知と侵入検知のインスタンスは、Factoryで作って……作ったインスタンスを防犯サービスクラスに渡すだけ。これで完成品。
class HomeSecurityService {private final TrespassDetectors trespassDetectors;private final AlarmDestinations alarmDestinations;HomeSecurityService(TrespassDetectors trespassDetectors,AlarmDestinations alarmDestinations) {this.trespassDetectors = trespassDetectors;this.alarmDestinations = alarmDestinations;}void execute() {if (trespassDetectors.detect()) {alarmDestinations.alarm();}}使う側どんな侵入検知機能や通知機能を持っているかは知らない。侵入検知して通知するだけ。
FactoryはDIコンテナでも良い(or望ましい)
以上がinterface設計の要点です
【トピック】目的のバリエーションと機能性
【疑問】同一目的なのになぜ手段にさまざまなバリエーションがあるのでしょう?
テクノロジーの本質は能力の拡大と縮小同じ目的でも達成するための能力や効率が格段に違います移動速度
移動速度小回り達成手段それぞれで性能(人間にとっての能力の拡大、縮小率)が異なる我々は状況に応じて最適な手段を選択して生活している※他にも輸送量やコストなどさまざまな観点がありますが割愛
● interface設計可能な箇所は、機能性向上の可能性を示唆しています。● 同一目的でも、より顧客にリーチする機能に置き換えられる可能性を秘めています。● 仕様変更で頻繁にコード変更している箇所はありませんでしょうか。そこはモードやバリエーションを切り替えるための分岐で複雑化していませんでしょうか。● そうした箇所を整理してinterfaceにすると、より高機能なものへ素早く置き換えられるようになり、開発生産性が高まるでしょう。
注意点
● 条件分岐をなんでもかんでもinterfaceに置き換えないように注意しましょう。interfaceは銀の弾丸ではありません。● 条件分岐はバリデーションの他、計算判断やアルゴリズム処理にも用いられます。● interfaceは本資料で解説したように、基本的には目的達成手段のバリエーション、という観点で用いるのが良いでしょう。
まとめ● システムは目的達成手段であり、その構成要素たるクラスやinterfaceも目的達成手段であることを意識すること。● interfaceは目的単位で抽象化すること。● interface実装クラスは同一目的に対する達成手段のバリエーション。● interfaceを用いる際は、「作る」と「使う」を分けること。● どのinterface実装クラスを用いるかの判断はFactoryやDIコンテナなど「作る側」に持たせ、全てを組み込んだ完成品として生成すること。● 「使う側」は完成品をただ実行するだけのシンプルな構造にすること。部品(interface実装クラス)が何であるかいちいち気にしない構造にすること。● interface設計した箇所はより高機能なものへ置換できる可能性を秘めている。● interfaceは銀の弾丸ではない。基本的には手段のバリエーション、という観点での設計を推奨。
おまけ
interface設計やプラグインアーキテクチャに詳しい書籍今回の防犯システムの例も本書の援用
抽象化の考え方に詳しい書籍
課題(目的)の具体化や深堀りに役立つ書籍
ご清聴ありがとうございました