TC-04P オーエス OS タブレット管理·保管カート オプション 換気ファン TC-04P



了解しました

  • 商品情報

    強制換気をすることで、ACアダプター部の温度上昇を防止する強制換気ファン。

    商品説明

    Naoto Ikeno
    Backend Engineer, Software Architecture & Design, Perl, Rust, Golang, GCP
    継承と委譲の使い分けと、インターフェースの重要性について
    August 28, 2018

    この記事は、「継承と委譲はどういった性質の違いがあり、どういった基準で使い分けているか?」「インターフェースは何が嬉しいのか?」といった点に関して自分なりに言語化を試みたものです。

    TL;DR
    • 継承は子が親と同じ能力や責務を獲得する。委譲は子が親を単なるツールとして所有するだけで、能力や責務は同じにはならない。

      • 子にとって親は単なるツールである場合は委譲を使うのが良い。継承してしまうと子が複数の責務を負うことになり、そのことによる不都合が生じる。
      • 逆に、子が親と同じ責務を持つべき場合には継承を使う。委譲を使うと、子が親と同じ能力を持っていないことによる不都合が生じる。
    • インターフェースを使うことで責務の分離を強制することができる。インターフェースを適切に設計することができれば、改修時の影響範囲が最低限に留まるなどシステムの保守性や堅牢性が向上し、テストの容易性も得られる。
    継承と委譲の違いと使い分け

    例として、「SQL データベースからユーザー情報を取り出したり保存したりする機能」を作ろうとしたときに、以下のような実装方針になったとします。

    1. DB の接続状態を保持したり SQL クエリを実行したりする部分のロジックは、ユーザー情報の取得に限らず、データベースから情報を取得するとき全般で使い回せそう(使いまわしたい)
    2. DB の操作周辺に関して使い回せそうな部分はクラスに切り出しておいて(ここでは Database クラスとする)、ユーザー情報の取得や保存をするためのクラス UserRepository から Database クラスを何らかの方法で利用するようにすれば、 Database クラスの DB 操作機能を使いまわせて良い感じ

    ここで、 UserRepository から Database クラスを利用する方法としては、継承または委譲が思い浮かびます。
    結論から言うと委譲を使ったほうが良いのですが、継承を使って以下のようにやってしまいたくなるかもしれません。

    委譲を使うべき場面で継承を使った例
    
    class Database {    void connectToDatabase(String dbHost, String dbName, ...){        / DBへの接続ロジック    }    Result doSQL(String sql){        / 接続しているDBにクエリを投げ、その結果を返すロジック    }    / そのほかデータベースの接続状態の管理など}
    
    
    class UserRepository extends Database {    User getUser(int userId) {        String sql = String.format("SELECT * FROM users WHERE user_id=%d", userId);        Result result = this.doSQL(sql)        / クエリの実行結果をもとにユーザーオブジェクトを作って返す    }}
    

    継承を使ったこの実装だと、 UserRepository Database を継承しているので

    :008261:アイワンファクトリー TC-04P TC-04P OS オーエス オプション オプション タブレット管理·保管カート 換気ファン TC-04P オーエス

    Database と同等の振る舞いをできるようになります。
    が、そのことによっていくつか不自然な点が生じてしまいます。

    継承によって生じた歪み

    継承によって、 UserRepository は「特定のユーザーを取得する」という本来持たせたかった役割の他に、 Database の持つ「DB との接続状態の保持や、DB への SQL クエリ送信」といった役割も獲得してしまっています。これは 単一責務の原則 に反することになります。
    本来 オーエス,OS,タブレット管理·保管カート,,オプション,換気ファン,TC-04P,,学校,会社,保管庫 収納キャビネット 充電:008261:TC-04P,オーエス,OS,タブレット管理·保管カート,オプション,,換気ファン,TC-04P,スマホ、タブレット、パソコン,スマホ、タブレットアクセサリー、周辺機器,スマホ、タブレット充電器,その他スマホ、タブレット充電器 UserRepository からすると、 Database は DB 接続のための単なるツールとして使いたいだけで、 Database 自身になりたいわけではありません。

    1 つのクラスが複数責務を持ってしまうとどうなるか

    継承などによって 1 つのクラスが複数の責務を持ってしまうと、以下のようなデメリットが生じる場合があります。

    • とある責務を期待してそのクラスを使いたいときに、期待していない機能までくっついてくることになり、クラスの再利用性が低くなる。
    • 単体テストがしずらくなる。

        強制換気をすることで、ACアダプター部の温度上昇を防止する強制換気ファン。
      • 今回の例だと、DB との接続機能だけをモックしてテストするということが困難なため、UserRepository のテストのためには実際の DB を用意しなくてはいけない。
      • 1 つの操作に対して複数の責務それぞれから起因する仕様が同時に絡んでくることになり、それらの責務を独立してテストできる場合に比べてテストケースが膨らむ。 m + n ケースで良かったところが、 m × n ケースになったりする。
    • 親クラスがパブリックメソッドを持つような場合、それらのメソッドは子クラスのメソッドとしても呼び出せるようになってしまう。今回の例だと、「ユーザーの取得」とは直接は関係のない DB 操作のメソッドも UserRepository の利用側が使えるようになり、 UserRepository が意図とは異なる使い方をされてしまう可能性が生じる。
    キッツ 青銅·黄銅バルブ スイングチャッキ OB型 JIS規格品 2インチ(50A) OB-2
    THULE(スーリー) レヴォーグ専用ベースキャリア(フット753+ウイングバー EVO7112B+キット3131)+スキーキャリア スノーパック7326B H26/6· VM#
    スガツネ工業 重量用スライドレールSR43·1490(190·027·921) SR43-1490
    魔獣王/スーパーファミコン(SFC)/箱·説明書あり
    忍び返し12型 ステンレス 10枚入り
    ゴムボート4人乗り ジョイクラフト J-キャット315(予備検査無)
    ゴムボート4人乗り ジョイクラフト グランド325(予備検査付)
    レジャーボート 水上パーティー ボート エアマット ポンプ付き 大人数
    新品本/歯周外科の臨床とテクニック 佐藤 直志
    Oxford分子医科学辞典 Constance R.Martin/〔編〕 瀬野悍二/監修 奥山明彦/監修
    wsydd Cross Stitch DIY Diamond Painting Autumn Season Flowers Trees Leaves Picture 5d Embroidery Mosaic Year Decoration Gift Diamond XR319 7
    PMSP44R8 光沢フィルム2ロール: エプソン
    50m×10個 カラー ホース ブルー 内径 15mm 中部ビニール カ施 送料無料 代引不可
    電解洗浄機 イオン洗浄器 ジュエル·リシャイナー E L21011
    法人用 会社実印·銀行印 チタン18ミリ丸 印鑑 はんこ
    オフィス 店舗 施設向け レターケース A4判縦3列 深型10段 COM A 310
    石目堆漆塗全籐倍23号(黄) (木魚バイ 木魚バチ)/受注生産商品
    御霊代用具 御霊代鏡入(白銅鏡)鏡3寸
    プロギア 2020 RS5 RS [アールエス] ドライバー(Speeder EVOLUTION for PRGR オリジナルカーボン)

    UserRepository Database のように 期待される役割が異なっている 2 つのクラス間で継承関係を持つと、上記のようにいくつか不自然な点が出てしまいます。
    エバニュー プール用品 コースロープ コースロープ7525 EVERNEW EHB018
    ダイワ(DAIWA) オフショアキャスティングロッド ソルティガAP(エアポータブル) C80HS 釣り竿
    タワースタイルステンレススチールプローントラップ2個パック 14インチ x 400フィート 無鉛シンキングラ
    HAGISOL ポケオシ1G 2CHデジタルストレージオシロスコープ メモリ長10K UDS-1G02S-10K ▼785-3025 ハギワラソリューションズ(株)
    10.2iPadフロアスタンド CR-LASTIP30Wl 同梱不可
    Wild Sports NFL Chicago Bears Deluxe MDF Cornhole Game Set 2' x 3', 9mm Thick with Corners and Aprons, Convenient Carry Handles and 8 Premiu
    栃木レザー ショルダーバッグ 肩掛け ワンショルダー 本革 キャメル チョコ レッド ブラック ポルコロッソ PORCO ROSSO 送料無料[nouki4]
    Vestax DJミキサー PMC-05PRO3 VCA エフェクトセンド/リターン機能搭載
    カーラッピングシート アルミブラッシュ152cm×30mダークシルバー ヘアラインブラッシュド耐熱耐水曲面対応裏溝付 カッティングシート
    (アクアリウム 用品)国産アクリル水槽  W1200×D450×H450/板厚:周囲8mm 底面6mm 重合接着 帯有り 同梱不可 送料要問い合わせ
    PR-501M-26D Comforter Extra Wide Medium-26 Dual Motor Lift Chair - with Head Pillow Fabric: Admiral【並行輸入品】
    MAX マックス 高圧コイルネイラ HN-50N3(D)-G クールグレー 高圧釘打機 HN91088
    ベネブ Benev GFヘアケアコンプレックスプラス [4ml×10本×2箱] GF Hair Care Complex Plus ※関税注意
    Marshall / SC212 マーシャル ギターアンプ キャビネット(お取り寄せ商品/納期別途ご案内)(WEBSHOP)
    【中古バイク】 790アドベンチャーR ワンオーナー ☆★高年式&低走行の美車!車検残たっぷり!★☆
    Yana Shiki HL2009-5 OEM Replacement Head Light Assembly for Yamaha YZF-R1【並行輸入品】
    紅白玉入れ台 / M14707/E1200 1
    (まとめ)ライオン チャーミーV 業務用 4L〔×30セット〕
    デンソーテン カーナビ ECLIPSE Rシリーズ AVN-R10W 7型ワイド トヨタ/ダイハツ用変換コード付 トヨタマップマスター地図搭
    そういう場合は代わりに委譲を使うのが適していると言えます。

    委譲を使って書き直した例

    Database は先程と同じ実装のままで、 UserRepository が変更になります。

    
    class UserRepository {    Database database;    UserRepository(Database database){        / UserRepositoryのインスタンス生成時にDatabaseのインスタンスをセット        this.database = database;    }    User getUser(int userId) {        String sql = String.format("SELECT * FROM users WHERE user_id=%d", userId);        Result result = this.database.doSQL(sql)        / クエリの実行結果をもとにユーザーオブジェクトを作って返す    }}
    

    継承はせず、 Database のインスタンスを保持しておいて、必要に応じてそれを呼び出す( 委譲 )ようになっています。
    Database のインスタンスは、 UserRepository のオブジェクト生成時のコンストラクタなどで渡すようにします。

    UserRepository Database を継承していないので Database としては振る舞えず、DB 接続ロジックなどの実装についてもよく知りません。DB のデータ ⇔User オブジェクトの変換ロジックにのみ責任を負っています。このように、移譲を使うことで単一責務の原則を守ることができ、前述した継承を用いた際のデメリットについても解消できます。
    「期待される役割が異なっている」クラスの機能を利用したい際は、継承ではなく委譲を用いるのが適切 と言えます。

    継承の使いどき

    逆に「子には親と同じ役割が期待される」場合には継承が適しています。

    たとえば、 Car (車)クラスと ElectoricCar (電気自動車)クラスを考えます。 Car のほうが ElectoricCar よりも意味が広く抽象度は高いですが、 Car を利用するときも ElectoricCar を利用するときも「加速する/減速する」「窓を開ける/閉める」といった基本的な動作は共通して可能であることが期待されます。
    「電気自動車を運転する」ことを、「車を運転する」と言い換えても間違いではないように、 Car ElectoricCar に期待される役割は基本的な部分では一緒であると言えます。

    このような場合に委譲を使うと以下のようになります。

    継承を使うべき場面で委譲を使った例
    
    class Car {    void SpeedUp(){        / ...    }    void OpenWindow(){        / ...    }    / ...}
    
    
    class ElectoricCar {    Car car;    void SpeedUp(){        car.SpeedUp();    }    void OpenWindow(){        car.OpenWindow();    }    / ...    / 電気自動車特有のロジック    void Charge() {        / ...    }}
    

    加速や減速、窓の開閉など、 Car

    :008261:アイワンファクトリー TC-04P TC-04P OS オーエス オプション オプション タブレット管理·保管カート 換気ファン TC-04P オーエス

    の持っている機能の数だけ ElectoricCar にボイラープレートコードが増えていきます。 Car に「ライトを点ける」動作が増えた場合には ElectoricCar にも変更を加えなくてはいけません。新たに TC-04P オーエス OS タブレット管理·保管カート オプション 換気ファン TC-04P HybridCar を増やそうとしたりすると目も当てられない感じになっていきます。
    また、2 つのクラスは継承関係に無いので、ダックタイピングを許さないような言語だとこのままでは ElectoricCar Car として扱うこともできません。

    継承を使って書き直した例

    Car は先程と同じ実装のままで、 ElectoricCar が変更になります。

    
    class ElectoricCar extends Car {    / 電気自動車特有のロジック    void Charge() {        / ...    }}
    

    当然といえば当然ですが委譲を使った際と比べて圧倒的にスッキリします。 Car に新たなメソッドが増えるなどの変更が入っても基本的には ElectoricCar に変更を入れる必要はありません。継承関係を持っているため ElectoricCar Car 型としても振る舞えます。
    「子には親と同じ役割が期待される」場合は、委譲ではなく継承を用いるのが適切 と言えます。

    ここまでのまとめ
    • 子に親と同じ役割が期待される場合、親と同様に振る舞えるようになる継承を使うのが良い
    • 子に親と同じ役割が期待されない場合、子にとって親は単なるツールである可能性が高いため委譲を使うのが良い
    インターフェースの使いどころ

    UserRepository について再び考えると、極端な話 UserRepository の利用側は、user さえ取得できればそれで良くて、その裏側にあるのがデータベースだろうが、 .txt ファイルに保存されていようが、オンメモリだろうがなんだって良いはずです。こういう「裏の実装は複雑な状態などを取りうるかもしれないが、使う側としては目的が達成できれば何でもいい」場合はインターフェースの出番であることが多いです。
    そこで、 IUserRepository という「ユーザーを取得・保存する」役割のインターフェースを作り、 UserRepository その他スマホ、タブレット充電器 が IUserRepository を実装するようにしてみます。

    
    interface IUserRepository {    User getUser(int userId);    void saveUser(User user);}
    
    
    class UserRepository implements IUserRepository{    Database database;    UserRepository(Database database){        this.database = database;    }    User getUser(int userId) {        String sql = String.format("SELECT * FROM users WHERE user_id=%d", userId);        / ...    }    / ...}
    

    UserRepository の利用側のロジックは例えば以下のようになります。

    
    class SomeApplicationService {    IUserRepository userRepository;    void changeUserEmail(String email){        User user = userRepository.getUser(userId);        user.setEmail(email);        userRepository.saveUser(user);    }}
    

    IUserRepository はインターフェースのため、実装に関して全く言及をしていません。そのため、 IUserRepository を利用している SomeApplicationService 側としては、「とにかく user が取得できることを期待しており、実装は問わない(実装の違いによって SomeApplicationService が影響を受けることはない)」ということをコードで表明している ことになります。
    インターフェースは、 何を公開し、何を隠すべきか?ということを明示的に定義し、それを強制させることができる というのが大きな魅力です。

    インターフェースの恩恵

    インターフェース IUserRepository は、DB の接続、状態管理、難しい SQL クエリといった色々な辛さがある外部世界をフィルタリングし、 「なんか知らんけど userId を投げるといい感じに user を返してくれるし、user を投げるといい感じに user を保存してくれる」という、キレイで抽象的な世界だけを見えるようにしてくれている と言えます。

    インターフェースを挟むことで、 SomeApplicationService UserRepository への依存から開放されています。
    このことによって、もし UserRepository の内部ロジックに何らかの大規模な変更が入ったとしても、仮に DB が MySQL から NoSQL に変わったとしても、インターフェースが変わらない限りは、 SomeApplicationService まで連鎖的に変更が波及するようなことはありません。

    この性質はテストの際にも大いに役立ちます。例えば SomeApplicationService のテスト用に IUserRepository を実装したインメモリの InMemoryUserRepository を用意しておいて、テストのときだけそれに差し替えるといったことも可能となるため、よりテストが容易になります。
    同じ要領で、インターフェースを使うと、たとえば「ユーザーにメールを飛ばす」といったクラスを、テストのときだけは「送っているように見えて実際には何もしない」クラスに差し替えるようなことも可能です。

    インターフェースの重要性

    インターフェースは、大規模な開発においては非常に重要な役割を果たします。インターフェースだけきちんとしてればその内部実装はまぁある程度雑でも案外困らないな、というのが最近の自分の考えです。
    逆に、初期にインターフェースの設計をミスると、いざ改修が必要になった時には既に手遅れになっているような場合も多く、その場合は大いなる苦しみを伴いながら作りかえを行うことになります。
    インターフェースの設計がその後のシステムの保守性・堅牢性の明暗を分けると言っても過言では無いため、インターフェースを設計するときは、抽象度は本当に適切なのか?余計な情報まで受け取ったり渡したりしてしまってないか?など、慎重に考えるとよいです。

    インターフェースという概念

    ここまで、インターフェースと言った際になんとなく Java の interface を意識して書いてきましたが、ダックタイピングが可能な言語ではわざわざ実装と別にインターフェースクラスを定義する必要性が薄い場合もあります。また、Java の interface のような言語機能がない言語も存在するかと思います。
    ただ、そういった場合でもインターフェースの重要さは変わらないと考えます。インターフェースの本質は、「どういう情報を公開情報としてクラスやオブジェクト同士がコミュニケーションを取るか」という点であり、そのことについて思考の余地がある言語であればインターフェースについて扱っていることになります。そこにある違いは、インターフェースという概念を具現化した言語機能が明示的に存在するか否かというだけであり、インターフェースの本質的な概念はプログラミング言語にかかわらず普遍的に存在し、とても大事な概念であるという点については変わりません。

    おわりに

    自分なりに継承/委譲/インターフェースについて解説してみました。
    わからない点や質問などあればぜひコメントいただけると嬉しいです。

    Naoto Ikeno
    Backend Engineer, Software Architecture & Design, Perl, Rust, Golang, GCP
    Back to blog posts