いわゆる「横断的関心事」は、SOLID / DI / IoCを破る有効な言い訳ですか?


43

私の同僚は、「ロギング/キャッシングなどが横断的な関心事である」と言ってから、対応するシングルトンをどこでも使用して進めます。それでも、彼らはIoCとDIが大好きです。

SOLI Dの原則を破ることは本当に正当な言い訳ですか?


27
とにかく、SOLIDの原則は、知らないうちに従うのに必要な経験を既に持っている人にのみ有用であることがわかりました。
svidgen

1
「横断的関心事」という用語はかなり特定の用語であり(側面に結び付けられている)、必ずしもシングルトンと同義語ではないことがわかりました。ロギングは、同じ方法で複数の場所に一般的なメッセージを記録したいという意味で横断的関心事になる場合があります(たとえば、呼び出しを行ったサービスメソッドへのすべての呼び出しまたは他の監査タイプのログを記録します)。ただし、ログは(一般的な意味で)あらゆる場所で使用されているという理由だけで横断的な関心事ではないと主張します。これは実際には標準的な用語ではありませんが、これは「ユビキタスな関心事」です。
ペース

4
@svidgen the SOLIDの原則は、それらを知らない人があなたのコードをレビューし、なぜあなたがそのようにしたのかを尋ねるときにも役立ちます。原則を指摘し、「それが理由だ」と言うことができてうれしいです
-candied_orange

1
ロギングが特別なケースだと思う理由はありますか?通常、使用しているXフレームワークの一部にデータをリダイレクトするのは非常に単純なチャネルです。
ksiimson

回答:


42

番号。

SOLIDは、避けられない変化を説明するためのガイドラインとして存在します。ロギングライブラリ、ターゲット、フィルタリング、フォーマットなどを実際に変更することはありませか?キャッシングライブラリ、ターゲット、戦略、スコーピングなどを実際に変更するつもりはありませか?

もちろんそうですね。で非常に少なくとも、あなたはテストのためにそれらを分離するためにまともな方法でこれらの事をモックとしたいとしています。また、テストのためにそれらを分離する場合は、実際の理由で分離するというビジネス上の理由に直面する可能性が高くなります。

そして、ロガー自体が変更を処理するという引数を取得します。「ああ、ターゲット/フィルタリング/フォーマット/戦略が変更されたら、設定を変更するだけです!」それはゴミです。これらすべてを処理するGodオブジェクトがあるだけでなく、静的分析を取得しないXML(または同様の)でコードを記述しているため、コンパイル時エラーが発生せず、本当に効果的な単体テストを取得できます。

SOLIDガイドラインに違反するケースはありますか?絶対に。場合によっては(とにかく完全な書き換えを必要とせずに)変化しないことがあります。LSPのわずかな違反が最もクリーンなソリューションである場合があります。孤立したインターフェースを作成しても価値がない場合があります。

しかし、ロギングとキャッシング(およびその他のユビキタスな分野横断的な懸念事項)はそうではありません。これらは通常、ガイドラインを無視した場合に発生するカップリングと設計の問題の優れた例です。


22
その場合、どのような選択肢がありますか?書くすべてのクラスにILoggerを注入しますか?ロガーを必要とするすべてのクラスのデコレーターを作成しますか?
ロバートハーベイ

16
うん。依存関係がある場合は、それを依存関係にします。そして、それがあまりにも多くの依存関係を意味する場合、またはロガーを行かないはずの場所に渡している場合- 良い、今、あなたはあなたの設計を修正することができます。
テラスティン


20
私が独断的なIoCで抱えている本当の問題は、ほとんどすべての最新のコードが、注入も抽象化もされていないものに依存しているということです。たとえば、C#を記述する場合、モジュールを抽象化しStringたり、モジュールから除外しInt32たりすることはあまりありませんList。変化を計画するのが合理的かつ正気である程度だけです。そして、ほとんど明白な「コア」タイプを超えて、おそらくあなたが変えるかもしれないものを見分けることは、まさに経験と判断の問題です。
svidgen

6
@svidgen-独断的なIoCを提唱する私の答えを読んでいないことを願っています。そして、これらのコアタイプ(および賢明な場合は他のもの)を抽象化しないからといって、グローバルなList / String / Int / etcを用意しても大丈夫というわけではありません。
テラスティン

38

はい

これが「横断的関心事」という用語の要点です。これは、SOLID原則にきちんと適合しないものを意味します。

理想主義と現実が出会うところです。

半ばソリッドで横断的である人々は、しばしばこの精神的な挑戦に直面します。大丈夫、びっくりしないでください。すべてをSOLIDの観点に入れるよう努めますが、SOLIDが意味をなさないログやキャッシュなどの場所がいくつかあります。クロスカットはSOLIDの兄弟であり、手をつないで行きます。


9
理にかなった、実用的で非教義的な答え。
user1936

11
5つのSOLID原則すべてが分野横断的な懸念によって破られる理由を教えてください。それを理解するのに問題があります。
ドックブラウン

4
うん。私は、各原則を個別に「破る」正当な理由を潜在的に考えることができました。しかし、5つすべてを壊す理由は1つではありません。
svidgen

1
単体テストのためにシングルトンロガー/キャッシュをモックする必要があるたびに頭を壁にぶつける必要はないのが理想です。HttpContextBase(まさにこの理由で導入された)なしでWebアプリケーションを単体テストすることを想像してください。このクラスがなければ私の現実は本当にすっぱいものになることは確かです。
-devnull

2
問題はむしろ、横断的関心事そのものよりも、それから...だと多分@devnull
ボリス・スパイダー

25

ロギングについてはそうだと思います。ロギングはある普及とサービス機能に一般的に無関係。ロギングフレームワークのシングルトンパターンを使用することは一般的であり、よく理解されています。そうしないと、あらゆる場所にロガーを作成して注入することになり、それは望ましくありません。

上記の問題の1つは、誰かが「しかし、どのようにロギングをテストできますか?」と言うことです。。私は、ログファイルを実際に読み取って理解できると断言する以外に、通常はロギングをテストしないと考えています。私は、ロギングがテスト見てきたときにクラスが実際にしたことを主張するために誰かを必要とするため、それが通常だ行って何かをして、彼らはそのフィードバックを得るために、ログメッセージを使用しています。むしろ、そのクラスにリスナー/オブザーバーを登録し、テストで呼び出されることをアサートします。その後、イベントロギングをそのオブザーバー内に配置できます。

ただし、キャッシュはまったく異なるシナリオだと思います。


3
FPに少し手を出してから、(おそらく単項の)関数合成を使用して、ロギングやエラー処理などをコアビジネスロジックに入れずにユースケースに埋め込むことに大きな関心を寄せるようになりました(fsharpforfunandprofit.com/ropを参照)
サラ

5
ロギング普及してなりません。もしそうなら、あなたはあまりにも多くのログを取りすぎており、実際にそれらのログを見に行く必要があるときに、あなた自身のためにノイズを作成しているだけです。ロガーの依存関係を注入することにより、実際に何かをログに記録する必要があるかどうかを考える必要があります。
ラバーダック

12
@RubberDuck-フィールドからバグレポートを取得したときに「ロギングが広範であってはならない」ことを教えてください。教訓は、ロギングは「広範」で非常に広範であるべきです。
ダンク

15
@RubberDuck:サーバーソフトウェアでは、ロギングが生き残るための唯一の方法です。自分のラップトップではなく、12時間前に発生したバグを把握する唯一の方法です。ノイズを心配しないでください。ログをクエリできるようにログ管理ソフトウェアを使用します(理想的には、エラーログでメールを送信するアラームも設定する必要があります)
-slebetman

6
サポートスタッフから電話があり、「お客様がいくつかのページをクリックしていて、エラーが表示された」と言われました。私は彼らにエラーが何を言ったかを尋ねます、彼らは知りません。顧客が具体的に何をクリックしたかを尋ねますが、彼らは知りません。私は彼らが繁殖できるかどうか尋ねます。ロギングは、いくつかの重要なヒンジポイントで適切に展開されたロガーを使用してほとんど達成できることに同意しますが、ここにある奇妙なメッセージも非常に貴重です。ロギングの恐怖は、ソフトウェアの品質低下につながります。データが多すぎる場合は、プルーニングしてください。時期尚早に最適化しないでください。
ペース

12

私の2セント...

はいといいえ。

あなたは決してすべきである本当にあなたが採用の原則に違反していません。しかし、あなたの原則は常に微妙であり、より高い目標に奉仕するために採用されるべきです。したがって、適切に条件付けられた理解では、明らかな違反の一部は、「精神」または「原則全体」の実際の違反ではない可能性があります。

特に、SOLIDの原則は、多くの微妙なニュアンスを必要とすることに加えて、最終的には「機能する保守可能なソフトウェアを提供する」という目標に従属します。したがって、SOLIDの特定の原則を順守することは、SOLIDの目標と矛盾する場合、自己破滅的かつ自己矛盾的です。そして、ここで、私はしばしば、配信が保守性よりも優れていることに注意します。

それでは、SOLIDDはどうですか?まあ、それはあなたの再利用可能なモジュールをそのコンテキストに比較的寛容にすることによって、メンテナンス性の向上に貢献します。また、「再利用可能なモジュール」を「別の異なるコンテキストで使用する予定のコードのコレクション」として定義できます。そして、それは単一の関数、クラス、クラスのセット、およびプログラムに適用されます。

もちろん、ロガーの実装を変更すると、モジュールが「別の個別のコンテキスト」になる可能性があります。

それで、2つの大きな注意点を申し上げます。

まず 、「再利用可能なモジュール」を構成するコードブロックの周りに線を引くことは、専門家の判断の問題です。そして、あなたの判断は必然的にあなたの経験に限定されます。

現在、別のコンテキストでモジュールを使用する予定がない場合は、そのモジュールに無力に依存しても大丈夫でしょう。警告への警告:あなたの計画はおそらく間違っている-しかしそれはまた良い。モジュールを次々と書くほど、「いつかまたこれが必要になる」という感覚がますます直感的で正確になります。しかし、おそらく、「すべてを可能な限りモジュール化して切り離しましたが、過剰なことはありません」と振り返ることはできないでしょう。

判断の誤りに罪悪感を覚えたら、告白に進んでください...

第二に、 反転制御依存性の注入とは異なります。

これは、依存関係を吐き出し始めたときに特に当てはまります。依存性注入は、包括的なIoC戦略に役立つ戦術です。しかし、私は、DIがであることを主張したい低い有効性インターフェイスとアダプタを使用してのような- -いくつかの他の戦術よりも単一モジュール内からコンテキストへの曝露の点。

そして、これに少し焦点を当てましょう。なぜなら、あなたが広告nauseamを注入したとしても、インターフェースに対してコードを書く必要があるからです。異なる順序でパラメータを受け取る異なるベンダーの新しいものを使用し始めることはできませんでした。この機能は、モジュール内に存在、依存関係を管理するための単一のサブモジュール(アダプター)を内部に持つインターフェースに対する、モジュール内のコーディングからもたらされます。Logger LoggerLogger

また、アダプタに対してコーディングしている場合、Loggerそのアダプタにインジェクトされるか、アダプタによって検出されるかは、一般に、全体的な保守性の目標にはほとんど関係ありません。さらに重要なことは、モジュールレベルのアダプターを使用している場合、おそらくそれを何かに挿入するのはばかげていることです。モジュールに書かれいます。

tl; dr-原則を使用している理由を考慮せずに、原則について悩まないでください。そして、より現実的には、モジュールごとにをビルドするだけAdapterです。「モジュール」の境界線をどこに描くかを決めるときは、判断してください。各モジュール内から、を直接参照してくださいAdapter。そして、注入、必ず実際のロガーにAdapter-しかし、ではない、それを必要とするかもしれないあらゆる小さい事に。


4
男...私は短い答えを与えたでしょうが、時間がありませんでした。
svidgen

2
アダプターを使用する場合は+1。可能な限り置き換えられるようにするには、サードパーティのコンポーネントの依存関係を分離する必要があります。ロギングはこのための簡単なターゲットです。多くのロギング実装が存在し、ほとんどが類似のAPIを持っているため、単純なアダプター実装により、それらを非常に簡単に変更できます。そして、あなたがあなたのロギングプロバイダーを決して変えないと言う人々に:私はこれをしなければならなかった、そしてそれは私がアダプターを使用していなかったので、それは本当の痛みを引き起こした。
ジュール

「特にSOLIDの原則は、多くのニュアンスを必要とすることに加えて、最終的には「機能する、メンテナンス可能なソフトウェアを提供する」という目標に従属します。」軽度のOCDコーダーとして、私は理想主義と生産性との闘いの分け前を持っていました。
DVK

古い回答に対する+1またはコメントを見るたびに、もう一度回答を読んで、自分がひどく混乱していて不明瞭な作家であることを再発見します... @DVKのコメントに感謝します。
svidgen 16

アダプターは命を救うものであり、特にコストがかからず、特にSOLIDを適用したい場合は、人生がずっと楽になります。テストは簡単です。
アラン

8

ロギングは常にシングルトンとして実装されるべきであるという考えは、多くの場合それが牽引力を得たと言われている嘘の1つです。

最新のオペレーティングシステムに関する限り、出力の性質に応じて複数の場所にログを記録することをお勧めします

システム設計者は、新しいソリューションに盲目的に組み込む前に、過去のソリューションの有効性に絶えず疑問を呈する必要があります。彼らがそのような勤勉さを実行していなければ、彼らは仕事をしていない。


2
では、提案されたソリューションは何ですか?あなたが書くすべてのクラスにILoggerを注入していますか?デコレータパターンの使用はどうですか?
ロバートハーベイ

4
今は本当に私の質問に答えていませんか?私はパターンを単に例として述べました...あなたがより良い何かを考えているならば、それはそれらである必要はありません。
ロバートハーヴェイ

8
私は本当に任意の具体的な提案の観点から、この答えを理解していない
ブライアンアグニュー

3
@RobbieDee-あなたは、複数の場所にログを記録したい「オフチャンス」によって、最も複雑で不便な方法でロギングを実装すべきだと言っていますか?そのような変更が発生した場合でも、Loggerを使用するかどうかを決定するときに、クラス間でインターフェイスを変更してLoggerを渡すためのすべての努力よりも、既存のLoggerインスタンスにその機能を追加するのは本当に大変だと思いますかその複数の場所のログが決して発生しない数十のプロジェクトですか?
ダンク

2
Re:「出力の性質に応じて複数の場所にログを記録したい場合があります」:もちろん、それを処理する最善の方法は複数の別個のロギング依存関係を挿入するのではなく、ロギングフレームワークです。(一般的なロギングフレームワークはすでにこれをサポートしています。)
ruakh

4

本当にログを記録するのは特別な場合です。

@Telastynの書き込み:

ロギングライブラリ、ターゲット、フィルタリング、フォーマットなどを実際に変更することはありませんか?

ロギングライブラリを変更する必要があると予想される場合は、ファサードを使用する必要があります。つまり、Javaの世界にいるならSLF4Jです。

残りについては、適切なロギングライブラリが、ロギングの場所、フィルタリングされるイベント、ロガー構成ファイルおよび(必要に応じて)カスタムプラグインクラスを使用したログイベントのフォーマット方法の変更を処理します。多数の市販の代替品があります。

要するに、これらはロギングのために解決された問題です...したがって、それらを解決するために依存性注入を使用する必要はありません。

DIが(標準のログ方法よりも)有益である唯一のケースは、アプリケーションのログを単体テストの対象にする場合です。ただし、ほとんどの開発者は、ロギングはクラス機能の一部ではなく、テストが必要なものではないと言うでしょう。

@Telastynの書き込み:

そして、ロガー自体が変更を処理するという引数を取得します。「ああ、ターゲット/フィルタリング/フォーマット/戦略が変更されたら、設定を変更するだけです!」それはゴミです。これらすべてを処理するGodオブジェクトがあるだけでなく、静的分析を取得しないXML(または同様の)でコードを記述しているため、コンパイル時エラーが発生せず、本当に効果的な単体テストを取得します。

私はそれが非常に理論的なリポストであることを恐れています。実際には、ほとんどの開発者とシステムインテグレーターは、設定ファイルを介してロギングを設定できるという事実を好みます。そして、彼らはモジュールのロギングを単体テストすることを期待されていないという事実を好みます。

確かに、ロギング設定を詰め込んだ場合、問題が発生する可能性がありますが、それらは起動時にアプリケーションが失敗するか、ロギングが多すぎる/少なすぎるとして現れます。1)これらの問題は、構成ファイルの間違いを修正することで簡単に修正できます。2)別の方法は、ログレベルを変更するたびに、完全なビルド/分析/テスト/展開サイクルを実行することです。それは受け入れられません。


3

はい いいえ

はい:すべてのサブシステムが同じ共通共有シングルトンに依存するのではなく、異なるサブシステム(またはセマンティックレイヤーまたはライブラリ、またはモジュールバンドリングの他の概念)がそれぞれ初期化中に(同じまたは)潜在的に異なるロガーを受け入れることが合理的だと思います。

しかしながら、

いいえ:同時に、すべての小さなオブジェクトのロギングをパラメーター化することは(コンストラクターまたはインスタンスメソッドによって)理不尽です。不要で無意味な肥大化を避けるために、より小さなエンティティは、それらを囲むコンテキストのシングルトンロガーを使用する必要があります。


これは、レベルのモジュール性を考える多くの理由の1つです。メソッドはクラスにバンドルされ、クラスはサブシステムやセマンティックレイヤーにバンドルされます。これらの大きなバンドルは、価値のある抽象化ツールです。モジュールの境界内では、それらを越える場合とは異なる考慮事項を与える必要があります。


3

まず、強力なシングルトンキャッシュで始まり、次に表示されるのは、グローバルステート、非記述的なclassesのAPI、およびテストできないコードを導入するデータベースレイヤーの強力なシングルトンです。

データベースにシングルトンを使用しないことにした場合、キャッシュにシングルトンを使用することはおそらく良い考えではありません。

クラスでシングルトンを使用すると、特定の量の依存関係を持つクラスが、理論的には無限の数を持つクラスに変わります。これは、静的メソッドの背後に実際に隠されているものがわからないためです。

過去10年間にプログラミングに費やしてきたのは、ロギングロジック(シングルトンとして記述されていた)を変更しようとする試みを目の当たりにしたケースは1つだけでした。そのため、依存関係の注入は大好きですが、ロギングはそれほど大きな関心事ではありません。一方、キャッシュは、常に依存関係として作成します。


はい、ロギングはめったに変更されず、通常は交換可能なモジュールである必要はありません。しかし、かつてロギングヘルパークラスの単体テストを試みましたが、これはロギングシステムに静的に依存していました。テスト可能にするための最も簡単なメカニズムは、テスト対象のクラスを別のプロセスで実行し、そのロガーをSTDOUTに書き込むように構成し、テストケースでその出力を解析することであると判断しましたಠ_clock d明らかに、リアルタイム以外は何も欲しくないのですよね?...もちろん、タイムゾーン/ DSTのエッジケースをテストする場合を除き
アモン

@amon:クロックは、DIと同じ目的を果たす別のメカニズム、つまりJoda-Timeとその多くのポートが既にあるという点で、ログインに似ています。(代わりにDIを使用していないがあることを何も間違っている、しかし、それはカスタム注射用アダプターを書こうとするよりも、直接ジョダタイムを使用するように簡単です、と私はそう誰も遺憾の意を見たことがありません。)
ruakh

@amon「もちろん、リアルタイム以外の時間を必要としない時計についても同様の経験がありますか?もちろん、タイムゾーン/ DSTエッジケースをテストする場合を除いて...」-またはバグに気付いたときデータベースを破損し、それを取り戻す唯一の希望は、イベントログを解析し、最後のバックアップから開始することです...しかし、突然、現在のログエントリではなく現在のログエントリのタイムスタンプに基づいてすべてのコードが動作する必要があります時間。
ジュール

3

はい、いいえ、しかしほとんどはいいえ

会話の大部分は、静的インスタンスと注入されたインスタンスに基づいていると思います。ロギングが私が想定しているSRPを破ることを提案している人はいないでしょうか?私たちは主に「依存性反転の原理」について話している。Tbh私は、Telastynの無回答にほぼ同意します。

いつ静的を使用しても大丈夫ですか?明らかにそれは時々大丈夫だからです。「はい」の回答は抽象化のメリットを示し、「いいえ」の回答はそれらがあなたが支払うものであることを示します。仕事が難しい理由の1つは、書き留めてすべての状況に適用できる答えが1つもないことです。

取る: Convert.ToInt32("1")

私はこれを好む:

private readonly IConverter _converter;

public MyClass(IConverter converter)
{
   Guard.NotNull(converter)
   _converter = conveter
}

.... 
var foo = _converter.ToInt32("1");

どうして? 変換コードを交換する柔軟性が必要な場合は、コードをリファクタリングする必要があることを受け入れています。私はこれをあざけることができないことを受け入れています。シンプルさと簡潔さはこの取引の価値があると思います。

場合は、スペクトルのもう一方の端を見ていIConverterSqlConnection、私はかなり静的呼び出しとしてそれを見て恐怖されるだろう。その理由は明らかです。私SQLConnectionは、アプリケーションでa がかなり「横断的」である可能性があることを指摘したいと思います。そのため、これらの正確な単語は使用しませんでした。

以上のようにログインしていますSQLConnectionConvert.ToInt32「SQLConnection」のようなものだと思います。

Loggingをモックする必要があります。外の世界と話します。を使用してメソッドを作成するときConvert.ToIn32、クラスの別のテスト可能な出力を計算するツールとして使用しています。Convert「1」+「2」==「3」であることを確認するときに、正しく呼び出されたことを確認する必要はありません。ロギングは異なり、クラスの完全に独立した出力です。私はそれがあなた、サポートチーム、そしてビジネスにとって価値のあるアウトプットだと思っています。ロギングが正しくない場合、クラスは機能しません。そのため、ユニットテストはパスしません。クラスのログをテストする必要があります。これは致命的な議論だと思います。本当にここでやめることができます。

私もそれが変わる可能性が高いと思います。優れたロギングは文字列を出力するだけでなく、アプリケーションが実行していることのビューです(イベントベースのロギングの大ファンです)。基本的なロギングが非常に複雑なレポートUIに変わるのを見てきました。ロギングが次のように見える場合_logger.Log(new ApplicationStartingEvent())とそうでない場合は、明らかにこの方向に進むのがずっと簡単Logger.Log("Application has started")です。これは決して起こらないかもしれない未来の目録を作成していると主張するかもしれません、これは判断の呼び出しであり、私はそれが価値があると思います。

実際、私の個人的なプロジェクトでは_logger、アプリケーションが何をしていたのかを把握するために純粋にを使用して非ロギングUIを作成しました。これは、アプリケーションが何をしているのかを理解するためにコードを書く必要がなかったことを意味し、最終的に堅実なロギングになりました。伐採に対する私の姿勢がシンプルで変わらないものであったなら、その考えは私には起こらなかったでしょう。

ロギングの場合、Telastynに同意します。


これは私が偶然に記録する方法です。記事などにリンクしたいのですが、見つけられません。.NETの世界にいて、イベントログを調べると、が見つかりますSemantic Logging Application Block。皮肉なことに、MSの「パターンと実用」チームによって作成されたコードのほとんどのように、それを使用しないでください。これはアンチパターンです。
ネイサンクーパー

3

最初の横断的関心事は主要な構成要素ではなく、システム内の依存関係として扱われるべきではありません。たとえば、ロガーが初期化されていないか、キャッシュが機能していない場合、システムは動作するはずです。どのようにしてシステムの結合性と凝集性を低下させますか?それが、SOLIDがオブジェクト指向システム設計で登場するところです。

オブジェクトをシングルトンとして保持することは、SOLIDとは関係ありません。これがオブジェクトのライフサイクルであり、オブジェクトをメモリ内で存続させる期間です。

初期化するために依存関係を必要とするクラスは、提供されたクラスインスタンスがシングルトンであるか一時的なものであるかを知らないはずです。しかし、tldr; すべてのメソッドまたはクラスでLogger.Instance.Log()を記述している場合、問題のあるコード(コードの匂い/ハードカップリング)が非常に厄介です。これは人々がSOLIDを乱用し始める瞬間です。そしてOPのような仲間の開発者は、このような本物の質問をし始めます。


2

継承と特性(一部の言語ではミックスインとも呼ばれます)の組み合わせを使用して、この問題を解決しました。特性は、この種のクロスカッティングの問題を解決するのに非常に便利です。通常は言語機能ですが、本当の答えは言語機能に依存していると思います。


面白い。特性をサポートする言語を使用して重要な作業を行ったことはありませんが、将来のプロジェクトでそのような言語を使用することを検討しているので、これを拡張して特性が問題を解決する方法を示すことができれば助かります。
ジュール
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.