Upgrade to Pro — share decks privately, control downloads, hide ads and more …

美しい設計の色香に惑わされないために - 手段としてのオブジェクト "C" 向

美しい設計の色香に惑わされないために - 手段としてのオブジェクト "C" 向

D068c367c4ebaaa982d583cc51dbd45a?s=128

Shota Takizawa

February 16, 2020
Tweet

Transcript

  1. 美しい設計の色香に 惑わされないために - 手段としてのオブジェクト "C" 向 GMO インターネット 次世代システム研究室 システムエンジニア

  2. 1 テストを書きやすくしたい! 仕様変更に強くしたい!

  3. 2 こういう設計があってね…… テストを書きやすくしたい! 仕様変更に強くしたい!

  4. 3 すてき!! こういう設計があってね…… テストを書きやすくしたい! 仕様変更に強くしたい!

  5. ちょっとまって! 4

  6. その設計 本当に必要ですか? 5

  7. その設計 本当に必要ですか? ※たぶん必要です 6

  8. はじめまして 7 GMOインターネット 株式会社 次世代システム研究室 システムエンジニア サーバサイドのアプリエンジニア

  9. Agenda ◼ そもそも設計の目的って? ◼ C言語でオブジェクト指向 ◼ 実際のアプリではどうなの? 8

  10. Agenda ◼ そもそも設計の目的って? ◼ C言語でオブジェクト指向 ◼ 実際のアプリではどうなの? 9

  11. 設計してますか? 10

  12. なんのために 11 設計してますか?

  13. 設計って何ですか? 12

  14. 13 コードレビューおねがいします!

  15. 14 コードレビューおねがいします! うーん。ちょっと設計的におかしいね

  16. 設計的におかしい 15

  17. 設計的に正しい 16

  18. 設計的に正しい 17 実装が設計に組み込まれた ルールに従っている

  19. 設計的に正しい 18 実装が設計に組み込まれた ルールに従っている 例:DB は必ず Model で操作する

  20. 設計的に正しい 19 実装が設計に組み込まれた ルールに従っている 例:DB は必ず Model で操作する

  21. ということは 20

  22. 設計 21 課題解決のための ルールをつくること

  23. 設計 22 課題解決のための ルールをつくること

  24. Object Oriented Conference 23

  25. オブジェクト指向 の話をします 24

  26. 25 テストのために DB 操作をまとめたい 課題

  27. 26 テストのために DB 操作をまとめたい 課題 ◼ 責務の適切な分担 ◼ DB アクセスの抽象化

    とかとか必要ですね
  28. 27 テストのために DB 操作をまとめたい 課題 ◼ 責務の適切な分担 ◼ DB アクセスの抽象化

    とかとか必要ですね ゼロから実現方法を 模索するのは大変
  29. 28 テストのために DB 操作をまとめたい 課題 ◼ 責務の適切な分担 ◼ DB アクセスの抽象化

    とかとか必要ですね ゼロから実現方法を 模索するのは大変 オブジェクト指向を導入 実装できるルールに落とし込む
  30. 29 テストのために DB 操作をまとめたい 課題

  31. 30 テストのために DB 操作をまとめたい 課題 DB 操作は必ず Model を使う ルール

  32. オブジェクト指向 31 設計のための手段・ツール

  33. 32 DB 操作は必ず Model を使う ルール

  34. 33 DB 操作は必ず Model を使う ルール

  35. 34 DB 操作は必ず Model を使う ルール MVC アーキテクチャ

  36. 35 設計のための手段・ツール ◼ オブジェクト指向 ◼ ◦◦アーキテクチャ ◼ □□設計

  37. 36 課題解決のためのルールづくり デザインパターンを道具として駆使

  38. ところで 37

  39. もう1つ疑問がありました 38

  40. 人間の 迷いを断つ ため 39

  41. 40 このメソッド, ここから呼んでいいんだっけ?

  42. 41 このフィールド, 外からアクセスしていいんだっけ?

  43. 設計で決められた ルールに従えば良い 42

  44. でも 人間がルールを守る のは大変 43

  45. かわりに 言語 に守ってもらう 44

  46. 45 アクセス指定子 メンバアクセスのルールを規定 例

  47. 46 抽象クラス 変数やメソッドのルールを規定 例

  48. ところで 47

  49. 今日の話 48

  50. オブジェクト “C”向 49

  51. C言語には オブジェクト指向の 構文がない 50

  52. オブジェクト指向的 なルールを規定したい 51

  53. 言語は 守ってくれない 52

  54. どうする? 53

  55. 人間が守るしかない 54

  56. 人間が守るしかない 55 限界がある

  57. 課題解決に 必要なルールだけ を決める 56

  58. 課題解決に 必要なルールだけ を決める 57 設計の本質に近い

  59. ということで 58

  60. Agenda ◼ そもそも設計の目的って? ◼ C言語でオブジェクト指向 ◼ 実際のアプリではどうなの? 59

  61. C言語で オブジェクト指向 60

  62. 「なんか面倒」 「制約あるな」 を感じてほしい 61

  63. オブジェクト指向の3大要素 62 カプセル化 メンバを保護 継承 クラスを引き継ぐ ポリモーフィズム 同じメソッドが複数の振る舞い

  64. これさえあれば どうにかなりそう 63

  65. その前に 64

  66. オブジェクト指向 65

  67. オブジェクト指向 C言語でオブジェクトを表現します 66

  68. オブジェクトの表現 素直に構造体 struct Request { int id; int src; int

    dst; char payload[256]; … }; 67
  69. オブジェクトの表現 「操作」が書けない struct Request { int id; int src; int

    dst; char payload[256]; … }; 68
  70. オブジェクトの表現 関数の名前で表現 void requestSetHeader(Request *r, char *header) { … }

    int requestSend(Request *r) { … } 69
  71. オブジェクトの表現 関数の名前で表現 void requestSetHeader(Request *r, char *header) { … }

    int requestSend(Request *r) { … } 70
  72. オブジェクトの表現 関数ポインタで表現 struct Request { int id; … void (*setHeader)(Request

    *r, char *); int (*send)(Request *r); }; 71
  73. オブジェクトの表現 関数ポインタで表現 struct Request req; req.setHeader = func1; req.send =

    func2; … req.send(req); 72
  74. カプセル化 73

  75. カプセル化 そのままだとすべてpublic struct Request { int id; int src; int

    dst; char payload[256]; … }; 74
  76. カプセル化 ヘッダファイル 関数や変数の宣言だけしておく ソースファイル 実際の処理を書く よそで作った関数はヘッダを #include 75

  77. カプセル化 ヘッダに前方宣言します typedef struct Request Request; Request.h 76

  78. カプセル化 コンストラクタと各種操作も宣言 typedef struct Request Request; Request *requestNew(int); int requestGetId(Request

    *); Request.h 77
  79. カプセル化 中身はソースに書く struct Request { int id; int src; int

    dst; char payload[256]; }; Request.c 78
  80. カプセル化 中身はソースに書く #include "Request.h" Request *requestNew(int id) { Request *r

    = (Request*)malloc(sizeof(Request)); r->id = id; return r; } int requestGetId(Request *r) { return r->id; } Request.c 79
  81. カプセル化 使うとき #include <stdio.h> #include "Request.h" int main(void) { Request

    *r = requestNew(1); printf("%d¥n", requestGetId(r)); } main.c 80
  82. カプセル化 これらは全てコンパイルできない Request r1; Request *r2 = (Request *)malloc(sizeof(Request)); Request

    *r3 = requestNew(1); r3->id; main.c 81
  83. カプセル化 これらは全てコンパイルできない Request r1; Request *r2 = (Request *)malloc(sizeof(Request)); Request

    *r3 = requestNew(1); r3->id; main.c 全てprivateになった! 82
  84. カプセル化 全てprivateでは困る? 83 関数ポインタどうなるのよ

  85. カプセル化 前方宣言をする 84 privateになる

  86. カプセル化 工夫すると部分的にprivateにできます struct RequestImpl; typedef struct Request { int id;

    struct RequestImpl *pImpl; } Request; Request.h 85
  87. カプセル化 工夫すると部分的にprivateにできます struct RequestImpl; typedef struct Request { int id;

    struct RequestImpl *pImpl; } Request; Request.h 86 public private RequestImplの中身を.cに書く
  88. カプセル化 Pimpl : Pointer to implementation struct RequestImpl; typedef struct

    Request { int id; struct RequestImpl *pImpl; } Request; Request.h 87 public private
  89. どこに何を書くか というルールは 人間が守る 88

  90. 正直 ヘッダ見てアドレス計算すれば 全部見える 89

  91. 継承 90

  92. struct Request { int id; int src; int dst; char

    payload[256]; … }; 継承 91 これを継承して RequestEx をつくる
  93. その前に 92

  94. 継承 メモリの話をします struct Request { int id; int src; int

    dst; char payload[256]; … }; 93
  95. 継承 struct Request { int id; int src; int dst;

    char payload[256]; … }; 0x00f5 b2da fdc0 プログラムからみた アドレス ︙ 94
  96. 継承 struct Request { int id; int src; int dst;

    char payload[256]; … }; int id int src int dst char payload[256] 0x00f5 b2da fdc0 プログラムからみた アドレス ︙ 上から順に並ぶ 95
  97. つまり 96

  98. 継承 struct Request { int id; int src; int dst;

    char payload[256]; … }; struct RequestEx { Request request; int type; } extends 親クラスをメンバの先頭に配置 97
  99. struct RequestEx { Request request; int type; } 継承 Request

    request int type 0x00f5 b2da fdc0 プログラムからみた アドレス ︙ 98
  100. struct RequestEx { Request request; int type; } 継承 Request

    request int type 0x00f5 b2da fdc0 プログラムからみた アドレス ︙ 99 ここだけ見れば親
  101. 継承 Request request int type 100 私は RequestEx のポインタです 中身は

    Request と int だな
  102. 継承 int id int src int dst char payload[256] int

    type 101 私は Request のポインタです 中身は int, int, int, char[256]
  103. 継承 キャストして親の関数を使う // 自分の関数はそのまま呼ぶ requestExMethod(rex); // 親の関数は親にキャストして呼ぶ requestMethod((Request *)rex); 102

  104. 先頭に配置する というルールは 人間が守る 103

  105. 継承 子クラスのコンストラクタ #include "Request.h" RequestEx *requestExNew(int id, int type) {

    RequestEx *rex = (RequestEx *) malloc(sizeof(RequestEx)); rex->request.id = id; rex->type = type; return rex; } 104
  106. 継承 親クラスのコンストラクタを使いたい #include "Request.h" RequestEx *requestExNew(int id, int type) {

    RequestEx *rex = (RequestEx *) malloc(sizeof(RequestEx)); rex->request.id = id; rex->type = type; return rex; } 105
  107. 継承 このままでは呼べない Request *requestNew(int); RequestExの中のRequestはポインタではないため struct RequestEx { Request request;

    int type; } 106
  108. 継承 メモリ確保を外に持っていく void requestNew(Request *r, int id) { … }

    void requestExNew(RequestEx *rex, int id, int type) { requestNew(&(rex->request), id); rex->type = type; } RequestEx *r = (RequestEx *)malloc(sizeof(RequestEx)); requestExNew(r, 1, 2); 107
  109. void requestNew(Request *r, int id) { … } void requestExNew(RequestEx

    *rex, int id, int type) { requestNew(&rex->request, id); rex->type = type; } RequestEx *r = (RequestEx *)malloc(sizeof(RequestEx)); requestExNew(r, 1, 2); 継承 メモリ確保を外に持っていく 108 併用不可
  110. void requestNew(Request *r, int id) { … } void requestExNew(RequestEx

    *rex, int id, int type) { requestNew(&rex->request, id); rex->type = type; } RequestEx *r = (RequestEx *)malloc(sizeof(RequestEx)); requestExNew(r, 1, 2); 継承 メモリ確保を外に持っていく 109 Pimpl を使う必要がある
  111. ポリモーフィズム 110

  112. ポリモーフィズム 関数ポインタを持つ親を継承 struct Request { int id; … void (*setHeader)(char

    *); int (*send)(void); }; struct RequestEx { Request request; … }; 111
  113. ポリモーフィズム 関数ポインタを持つ親を継承 struct Request { int id; … void (*setHeader)(char

    *); int (*send)(void); }; struct RequestEx { Request request; … }; 112 子のコンストラクタで関数ポインタを上書き
  114. ポリモーフィズム 関数ポインタを持つ親を継承パターン void requestPush(Request *r) { r->send(r); } /* RequestEx

    *rex を用意 */ /* r->send は Request と RequestEx で別の関数 */ requestPush((Request *)rex); 113
  115. キャストして呼ぶ というルールは 人間が守る 114

  116. 他にもあるけど ちょっと複雑です 115

  117. それらしい実装 に落とすことはできる 116

  118. 結構つらい 117

  119. やっぱり Cで書かれたOSSでは ゴリゴリに 使われているのか? 118

  120. そんなことはない 119

  121. 使うべきところに 使いやすい形で 使っている 120

  122. Agenda ◼ そもそも設計の目的って? ◼ C言語でオブジェクト指向 ◼ 実際のアプリではどうなの? 121

  123. Linux の ファイルシステム をみてみましょう 122

  124. 123 ファイルシステム ディスクを抽象化 HDD HDD USBメモリ FS FS FS

  125. 124 ファイルシステム自体も複数種類 HDD HDD USBメモリ ext4 btrfs FAT32

  126. 125 ファイルシステム自体も複数種類 HDD HDD USBメモリ ext4 btrfs FAT32 これらを統一的に扱いたい

  127. VFS 126

  128. VFSが解決したい課題 127 様々なファイルシステムを 統一的に扱う

  129. VFSが解決したい課題 128 様々なファイルシステムを 統一的に扱う オブジェクト指向の出番だ!

  130. 継承! ポリモーフィズム! 129

  131. 実装を 見てみましょう 130 Linux Kernel 5.5 の

  132. 131 ファイルシステムを使えるようにする Kernel HDD ??? HDD ??? USBメモリ ??? ext4

    brtfs FAT32
  133. 132 ファイルシステムを使えるようにする Kernel HDD ext4 HDD btrfs USBメモリ FAT32 ext4

    brtfs FAT32
  134. 133 int register_filesystem(struct file_system_type * fs) { … } fs/filesystems.c

    ファイルシステムを使えるようにする
  135. 134 int register_filesystem(struct file_system_type * fs) { … } fs/filesystems.c

    ファイルシステムを使えるようにする ここに必要な情報を書いて渡す
  136. 135 struct file_system_type { const char *name; int fs_flags; …

    int (*init_fs_context)(struct fs_context *); … include/linux/fs.h ファイルシステムを使えるようにする 名前やら関数ポインタやら
  137. 136 ext4 の場合 static struct file_system_type ext4_fs_type = { .owner

    = THIS_MODULE, .name = "ext4", .mount = ext4_mount, .kill_sb = kill_block_super, .fs_flags = FS_REQUIRES_DEV, }; /* 初期化関数内 */ err = register_filesystem(&ext4_fs_type); fs/ext4/super.c 普通のインスタンスでした
  138. 137 本丸に突入

  139. 138 ファイルを操作する部分 const struct file_operations ext4_file_operations = { … .open

    = ext4_file_open, .release = ext4_release_file, .fsync = ext4_sync_file, … fs/ext4/file.c 構造体に関数ポインタを設定
  140. 139 どこから呼ばれるのか struct inode { … const struct file_operations *i_fop;

    … include/linux/fs.h inode 構造体のメンバにいた! これもインスタンスへのポインタ
  141. 140 オブジェクト指向 どこ…… 前半の説明は何だったんだ……

  142. 141 外から見ると多態 struct inode *p_inode; … /* 内部で p_inode->i_fop を参照

    */ any_function(p_inode); i_fop で指定した関数で動作が変わる
  143. 142 実質的にインタフェース 言語の機能としては構造体のインスタンス 意図は明らかにインタフェース struct inode { … const struct

    file_operations *i_fop; … class Ext4 implements VFS {…} class Inode { VFS vfs; } ※inode は ext4 用のものというわけではない
  144. VFS 143 課題 ファイルシステムを統一的に扱う

  145. VFS 144 課題 ファイルシステムを統一的に扱う 設計 オブジェクト指向を取り入れて インタフェースで表現

  146. VFS 145 課題 ファイルシステムを統一的に扱う 設計 オブジェクト指向を取り入れて インタフェースで表現 実装 構造体と手続き的な記述を駆使して インタフェースを実現

  147. 146 もうひとつ

  148. VFS 147 課題 ファイルシステムを統一的に扱う 設計 オブジェクト指向を取り入れて インタフェースで表現 実装 構造体と手続き的な記述を駆使して インタフェースを実現

    妥協点
  149. 148 インタフェースがある言語

  150. 149 class Ext4 implements VFS { … } class Btrfs

    implements VFS { … } ファイルシステムは VFS インタフェースを使うのね
  151. 150 interface VFS { public void file_open() { ... }

    public void file_read() { ... } public void file_write() { ... } … } ふむふむ こんなメソッドがあるのね
  152. 151 インタフェースがある言語 概念の理解が完全でなくても ある程度追える

  153. 152 インタフェースがある言語 概念の理解が完全でなくても ある程度追える コードから VFS の概念を 読み取ることができる

  154. 153 今回紹介した インタフェースっぽい実装

  155. 154 とりあえず ext4 を見るか

  156. 155 とりあえず ext4 を見るか げっ 関数がいっぱいある……

  157. 156 とりあえず ext4 を見るか げっ 関数がいっぱいある…… これがファイルを開く処理?

  158. 157 const struct file_operations ext4_file_operations = { … .open =

    ext4_file_open, .release = ext4_release_file, .fsync = ext4_sync_file, … おっ。それっぽいな。
  159. 158 const struct file_operations ext4_file_operations = { … .open =

    ext4_file_open, .release = ext4_release_file, .fsync = ext4_sync_file, … おっ。それっぽいな。 他のファイルシステムも同じだ。 これがインタフェースの役割か。
  160. 159 インタフェースっぽい実装 VFS を知らないとつらい

  161. 160 インタフェースっぽい実装 VFS を知らないとつらい 今日紹介した複雑な記述はなく 本質的な処理のみ で構成している

  162. VFS 161 課題 ファイルシステムを統一的に扱う 設計 オブジェクト指向を取り入れて インタフェースで表現 実装 構造体と手続き的な記述を駆使して インタフェースを実現

    妥協点 開発者が VFS を知っている前提
  163. Linuxカーネル 「わかっている人」が開発 162 ルールが弱くても迷わない 構文的に面倒なオブジェクト指向の要素は不要 「先頭に書く」など本質的でないルールは排除

  164. ライブラリ GObject, GStreamer とか かっちりルール ≃ わかりやすさ 163 がっつりオブジェクト指向 本来の処理とは無関係な記述が増える

  165. Cから見えてくる 設計=ルール のキモ 164

  166. 165 トレードオフ点を決める

  167. 166 妥協 理想 トレードオフ点を決める

  168. 簡潔さ 泥臭さ 167 トレードオフ点を決める

  169. モダンな言語はある程度決まっている C言語は自分で決める 168 トレードオフ点

  170. オレオレするなら 美しいデザインパターン は不要? 169

  171. オレオレするなら 美しいデザインパターン は不要? 170

  172. 守破離 171

  173. 守破離 172 今日紹介した例はこっち

  174. 守破離 173 こっちの理論がかなり強力

  175. 守破離 174 守れるなら守ったほうが気楽

  176. 175 デザインパターンを知る

  177. 正確な秤 を得る 176 デザインパターンを知る

  178. 177 美しい 泥臭い

  179. 178 美しい 泥臭い 理想 妥協 最終的には”理想”かもしれない

  180. その設計 本当に必要ですか? ※たぶん必要です 179

  181. その設計 本当に必要ですか? ※たぶん必要です 180

  182. その設計 本当に必要ですか? ※たぶん必要です 181

  183. 良い道具で 最高の設計を 182