ハブロフスク在住の皆さん、ご挨拶申し上げます。DaveCheneyブログの言及されたSOLID Go Design投稿の翻訳(個人的な意見による)をコミュニティと頻繁に共有することにしました。 おそらく誰かにとっては役に立つでしょう。
SOLID Goデザイン
この投稿は、2016年8月18日のGolangUKのメインレポートのテキストに基づいています。
パフォーマンスの記録はYouTubeで入手できます 。
世界中に何人の囲programmerプログラマーがいますか?
世界中に何人の囲programmerプログラマーがいますか? 数字を考えて、それを頭に入れておきます
会話の最後にこの質問に戻ります。
コードレビュー
作業の一環として、誰がここでコードをレビューしますか? (聴衆のほとんどが手を挙げており、これは励みになります)。 では、なぜコードレビューを行っているのですか? (誰かが「コードを改善するために」と叫ぶ)
悪いコードをキャッチするためにコードレビューが必要な場合、レビューしているコードが良いか悪いかをどのようにして知るのでしょうか?
「この絵画は美しい」または「この部屋は美しい」と言ったように、「このコードはひどい」または「うわー、このコードは美しい」と言ってもいいのですが、これらは主観的な概念であり、客観的な方法を探しています良いコードまたは悪いコードの特性について話します。
悪いコード
レビュー時に使用できる不良コードの特性は何ですか?
- 柔軟性がない 。 コードに柔軟性はありませんか? それは、変更を困難にするタイプとパラメーターの厳格なセットを含んでいますか?
- 壊れやすい 。 コードは壊れやすいですか? カオスはコードベースのわずかな変化を引き起こしますか?
- 動かない 。 コードのリファクタリングは難しいですか? サイクリックインポートの1回のキーストロークで実行されますか?
- 複雑な このコードは、コードのためだけに存在し、複雑すぎないのですか?
- 冗長 。 コードを酷使していますか? コードを見て、何をしようとしているのかを言うことができますか?
これらの言葉はポジティブですか? コードを確認しながらこれらの言葉を聞いて楽しんでいただけますか?
おそらくない。
良いデザイン
しかし、これは改善です。今では、「変更するのが難しすぎるので気に入らない」、「このコードが何をしようとしているのかわからないので気に入らない」などと言うことができますが、積極的な議論をするには?
悪いだけでなく客観的な用語で推論できるように、良いデザインの特性を説明する方法があれば素晴らしいと思いませんか?
固い
2002年、Robert Martinは著書「 アジャイルソフトウェア開発、原則、パターン、および実践」を出版しました。 その中で、彼は再利用可能なソフトウェア設計の5つの原則を説明しました。これは、SOLID原則と呼ばれ、その名前の略語です。
- 単独責任の原則
- 開放性/近接性の原理
- バーバラ・リスコフの代替原理
- インターフェース分離の原理
- 依存関係の逆転の原理
この本は少し時代遅れで、私たちが話している言語は約10年前に使用されていました。 しかし、SOLIDの原則には、適切に設計されたGoプログラムについて話す方法の手がかりを提供できるいくつかの側面があるかもしれません。
これがまさに今朝あなたと話したいことです。
単独責任の原則
SOLIDの最初の原則、これはS-共有責任の原則です。
クラスには、変更の理由が1つだけ必要です。
-ロバート・S・マーティン
Goにはクラスがまったく含まれておらず、それらの代わりに、より強力な構成の概念がありますが、クラスの概念の使用の歴史を見ると、ここに何らかの意味があると思います。
1つのコードに変更の理由が1つしかないことがなぜそれほど重要なのですか? さて、コードを変更できるという考えは苦痛ですが、コードが依存するコードも変更できるという事実よりもはるかに痛みが少ないです。 また、コードを変更する必要がある場合、特定の要件に従ってコードを変更する必要があり、付随的な損害の犠牲者になることはありません。
したがって、唯一のタスクを担当するコードには、変更を行う理由が少なくなります。
接続性とユニティ
プログラムを簡単に変更できることを表す2つの言葉は、つながりと統一性です。
接続性とは、ある場所での変更が別の場所での強制的な変更を意味する場合に、2つのコードの同時変更を記述する単純な概念です。
関連するが別個の概念であるこの統一は、相互に引き付ける力です。
ソフトウェアのコンテキストでは、ユニティは、コードのセクションが自然にどのように関連するかを記述するプロパティです。
Goのプログラムでの接続性と統一の原則の実装を説明するために、SRP(共有責任の原則)を議論するときによくあることですが、機能と方法について話すことができますが、すべてはGoのパッケージシステムで始まると思います。
パッケージ名
Goでは、すべてのコードはパッケージ内に存在し、適切なパッケージ設計はその名前から始まります。 パッケージ名は、その目的の説明と名前空間プレフィックスの両方です。 Go標準ライブラリの適切なパッケージ名の例は次のとおりです。
- net / http。httpクライアントとサーバーを提供します。
- os / exec 、外部コマンドを実行します。
- encoding / json。JSONドキュメントのエンコードとデコードを実装します。
独自のパッケージ内で別のパッケージのシンボルを使用する場合、これは2つのパッケージ間のソースレベルの接続を確立するimport
キーワードを使用して行われます。 今、彼らはお互いの存在について知っています。
悪いパッケージ名
ネーミングへのそのような焦点は、単なる教育ではありません。 名前が不適切なパッケージは、タスクがあったとしても、タスクを説明する機会を逃します。
package server
はどのような機会を提供しますか?.. package server
かもしれませんが、どのプロトコルで実装しますか?
package private
はどのような機会を提供しますか? 見るべきではないもの? 彼は公共のキャラクターさえ持っているべきですか?
そして、 package utils
パートナーのように、 package common
、他の悪意のある侵入者の隣にしばしば見られます。
そのようなパッケージを引き付けると、多くの責任があり、多くの場合理由なく変更されるため、コードはダンプに変わります。
GoのFilisofia UNIX
私の見解では、ダグラス・マックロイの「Philisophy of UNIX」の仕事に言及せずに、個別の設計の議論を完了することはできません。 多くの場合、元の作者によって提供されなかった、より大きな問題を解決するために組み合わされた小さく鋭いツール。 GoパッケージはUNIX哲学の精神を体現していると思います。 実際、各Goパッケージ自体は小さなGoプログラムであり、単一の責任を伴う単一の変更点です。
開放性/近接性の原理
2番目の原則Oは、1988年に次のように書いたBertrand Meyerの開放性/閉鎖性の原則です。
プログラムオブジェクトは、展開のために開かれ、変更のために閉じられる必要があります。
-Bertrand Meyer、オブジェクト指向ソフトウェアの構築
このヒントは、21年前に作成された言語にどのように適用されましたか?
package main type A struct { year int } func (a A) Greet() { fmt.Println("Hello GolangUK", a.year) } type B struct { A } func (b B) Greet() { fmt.Println("Welcome to GolangUK", b.year) } func main() { var a A a.year = 2016 var b B b.year = 2016 a.Greet() // Hello GolangUK 2016 b.Greet() // Welcome to GolangUK 2016 }
year
フィールドとGreet
メソッドを持つタイプA
あります。 A
はB
のフィールドとして埋め込まれ、 B
は独自のGreet
メソッドを提供し、 A
同じメソッドを非表示にA
ため、 A
が埋め込まれた2番目のタイプB
には、メソッド呼び出しB
がメソッド呼び出しA
オーバーライドします
ただし、インライン化はメソッドだけでなく、組み込み型フィールドへのアクセスも提供します。 ご覧のとおり、 A
とB
両方とも同じパッケージで定義されているため、 B
はA
のyear
のプライベートフィールドにアクセスできますB
したがって、埋め込みは、Goの型を拡張に開放できる強力なツールです。
package main type Cat struct { Name string } func (c Cat) Legs() int { return 4 } func (c Cat) PrintLegs() { fmt.Printf("I have %d legs\n", c.Legs()) } type OctoCat struct { Cat } func (o OctoCat) Legs() int { return 5 } func main() { var octo OctoCat fmt.Println(octo.Legs()) // 5 octo.PrintLegs() // I have 4 legs }
この例では、 Legs
メソッドを使用してLegs
数をカウントできるCat
タイプがあります。 このタイプのCat
を新しいタイプのOctoCat
、 OctocatS
は5つの脚があることを宣言します。 そうすることで、 OctoCat
は独自のLegs
メソッドを定義し、 PrintLegs
メソッドがPrintLegs
5を返し、4を返します。
これは、 PrintLegs
Cat
タイプ内PrintLegs
定義されているためPrintLegs
。 これは、 Cat
を受信機として受け入れ、 Cat
タイプのLegs
メソッドを指します。 Cat
は、それが埋め込まれた型を知る必要があるため、埋め込みによってメソッドを変更することはできません。
ここから、Goの型は拡張用に開か れ、変更用に閉じられて いると言えます。
実際、Goのメソッドは、定義済みの仮パラメーターを使用した関数の構文糖衣以上のものであり、レシーバーです。
func (c Cat) PrintLegs() { fmt.Printf("I have %d legs\n", c.Legs()) } func PrintLegs(c Cat) { fmt.Printf("I have %d legs\n", c.Legs()) }
レシーバーは、関数の最初のパラメーターである渡されたものであり、Goは関数のオーバーロードをサポートしていないため、 OctoCat
通常のCats
型と互換OctoCat
ません。 それは次の原則に私をもたらします。
バーバラ・リスコフの代替原理
Barbara Liskovによって発明されたLiskyの置換原理では、呼び出し側が違いを判断できない動作を示す場合、2つのタイプは互換性があるとされています。
クラスベースの言語では、Liskの置換原則は、多くの場合、さまざまな特定のサブタイプを持つ抽象クラスの仕様として解釈されます。 しかし、Goにはクラスや継承がないため、抽象クラスの階層の観点から置換を実装することはできません。
インターフェース
代わりに、置換はGoのインターフェイスの能力です。 Goでは、特定のインターフェイスを実装するために型は必要ありませんが、その代わりに、実装するすべての型のインターフェイスは、シグネチャがインターフェイス宣言と一致するメソッドを含むだけです。
Goでは、インターフェイスは明示的な一致ではなく暗黙的に満たされ、これが言語での使用方法に大きな影響を与えると言います。
適切に設計されたインターフェイスは、おそらく小さなインターフェイスです。 主なイディオムは、インターフェイスに含まれるメソッドが1つだけであることです。 それ以外のことを行うのは難しいので、小さなインターフェイスに単純な実装が含まれることは論理的です。 パッケージが通常の振舞いに関連している簡単な実装への妥協ソリューションであるということから続きます。
io.Reader
type Reader interface { // Read reads up to len(buf) bytes into buf. Read(buf []byte) (n int, err error) }
これにより、お気に入りのGoインターフェイスであるio.Reader
できます。
io.Reader
インターフェースio.Reader
非常にシンプルです。 Read
は、指定されたバッファーにデータRead
読み取り、読み取られたバイト数と読み取り中に発生する可能性のあるエラーを呼び出し元のコードに返します。 シンプルに見えますが、非常に強力です。
io.Reader
はバイトストリームとして表現できるものを扱うため、文字通り何でもリーダーオブジェクトを構築できます。 定数文字列、バイト配列、標準入力ストリーム、ネットワークストリーム、gzip tarアーカイブ、標準出力ストリーム、またはsshを介してリモートで実行されるコマンド。
そして、これらの実装はすべて、1つの単純な契約を満たすため、交換可能です。
したがって、Lisk置換の原則はGoにも適用され、言われたことは、故ジムワイリッヒの素晴らしい格言によって要約できます。
これ以上要求することはありません。
-ジム・ワイリッヒ
これは、第4のSOLID原則への大きな移行です。
インターフェース分離の原理
4番目の原則は、インターフェイスの分離の原則であり、次のように解釈されます。
クライアントは、使用しないメソッドに依存することを強制されるべきではありません。
-ロバート・S・マーティン
Goでは、インターフェイス分離の原則の適用は、その機能を実行するために必要な機能の動作を分離するプロセスとして理解できます。 具体的な例として、 Document
構造をディスクに保存する関数を作成するタスクがあるとします。
// Save writes the contents of doc to the file f. func Save(f *os.File, doc *Document) error
そのような関数を定義できます。 Save
と呼びましょう。提供されたDocument
を書き込むためのソースとして*os.File
を取りSave
。 しかし、ここにはいくつかの問題があります。
Save
署名により、ネットワーク上のどのアドレスにデータを書き込むことができなくなります。 ネットワークストレージが将来必要になる可能性があり、この関数のシグネチャが変更される可能性があり、それがそれを呼び出すすべての人に影響すると仮定します。
Save
はディスク上のファイルを直接操作するため、テストはかなり不快です。 操作を検証するには、テストは書き込み後にファイルの内容を読み取る必要があります。 さらに、テストでは、 f
一時ストレージに書き込まれ、その後は常に削除されることを確認する必要f
ます。
*os.File
は、ディレクトリの読み取りやパスがシンボリックリンクであるかどうかの確認など、 Save
に関連しない多くのメソッドも定義しSave
。 Save
関数の署名が、タスクに関連する*os.File
部分によってのみ記述されている場合に*os.File
ます。
これらの問題で何ができますか?
// Save writes the contents of doc to the supplied ReadWriterCloser. func Save(rwc io.ReadWriteCloser, doc *Document) error
io.ReadWriteCloser
を使用すると、インターフェイスの分離の原則を適用してSave
をオーバーライドし、より一般的なファイル操作を記述するインターフェイスを受け入れることができます。
これらの変更により、 io.ReadWriteCloser
インターフェースを実装するタイプは、以前の*os.File
置き換えることができます。 これにより、 Save
のアプリケーションの幅が広がり、 Save
*os.File
呼び出し側に、タイプ*os.File
メソッドが必要な操作に関連するかを説明しSave
。
Save
の作成者として、 io.ReadWriteCloser
インターフェイスの背後に隠されているため、 *os.File
からすべての無関係なメソッドを呼び出すことはできません。 しかし、インターフェイス分割メソッドを使用してもう少し先に進むことができます。
まず、 Save
が単独の責任の原則に従う可能性は低く、コンテンツをチェックするために書き込んだファイルを読み取ります。これはコードの別の部分の責任です。 そのため、ファイルを開いたり閉じたりする前に、 Save
排他的に渡すインターフェイスの仕様を絞り込むことができます。
// Save writes the contents of doc to the supplied WriteCloser. func Save(wc io.WriteCloser, doc *Document) error
第二に、 Save
、通常のファイルメカニズムのように見せるために継承したストリームクローズメカニズムを提供することにより、 wc
がどのような状況でクローズされるかという疑問が生じます。 おそらく、 Save
は条件なしでClose
を呼び出すか、成功するとClose
を呼び出しClose
。
文書が既に記録された後にストリームに追加情報を追加したい場合があるため、これはすべて、呼び出しSave
にとって課題です。
type NopCloser struct { io.Writer } // Close has no effect on the underlying writer. func (c *NopCloser) Close() error { return nil }
大まかな解決策は、 io.Writer
を埋め込み、 Close
メソッドをオーバーライドする新しい型を定義して、閉じたメインスレッドからSave
が呼び出されないようにすることです。
しかし、 NopCloser
は実際には何もカバーしていないため、これはBarbara Liskovの代替原則に違反する可能性があります。
// Save writes the contents of doc to the supplied Writer. func Save(w io.Writer, doc *Document) error
より良い解決策は、 Save
をオーバーライドしてio.Writer
のみをio.Writer
、データをストリームに書き込む以外の機能を完全に奪うことです。
ただし、 Save
関数にインターフェイスを分割するという原則を適用すると、結果は同時に要件の条件に最も固有の関数になります。必要なのは書き込むものだけで、この関数で最も重要なのはSave
toを使用できることですio.Writer
インターフェースがio.Writer
いる場所にデータを保存します。
Goの重要な経験則は、インターフェイスを受け入れて構造を返すことです。
-ジャック・リンダムード
上記の引用は、ここ数年でGoの精神に漏れ込んでいる興味深いミームです。
このバージョンでは、標準ツイートの一部として、1つのニュアンスが欠落しており、これはJackのせいではありませんが、Go言語デザインが登場する主な理由の1つだと思います。
依存関係の逆転の原則
最後のSOLID原則は、依存関係の逆転の原則です。
最上位モジュールは、下位モジュールに依存するべきではありません。 両方のレベルは抽象化に依存する必要があります。
抽象化はその詳細に依存するべきではありません。 詳細は抽象化に依存する必要があります。
-ロバート・S・マーティン
しかし、Goプログラマにとって、依存関係の反転は実際には何を意味するのでしょうか?
これまでに説明したすべての原則を適用する場合、コードは、それぞれが単一の明確に定義された依存関係または目的を持つ個別のパッケージに既に配置されている必要があります。 コードは、インターフェイスの観点から依存関係を記述し、これらのインターフェイスは、これらの関数が必要とする動作のみを記述することを目的とする必要があります。 つまり、多くの作業が残ってはいけません。
私が想像するように、ここでマーティンが話しているのは、主にGoのコンテキストで、インポートグラフの構造です。
Goでは、インポートグラフは非周期的である必要があります。 非周期性を無視しようとすると、コンパイルエラーが発生しますが、アーキテクチャでさらに深刻なエラーが発生する場合があります。 他のすべてが等しい場合、適切に設計されたGoプログラムのインポートグラフは、高くて狭くなるのではなく、広くて比較的平らでなければなりません。 別のパッケージの支援なしでは機能を実行できないパッケージがある場合、これはパッケージの境界が明確に定義されていないことを示す信号である可能性があります。
依存関係の反転の原則により、インポート列で特定の責任を可能な限り高いレベルでmain
パッケージまたはプロセッサの上位レベルに移し、下位レベルのコードを抽象化およびインターフェイスで動作させることができます。
SOLID Goデザイン
要約すると、SOLIDの各原則がGoに適用される場合、それらは強力な設計ツールですが、一緒に使用されると、主要なテーマになります。
共有責任の原則により、関数、型、およびメソッドを自然に関連するパッケージに構造化することが奨励されます。 タイプと機能は一緒に単一の目的を果たします。
開放性/閉鎖性の原則は、シェイクを使用して単純なタイプとより複雑なタイプを妥協することを奨励します。
Barbara Liskovの代替原則は、特定のタイプではなく、インターフェースの観点からパッケージ間の依存関係を表現することを奨励しています。 小さなインターフェイスを定義することで、実装が契約を満たせると確信できます。
インターフェイスの分離の原則はこの考えを継続し、必要な動作のみに依存する関数とメソッドを定義することをお勧めします。 関数が単一のメソッドを持つインターフェース型パラメーターのみを必要とする場合、これらの関数は単一の責任を持っている可能性が高いです。
クレジットの反転の原則は、コンパイル段階からパッケージの依存関係に関する知識を転送することをお勧めします。Goでは、特定のパッケージで使用されるインポートの数をコード実行の段階に減らすことでこれを観察します。
この会話を要約したい場合、最も可能性の高いものは次のとおりです。 インターフェイスにより、GoプログラムでSOLIDの原則を適用できます 。
インターフェイスにより、Goプログラマは特定の実装ではなく、パッケージの機能を説明できます。 疎結合されたコードは変更が容易であるため、これらはすべて「切断」と言う別の方法です。これが目標です。
サンディ・メッツが述べたように:
デザインとは、 今日機能し 、 常に簡単に変更できるコードを整理する技術です。
サンディメッツ
Goが長期的に企業が投資する言語になることを計画している場合、その決定の重要な要素は、Goコードの保守の容易さと変更の容易さであるためです。
おわりに
結論として、この会話を開いた質問に戻りましょう。 世界中に何人の囲programmerプログラマーがいますか? 私の推測は次のとおりです。
2020年には、Goに500,000人の開発者がいます。
-デイブ・チェイニー
50万人のGoプログラマが時間をかけて何をしますか? まあ、明らかに、彼らはGoで多くのコードを書くでしょう、そして正直なところ、すべてのコードが良いわけではなく、コードの一部が悪いでしょう。
私が残酷になろうとしているわけではないことを理解してください。しかし、この部屋にいる皆さんは、他の言語、あなたがGoに来た言語で開発した経験があります。
C ++には、はるかにクリーンで進化した言語が登場しようとしています。
-BjörnStraustrup、Design and Evolution C ++
私たちの言語がすべてのプログラマーのために成功するのを支援する能力は、今日C ++について冗談を言うときに人々が話し始めるような混乱を作成しないことです。
他の言語が肥大化しており、冗長で、かつ過負荷であるために他の言語をからかうストーリーはGoに適用できますが、これが発生したくないので、リクエストがあります。
Goプログラマーは、フレームワークについて話すのをやめ、設計についてもっと話し始める必要があります。 すべてのコストで生産性に集中するのをやめ、代わりにすべてのコストで再利用しないことに集中する必要があります。
今日は、選択肢や制限に関係なく、人々が私たちが持っている言語をどのように使用してソリューションを作成し、実際の問題を解決するかについてどのように話すかを見てみたいと思います。
今日は、よく設計され、切断され、再利用され、変化に対応するようにGoプログラムを設計することについて人々がどのように話しているのかを聞きたいと思います。
...および別の詳細
現在、非常に多くの人々がこのような優れたスピーカーの構成を聞くようになったのは素晴らしいことですが、現実は、この会議がどれだけ成長しても、彼の生涯を通じてGoを使用する人の総数に比べて、私たちはほんのわずかです一部。
したがって、私たちの仕事は、他のどの国に良いソフトウェアを書くべきかを伝えることです。 優れたソフトウェアで、互換性があり、変更可能で、Goを使用してそれを行う方法を示します。 そして、それはあなたから始まります。
デザインについて話を始めてください。ここで紹介したアイデアを使用してください。独自の研究を行い、これらのアイデアをプロジェクトに適用してください。 それから私はあなたにしたい:
-これについてのブログ投稿を書きました。
-セミナーで行ったことを教えてください。
-学んだことに関する本を書く。
そして来年この会議に戻って、あなたが達成したことについて教えてください。
これらすべてを実行することにより、Goの開発者のエコシステムを作成し、作業を継続するように設計されたプログラムを管理できます。
ありがとう