私が関わっているほとんどのプロジェクトは、いくつかのオープンソースコンポーネントを使用しています。一般的な原則として、コードのすべてのコンポーネントをサードパーティのライブラリにバインドすることを常に避け、代わりにカプセル化ラッパーを経由して変更の痛みを回避することは良い考えですか?
例として、PHPプロジェクトのほとんどは、log4phpをログフレームワークとして直接使用します。つまり、\ Logger :: getLogger()を介してインスタンス化し、-> info()または-> warn( )メソッドなど。ただし、将来的には、いくつかの点でより優れた架空のロギングフレームワークが登場する可能性があります。現状では、log4phpメソッドシグネチャに密接に結合するすべてのプロジェクトは、数十か所で変更する必要があります。これは、コードベースに明らかに大きな影響を与え、変更は潜在的な問題です。
この種のシナリオから将来性のある新しいコードベースを作成するために、ロギング機能をカプセル化し、絶対確実ではないものの、最小限の変更で将来のロギングの動作方法を簡単に変更できるように、ラッパークラスを検討(および場合によっては実装)します。 ;コードはラッパーを呼び出し、ラッパーはロギングフレームワークに呼び出しを渡しますdu jour。
他のライブラリにはもっと複雑な例があることを念頭に置いて、私は過剰設計ですか、またはこれはほとんどの場合賢明な予防策ですか?
編集:その他の考慮事項-依存性注入とテストダブルを使用するには、実質的にほとんどのAPIを抽象化する必要があります(「コードの実行をチェックしてその状態を更新したいが、ログコメントを書き込んだり、実際のデータベースにアクセスしたりしない」)。これは決定者ではありませんか?
サードパーティAPIの小さなサブセットのみを使用する場合、ラッパーを作成することは理にかなっています。これはカプセル化と情報の隠蔽に役立ち、独自のコードに巨大なAPIを公開しないようにします。 しないでください使用したい機能がすべて「非表示」であることを確認するのにも役立ちます。
ラッパーのもう1つの良い理由は、サードパーティのライブラリを変更するためにexpectを使用する場合です。これがインフラストラクチャの一部である場合know変更しないので、そのラッパーを作成しないでください。
サードパーティのライブラリをラップすることにより、その上に追加の抽象化レイヤーを追加します。これにはいくつかの利点があります。
ライブラリを別のライブラリに置き換える必要がある場合は、ラッパーの実装を変更するだけで済みます-1か所。ラッパーの実装を変更でき、他のことについて変更する必要はありません。つまり、疎結合システムがあります。それ以外の場合は、コードベース全体を調べて、どこにでも変更を加える必要があります。これは明らかに望ましいことではありません。
ライブラリが異なれば、APIも大きく異なる可能性がありますが、同時に、それらのAPIのどれもが、必ずしも必要なものではない場合があります。一部のライブラリで、すべての呼び出しでトークンを渡す必要がある場合はどうなりますか?ライブラリを使用する必要がある場所ならどこでもアプリ内でトークンを渡すことができます。また、どこかより集中的に安全に保管できますが、いずれの場合もトークンが必要です。ラッパークラスを使用すると、この全体が再び簡単になります。トークンをラッパークラス内に保持し、トークンをアプリ内のコンポーネントに公開することなく、トークンの必要性を完全に抽象化できるからです。優れたAPI設計を強調しないライブラリを使用したことがあれば、非常に有利です。
単体テストは1つのことだけをテストする必要があります。クラスを単体テストしたい場合は、その依存関係をモックする必要があります。これは、そのクラスがネットワーク呼び出しを行ったり、ソフトウェアの外部の他のリソースにアクセスしたりする場合、さらに重要になります。サードパーティのライブラリをラップすることで、これらの呼び出しを模擬してテストデータやユニットテストに必要なものを返すのは簡単です。このような抽象化レイヤーがない場合、これを行うのははるかに困難になります。ほとんどの場合、これにより多くの醜いコードが生成されます。
ラッパーへの変更は、ソフトウェアの他の部分には影響を与えません-少なくともラッパーの動作を変更しない限り。このラッパーのような抽象化レイヤーを導入することで、ライブラリへの呼び出しを簡素化し、そのライブラリへのアプリの依存関係をほぼ完全に削除できます。ソフトウェアはラッパーを使用するだけで、ラッパーの実装方法やラッパーの動作に違いはありません。
正直に言いましょう。人々はこのようなものの長所と短所について何時間も議論することができます-そのため私はむしろあなたに例を示すだけです。
ある種のAndroidアプリがあり、画像をダウンロードする必要があるとします。たとえば、画像のロードとキャッシュを簡単にするライブラリがたくさんあります Picasso または Universal Image Loader 。
これで、使用するライブラリをラップするために使用するインターフェースを定義できます。
_public interface ImageService {
Bitmap load(String url);
}
_
これは、画像を読み込む必要があるときにアプリ全体で使用できるインターフェイスです。このインターフェイスの実装を作成し、依存関係注入を使用して、ImageService
を使用するすべての場所にその実装のインスタンスを注入できます。
最初にピカソを使用することにしたとしましょう。これで、内部でPicassoを使用するImageService
の実装を作成できます。
_public class PicassoImageService implements ImageService {
private final Context mContext;
public PicassoImageService(Context context) {
mContext = context;
}
@Override
public Bitmap load(String url) {
return Picasso.with(mContext).load(url).get();
}
}
_
あなたが私に尋ねればかなり簡単です。ライブラリのラッパーは、便利であるために複雑である必要はありません。インターフェースと実装のコード行の合計は25行に満たないため、これを作成するのはほとんど手間がかかりませんでしたが、すでにこれを行うことで何かを得ています。実装のContext
フィールドを参照してください。 ImageService
を使用する前に、選択した依存関係注入フレームワークが既に依存関係を注入しているため、アプリは画像のダウンロード方法やライブラリの依存関係を気にする必要がありません。アプリが見るのはImageService
だけで、画像が必要な場合はload()
をURLで呼び出します-シンプルで簡単です。
しかし、本当のメリットは、物事を変え始めたときです。ピカソは現在必要な機能の一部をサポートしていないため、ピカソをユニバーサルイメージローダーに置き換える必要があるとします。いくつかのピカソ呼び出しを忘れたため、コードベースをくまなく調べて、ピカソへのすべての呼び出しを面倒に置き換えてから、何十ものコンパイルエラーに対処する必要がありますか? いいえ必要なのは、ImageService
の新しい実装を作成し、依存関係注入フレームワークにこの実装を今後使用するように指示することだけです。
_public class UniversalImageLoaderImageService implements ImageService {
private final ImageLoader mImageLoader;
public UniversalImageLoaderImageService(Context context) {
DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.defaultDisplayImageOptions(defaultOptions)
.build();
mImageLoader = ImageLoader.getInstance();
mImageLoader.init(config);
}
@Override
public Bitmap load(String url) {
return mImageLoader.loadImageSync(url);
}
}
_
ご覧のとおり、実装は大きく異なる場合がありますが、問題ではありません。アプリの他の場所で1行のコードを変更する必要はありませんでした。私たちは完全に異なるライブラリを使用します。これは完全に異なる機能を持っているか、まったく異なる方法で使用されているかもしれませんが、私たちのアプリは気にしません。前と同じように、アプリの残りの部分はload()
メソッドを持つImageService
インターフェースを参照するだけですが、このメソッドが実装されていることは問題ではありません。
少なくとも私にとって、これはすでにかなりいい音に聞こえますが、待ってください!まだまだあります。作業中のクラスの単体テストを作成していて、このクラスがImageService
を使用しているとします。もちろん、ユニットテストに他のサーバーにあるリソースへのネットワークコールを許可することはできませんが、ImageService
を使用しているため、load()
でユニットテストに使用される静的Bitmap
を簡単に返すことができます。モックされたImageService
の実装:
_public class MockImageService implements ImageService {
private final Bitmap mMockBitmap;
public MockImageService(Bitmap mockBitmap) {
mMockBitmap = mockBitmap;
}
@Override
public Bitmap load(String url) {
return mMockBitmap;
}
}
_
要約すると、サードパーティのライブラリをラップすることにより、コードベースは変更に対してより柔軟になり、全体的にシンプルでテストが容易になり、ソフトウェアのさまざまなコンポーネントの結合を減らします-ソフトウェアをより長く維持するほど、ますます重要になるすべてのもの。
この将来の改良されたロガーの申し立てがあるどのような非常に優れた新機能を知らずに、ラッパーをどのように記述しますか?最も論理的な選択は、ラッパーにある種のロガークラスをインスタンス化させ、->info()
や->warn()
などのメソッドを用意することです。つまり、基本的に現在のAPIと同じです。
私が変更する必要がまったくない、またはどうしても避けられない書き直しを必要とする可能性のある将来性のあるコードではなく、「過去に耐える」コードを使用することを好みます。つまり、コンポーネントを大幅に変更するまれなケースですが、過去のコードと互換性のあるラッパーを作成するときはthat'sです。ただし、新しいコードはすべて新しいAPIを使用しており、同じファイルに変更を加えるときはいつでも、またはスケジュールが許す限り、古いコードをリファクタリングして使用します。数か月後、ラッパーを削除できます。変更は段階的で堅牢です。
別の言い方をすると、ラッパーは、ラップする必要のあるすべてのAPIをすでに知っている場合にのみ意味を持ちます。良い例は、アプリケーションが現在多くの異なるデータベースドライバ、オペレーティングシステム、またはPHPバージョンをサポートする必要がある場合です。
明日より良いものが来た場合に備えて、サードパーティのライブラリをラップすることは、YAGNIの非常に無駄な違反だと思います。アプリケーションに固有の方法でサードパーティのコードを繰り返し呼び出す場合は、それらの呼び出しをラッピングクラスにリファクタリングして、繰り返しを排除する必要があります。それ以外の場合は、ライブラリAPIを完全に使用しており、ラッパーはライブラリ自体のように見えます。
次に、新しいライブラリが優れたパフォーマンスなどで表示されるとします。最初のケースでは、新しいAPIのラッパーを書き換えるだけです。問題ない。
2番目のケースでは、新しいライブラリを駆動するために古いインターフェイスを適合させるラッパーを作成します。もう少し作業が必要ですが、問題がなく、以前にラッパーを作成した場合よりも作業は多くありません。
サードパーティライブラリのラッパーを作成する基本的な理由は、それを使用するコードを変更せずにそのサードパーティライブラリを交換できるようにするためです。何かとのカップリングは避けられないので、あなたが書いたAPIとカップリングする方が良いという議論があります。
これが努力に値するかどうかは別の話です。その議論はおそらく長い間続くでしょう。
このような変更が必要になる可能性が低い小規模プロジェクトの場合、おそらく不必要な作業です。大規模なプロジェクトの場合、その柔軟性は、ライブラリをラップする余分な労力をはるかに上回ります。しかしながら、それが前もってそうであるかどうかを知ることは難しい。
別の見方をすると、変化する可能性のあるものを抽象化するという基本原則です。したがって、サードパーティのライブラリが十分に確立されており、変更される可能性が低い場合は、ラップしない方がよい場合があります。ただし、サードパーティのライブラリが比較的新しい場合は、交換する必要がある可能性が高くなります。とはいえ、確立されたライブラリの開発は何度も見捨てられてきました。したがって、これは簡単に答えられる質問ではありません。
私はラッピングキャンプに強く参加しており、サードパーティのライブラリを最優先で置き換えることはできません(それはボーナスです)。ラッピングを支持する私の主な根拠は単純です
サードパーティのライブラリは、ではなく特定のニーズに合わせて設計されています。
そして、これは通常、コードの重複のボートロードの形で現れます。開発者がQButton
を作成してアプリケーションを探すために必要な方法で、デザイナーだけのために8行のコードを書くようなものです。見た目だけでなく、ボタンの機能もソフトウェア全体で完全に変更できるようにしたいため、何千行ものコードをさかのぼって書き換える必要がありました。または、コードベースが低レベルに散りばめられているため、レンダリングパイプラインの近代化には壮大な書き換えが必要であることがわかりました。リアルタイムレンダラーの設計を一元化してOGLの使用を厳密にその実装に任せるのではなく、あらゆる場所でレベルのアドホック固定パイプラインOpenGLコードを使用します。
これらのデザインは、特定のデザインニーズに合わせて調整されていません。それらは実際に必要なものの大規模なスーパーセットを提供する傾向があり(そして、デザインの一部ではないものは、それ以上ではないにしても重要です)、それらのインターフェースは、高レベルの「1考えた= 1つの要求」のような方法で、直接使用すると、すべての中心的な設計管理が奪われます。開発者が必要なものを表現するのに必要なレベルよりもはるかに低いレベルのコードを作成してしまうと、そのコードをアドホックな方法でラップして、何十もの急いで作成された大雑把なものになってしまう場合があります。十分に設計され、文書化された1つのラッパーではなく、設計および文書化されたラッパー。
もちろん、ラッパーがサードパーティのAPIが提供するもののほぼ1対1の翻訳であるライブラリには、強い例外を適用します。その場合、ビジネス要件と設計要件をより直接的に表現する、より高いレベルの設計を探す必要がない場合があります(たとえば、「ユーティリティ」ライブラリに似たものの場合など)。しかし、私たちのニーズをより直接的に表現する、よりカスタマイズされたデザインが利用可能な場合、私はラッピングキャンプにいることになります。これは、より高いレベルの関数を使用し、アセンブリコードのインライン化よりも再利用することを強く望んでいるのと同じです。あらゆる所に。
奇妙なことに、ボタンを作成してそれを返す機能を設計する能力について、開発者が非常に不信感と悲観的な見方をして開発者と衝突しました。上記の機能の設計と使用に関するボタン作成の詳細(最終的には繰り返し変更する必要がありました)。これらの種類のラッパーを合理的な方法で設計すると自分自身を信頼できない場合は、そもそも何かを設計しようとしている目的を見さえしません。
別の言い方をすれば、サードパーティのライブラリを、システムの設計の代わりとしてではなく、実装の時間を大幅に節約できる可能性があると見なしています。
@ Oded がすでに言ったことに加えて、ロギングの特別な目的のためにこの回答を追加したいと思います。
私は常にロギング用のインターフェースを持っていますが、log4foo
フレームワークを置き換える必要はありませんでした。
インターフェイスを提供してラッパーを作成するのに30分しかかからないので、不要になったとしても、時間を無駄にしないでください。
YAGNIの特別なケースです。必要ありませんが時間がかからず安心です。ロガーを交換する日が本当に来たら、実際のプロジェクトで電話を交換する1日よりも多く節約できるので、30分の投資をしてよかったと思います。また、ロギング用の単体テスト(ロガー実装自体のテストは別として)を書いたり見たりしたことがないので、ラッパーなしで欠陥を予期します。
私は現在取り組んでいるプロジェクトでこの正確な問題に対処しています。しかし私の場合、ライブラリはグラフィック用であり、そのため、プロジェクト全体に散在させるのではなく、グラフィックを扱う少数のクラスに使用を制限することができます。したがって、必要に応じて後でAPIを切り替えるのは非常に簡単です。ロガーの場合、問題はさらに複雑になります。
したがって、この決定は、サードパーティのライブラリが正確に何をしているのか、そしてそれを変更することにどれだけの苦痛が伴うかと関係があると思います。すべてのAPI呼び出しを簡単に変更できる場合は、おそらくそれを行う価値はありません。ただし、後でライブラリを変更することが本当に難しい場合は、おそらく今すぐラップします。
それ以上に、他の回答は主な質問を非常によくカバーしているので、依存関係の注入とモックオブジェクトについての最後の追加に焦点を当てたいと思います。もちろん、ロギングフレームワークがどのように機能するかによって異なりますが、ほとんどの場合、ラッパーは必要ありません(おそらくラッパーから恩恵を受けるでしょう)。モックオブジェクトのAPIをサードパーティライブラリとまったく同じにするだけで、テスト用のモックオブジェクトを簡単に入れ替えることができます。
ここでの主な要因は、サードパーティライブラリが依存関係の挿入(またはサービスロケータまたはそのような疎結合パターン)によって実装されているかどうかです。ライブラリ関数がシングルトンまたは静的メソッドなどを介してアクセスされる場合は、依存関係注入で操作できるオブジェクトにそれをラップする必要があります。
サードパーティライブラリに関する私の考え:
サードパーティの依存関係を使用することの賛否両論(大丈夫、ほとんどの短所)について、 最近iOSコミュニティでいくつかの議論 がありました。私が見た多くの議論はかなり一般的でした—すべてのサードパーティライブラリを1つのバスケットにグループ化しました。ただし、ほとんどの場合と同様に、それほど単純ではありません。では、1つのケースに焦点を当ててみましょう
サードパーティのUIライブラリの使用を避けるべきですか?
サードパーティライブラリを検討する理由:
開発者がサードパーティライブラリの使用を検討する主な理由は2つあるようです。
ほとんどのUIライブラリ( すべてではありません! )は、2番目のカテゴリに分類される傾向があります。これはロケット科学ではありませんが、正しく構築するには時間がかかります。
それがコアビジネス機能である場合—それが何であっても、自分で実行してください。
2つのタイプのコントロール/ビューがあります:
UICollectionView
のUIKit
から。UIPickerView
。ほとんどのサードパーティライブラリは、2番目のカテゴリに分類される傾向があります。さらに、それらは多くの場合、最適化された既存のコードベースから抽出されます。不明な初期の仮定
開発者の多くは内部コードのコードレビューを行っていますが、サードパーティのソースコードの品質を当然のことと考えている場合があります。ライブラリのコードを閲覧するだけで、少し時間をかける価値があります。あなたは、いくつかの赤い旗を見ることに驚いてしまうかもしれません。必要のない場所で使用されるスウィズリング。
多くの場合、アイデアを学ぶことは、結果のコード自体を取得するよりも有益です。
非表示にできません
UIKitの設計方法により、サードパーティのUIライブラリを非表示にすることはできません。アダプターの後ろ。ライブラリはUIコードと絡み合い、プロジェクトのデファクトになります。
将来の時間コスト
UIKitは、iOSのリリースごとに異なります。物事が壊れます。サードパーティの依存関係は、予想されるほどメンテナンスフリーではありません。
結論:
私の個人的な経験から、サードパーティのUIコードのほとんどの使用は、ある程度の時間の利益のために小さな柔軟性を交換することに要約されます。
既製のコードを使用して、現在のリリースをより早く出荷します。しかし、遅かれ早かれ、私たちはライブラリの限界に達し、難しい決断の前に立ちます。次に何をすべきか?
ライブラリを直接使用すると、開発チームにとってよりフレンドリーになります。新しい開発者が加わったとき、彼は使用されているすべてのフレームワークを十分に経験しているかもしれませんが、自社開発のAPIを学ぶ前に生産的に貢献することはできません。若い開発者があなたのグループで進歩しようとするとき、彼はより有用な一般的な能力を獲得する代わりに、どこにも存在しないあなたの特定のAPIを学ぶことを強いられます。誰かが元のAPIの有用な機能や可能性を知っている場合、それらを知らない誰かによって書かれたレイヤーを越えて到達できない可能性があります。誰かが仕事を探している間にプログラミングタスクを取得する場合、ラッパーを通じて必要な機能にアクセスしているために、彼が何度も使用した基本的なことを実証できない可能性があります。
これらの問題は、後で完全に異なるライブラリを使用する可能性よりもむしろ重要であると思います。私がラッパーを使用する唯一のケースは、別の実装への移行が確実に計画されている場合、またはラップされたAPIが十分にフリーズされておらず、変化し続ける場合です。