web-dev-qa-db-ja.com

高度にカスタマイズされたソフトウェアをどのように整理しますか?

私は、世界中のさまざまな顧客向けに高度にカスタマイズされた大規模なソフトウェアプロジェクトに取り組んでいます。これは、さまざまな顧客に共通する80%のコードだけでなく、顧客ごとに変更する必要がある多くのコードがあることを意味します。以前は個別のリポジトリ(SVN)で開発を行い、新しいプロジェクトが開始したとき(私たちは少数ですが大規模な顧客がいます)、ニーズに最適なコードベースを持つ過去のプロジェクトに基づいて別のリポジトリを作成しました。これは以前は機能していましたが、いくつかの問題が発生しました。

  • バグ 1つのリポジトリで修正されたものは、他のリポジトリではパッチされません。これは組織の問題かもしれませんが、5つの異なるリポジトリでバグを修正してパッチを適用するのは難しいと思います。このリポジトリを管理しているチームは世界の別の場所にいる可能性があり、テスト環境がないためです、彼らのスケジュールや彼らが持っているどんな要件も知りません(ある国の「バグ」は別の国の「機能」かもしれません)。
  • 機能と、あるプロジェクトに加えられた改善は、別のプロジェクトにも役立つかもしれませんが、失われたり、それらが別のプロジェクトで使用されたりすると、あるコードベースから別のコードベースにマージするときに大きな頭痛の種になることがよくあります(両方のブランチが1年間独立して開発されました)。
  • リファクタリングとコードの改善ブランチ間でこれらのすべての変更をマージしなければならない場合、1つの開発ブランチで作成されたものが失われるか、または害をもたらします。

現在、これらの問題を解決する方法について議論しており、これまでのところ、これを解決する方法について次のアイデアを考え出しました。

  1. 個別のブランチで開発を維持しかし、一般的なバグ修正がマージされる中央リポジトリを用意し、すべてのプロジェクトでこの中央リポジトリからの変更を定期的(たとえば毎日)に独自のリポジトリにマージすることで、より適切に編成します。これには、ブランチ間のマージのために大きな規律と多くの努力が必要です。だから私はそれがうまくいくとは確信しておらず、特に時間的なプレッシャーがかかったときにこの規律を保つことができます。

  2. 個別の開発ブランチを放棄そして、すべてのコードが存在する中央コードリポジトリを持ち、プラグイン可能なモジュールと構成オプションを使用してカスタマイズを行います。 Dependency Injectionコンテナーを使用してコードの依存関係を解決しており、ほとんどのコードでMVVMパターンに従ってビジネスロジックをUIから明確に分離しています。

2番目のアプローチはより洗練されているように見えますが、このアプローチには多くの未解決の問題があります。例:モデル/データベースでの変更/追加の処理方法。 Entity Frameworkで.NETを使用して、強く型付けされたエンティティを作成しています。データモデルを乱雑にすることなく、ある顧客には必要であるが別の顧客には役に立たないプロパティをどのように処理できるかわかりません。これをデータベースでサテライトテーブル(特定のエンティティの追加の列が元のエンティティへの1:1マッピングで存在する別のテーブルを持っている)を使用して解決することを考えていますが、これはデータベースのみです。これをコードでどのように処理しますか?私たちのデータモデルは中央のライブラリにあり、このアプローチを使用して、顧客ごとに拡張することはできません。

この問題に取り組んでいるのは私たちだけのチームではないと私は確信しています。このトピックに関する資料がほとんどないことに驚いています。

私の質問は次のとおりです:

  1. 高度にカスタマイズされたソフトウェアについてどのような経験がありますか、どのアプローチを選択し、どのように機能しましたか?
  2. どのようなアプローチをお勧めしますか、またその理由は何ですか。より良いアプローチはありますか?
  3. あなたが推薦できるトピックについての良い本や記事はありますか?
  4. 弊社の技術環境(.NET、Entity Framework、WPF、DI)に対する具体的な推奨事項はありますか?

編集:

すべての提案をありがとう。ほとんどのアイデアは、私たちのチームですでに持っているものと一致しますが、それらでの経験とそれらをよりよく実装するためのヒントを確認することは非常に役立ちます。

どちらに進むかはまだわからないし、決断も(単独で)しませんが、チームでこれを渡し、それが役立つと確信しています。

現時点では、テナーは、さまざまな顧客固有のモジュールを使用する単一のリポジトリのようです。私たちのアーキテクチャがこれまでのところか、それを適合させるためにどれだけの投資が必要かわからないため、しばらくの間は別のリポジトリにあるものもあるかもしれませんが、これが機能する唯一の長期的な解決策だと思います。

それでは、すべての回答に再度感謝します!

29
aKzenT

基本的な問題は、コードリポジトリのメンテナンスだけではなく、適切なアーキテクチャの欠如のようです。

  1. システムのコア/エッセンスは何ですか?それは常にすべてのシステムによって共有されますか?
  2. 各顧客にはどのような拡張/偏差が必要ですか?

フレームワークまたは標準ライブラリには前者が含まれ、後者はアドオン(プラグイン、サブクラス、DI、コード構造にとって意味のあるもの)として実装されます。

ブランチと分散開発を管理するソース管理システムもおそらく役立つでしょう。私はMercurialのファンですが、他の人はGitを好みます。フレームワークはメインのブランチであり、カスタマイズされた各システムは、たとえばサブブランチになります。

システムの実装に使用される特定のテクノロジー(.NET、WPFなど)は、ほとんど重要ではありません。

これを正しく行うことは簡単ではありませんが、長期的な実行可能性のためにはクリティカルです。そしてもちろん、長く待つほど、対処しなければならない技術的負債が大きくなります。

この本Software Architecture:Organizational Principles and Patternsが役に立つかもしれません。

幸運を!

11
Steven A. Lowe

私が働いていたある会社にも同じ問題があり、問題に取り組むためのアプローチは次のとおりでした。すべての新しいプロジェクトに共通のフレームワークが作成されました。これには、すべてのプロジェクトで同じでなければならないすべてのものが含まれます。例えば。フォーム生成ツール、Excelへのエクスポート、ロギング。この共通のフレームワークは(新しいプロジェクトで新しい機能が必要になったときに)改善されるだけで、決して分岐しないようにするための努力が払われました。

そのフレームワークに基づいて、顧客固有のコードが個別のリポジトリで維持されていました。有用または必要な場合は、バグ修正と改善がプロジェクト間でコピーアンドペーストされます(質問に記載されているすべての注意事項が含まれます)。ただし、グローバルに役立つ改善は共通のフレームワークに入ります。

すべての顧客に共通のコードベースにすべてを含めることにはいくつかの利点がありますが、一方で、プログラムが顧客ごとに異なるように動作するifsが無数にあると、コードを読み取ることが難しくなります。

編集:これをより理解しやすくするための1つの逸話:

その会社の領域は倉庫管理であり、倉庫管理システムの1つのタスクは、入荷商品の無料保管場所を見つけることです。簡単に聞こえますが、実際には、多くの制約と戦略を守る必要があります。

ある時点で、経営陣はプログラマに、ストレージの場所を見つけるための柔軟でパラメータ化可能なモジュールを作成するように依頼しました。これは、いくつかの異なる戦略を実装し、後続のすべてのプロジェクトで使用されるべきでした。高貴な努力の結果、複雑なモジュールが発生し、理解と維持が非常に困難になりました。次のプロジェクトでは、プロジェクトリーダーがそのウェアハウスで機能させる方法を理解できず、そのモジュールの開発者がいなくなったため、最終的にそれを無視して、そのタスク用のカスタムアルゴリズムを作成しました。

数年後、このモジュールが最初に使用された倉庫のレイアウトが変更され、その柔軟性を備えたモジュールは新しい要件に適合しませんでした。そこで、カスタムアルゴリズムに置き換えました。

LOCは適切な測定値ではありませんが、とにかく「フレキシブル」モジュールのサイズは〜3000 LOC(PL/SQL)でしたが、同じタスクのカスタムモジュールは〜100..250 LOCかかります。したがって、柔軟にしようとすると、コードベースのサイズが非常に大きくなり、期待した再利用性が得られませんでした。

11
user281377

多数の製品リリースでサポートされている複数のプラットフォーム(5つ以上)で取り組んだプロジェクトの1つ。あなたが説明している課題の多くは、少し異なる方法ではありますが、私たちが直面したものでした。私たちは独自のDBを持っていたので、その分野では同じタイプの問題はありませんでした。

私たちの構造はあなたのものと似ていましたが、私たちはコードのための単一のリポジトリを持っていました。プラットフォーム固有のコードは、コードツリー内の独自のプロジェクトフォルダーに入りました。一般的なコードは、それが属しているレイヤーに基づいてツリー内に存在していました。

構築中のプラットフォームに基づいて、条件付きコンパイルを行いました。それを維持するのはちょっと面倒でしたが、プラットフォーム固有の層に新しいモジュールが追加されたときにのみ実行する必要がありました。

すべてのコードを1つのリポジトリに格納することで、複数のプラットフォームとリリースで同時にバグ修正を簡単に行うことができました。新しいコードが関連しないと思われるプラットフォームを壊した場合に備えて、すべてのプラットフォームに自動化されたビルド環境がありました。

私たちはそれを思いとどまらせようとしましたが、他の方法では一般的なコードにあったプラットフォーム固有のバグに基づいてプラットフォームが修正を必要とする場合があります。モジュールを曖昧に見せずに条件付きでコンパイルをオーバーライドできる場合は、最初にそれを行います。そうでない場合は、モジュールを共通の領域から移動して、プラットフォーム固有にプッシュします。

データベースについては、プラットフォーム固有の列/変更を含むいくつかのテーブルがありました。テーブルのすべてのプラットフォームバージョンが機能のベースラインレベルを満たしていることを確認するため、プラットフォームの依存関係を気にすることなく、一般的なコードがそれを参照できます。プラットフォーム固有のクエリ/操作がプラットフォームプロジェクトレイヤーにプッシュされました。

だから、あなたの質問に答えるには:

  1. たくさん、そしてそれは私が一緒に働いた中で最高のチームの一つでした。当時のコードベースは約100万ロケーションでした。私はアプローチを選択することができませんでしたが、かなりうまくいきました。振り返ってみても、これ以上の扱い方はわかりません。
  2. 私が私の回答で言及したニュアンスで提案した2番目のアプローチをお勧めします。
  3. 考えられる本はありませんが、まずはマルチプラットフォーム開発を研究します。
  4. 強力なガバナンスを構築します。それはあなたのコーディング標準が守られていることを確認するための鍵です。これらの標準に従うことは、物事を管理および保守可能に保つ唯一の方法です。私たちが追っていたモデルを打ち破るために、私たちは情熱的な嘆願を共有しましたが、これらの訴えのいずれも、上級開発チーム全体を揺るがしませんでした。
5
user53019

私は長年にわたり、同様の問題を抱えていた年金管理アプリケーションに取り組んできました。年金制度は企業間で大きく異なり、計算ロジックとレポートを実装するための高度な専門知識と非常に異なるデータ設計が必要です。アーキテクチャの一部については簡単に説明することしかできませんが、アイデアが十分にわかると思います。

私たちには2つの別々のチームがありました:コアシステムコード(上記の80%の共有コード)を担当するコアdevelopmentチーム、およびimplementationチーム、年金システムに関する専門知識があり、クライアントの要件を学習し、クライアント向けのスクリプトとレポートをコーディングしました。

すべてのテーブルをXmlで定義しました(これは、エンティティフレームワークが時間テストされ、一般的だった時代の前です)。実装チームはすべてのテーブルをXmlで設計し、コアアプリケーションはすべてのテーブルをXmlで生成するように要求されます。また、各クライアントに関連するVBスクリプトファイル、Crystal Reports、Wordドキュメントなど)もありました(他の実装の再利用を可能にするために、Xmlに組み込まれた継承モデルもありました)。

コアアプリケーション(すべてのクライアントに対して1つのアプリケーション)は、そのクライアントに対する要求が来たときにすべてのクライアント固有のものをキャッシュし、共通のデータオブジェクト(リモートのようなものADOレコード)を生成しますセット)、シリアル化して渡すことができます。

このデータモデルはエンティティ/ドメインオブジェクトほど滑らかではありませんが、柔軟性が高く、普遍的で、1組のコアコードで処理できます。おそらくあなたのケースでは、共通フィールドのみでベースエンティティオブジェクトを定義し、カスタムフィールド用の追加のディクショナリを用意できます(エンティティオブジェクトに何らかの種類のデータ記述子のセットを追加して、カスタムフィールド用のメタデータを持つようにします。 )

コアシステムコード用と実装コード用に別々のソースリポジトリがありました。

私たちのコアシステムは、いくつかの非常に標準的な一般的な計算モジュールを除いて、実際にはビジネスロジックがほとんどありませんでした。コアシステムは、画面ジェネレーター、スクリプトランナー、レポートジェネレーター、データアクセス、トランスポートレイヤーとして機能しました。

コアロジックとカスタマイズされたロジックをセグメント化することは難しい課題です。ただし、クライアントごとにシステムの複数のコピーを実行するよりも、1つのコアシステムで複数のクライアントを実行する方が良いと常に感じていました。

4
Sam Goldberg

2番目のアプローチはより洗練されているように見えますが、このアプローチには多くの未解決の問題があります。

これらの問題は次々と解決できると思います。行き詰まった場合は、ここまたはSO特定の問題について尋ねてください。

他の人が指摘したように、1つの中央コードベース/ 1つのリポジトリを持つことは、あなたが好むはずのオプションです。私はあなたの例の質問に答えようとします。

例:モデル/データベースでの変更/追加の処理方法。 Entity Frameworkで.NETを使用して、強く型付けされたエンティティを作成しています。データモデルを乱雑にすることなく、ある顧客には必要であるが別の顧客には役に立たないプロパティをどのように処理できるかわかりません。

いくつかの可能性がありますが、すべて現実のシステムで見ました。どちらを選択するかは、状況によって異なります。

  • ある程度雑然として生きる
  • テーブル「CustomAttributes」(名前とタイプを説明する)と「CustomAttributeValues」(数値の場合でも文字列表現として格納される値など)を紹介します。これにより、インストール時または実行時に、顧客ごとに個別の値を持つそのような属性を追加できます。各カスタム属性をデータモデルで「視覚的に」モデル化することを強く要求しないでください。

  • これで、コードでそれを使用する方法が明確になります。これらのテーブルにアクセスするための一般的なコードと、その属性を正しく解釈するための個別のコード(おそらく別のプラグインDLL)があります。

  • 別の方法として、各エンティティテーブルに大きな文字列フィールドを指定し、個別のXML文字列を追加することもできます。
  • いくつかの概念を一般化して、さまざまな顧客でより簡単に再利用できるようにします。マーティン・ファウラーの本「 分析パターン 」をお勧めします。この本はソフトウェア自体をカスタマイズすることについては書かれていませんが、あなたにも役立つかもしれません。

特定のコードの場合:特に顧客固有のスクリプトを追加するために、スクリプト言語を製品に導入することもできます。このようにして、コードと顧客固有のコードの間に明確な境界を作成するだけでなく、顧客がシステムをある程度自分でカスタマイズできるようにすることもできます。

2
Doc Brown

私は小規模なシステム(20 kloc)で作業しましたが、DIと構成はどちらもクライアント間の差異を管理する優れた方法ですが、システムの分岐を回避するには不十分です。データベースは、固定スキーマを持つアプリケーション固有の部分と、カスタムXML構成ドキュメントを通じて定義されるクライアント依存部分に分割されます。

Mercurialには単一のブランチがあり、それは配信可能であるかのように構成されていますが、架空のクライアント用にブランド化および構成されています。バグ修正はそのプロジェクトに組み込まれ、コア機能の新しい開発はそこでのみ発生します。実際のクライアントへのリリースは、その分岐であり、独自のリポジトリに格納されます。手動で記述されたバージョン番号を通じてコードへの大きな変更を追跡し、コミット番号を使用してバグ修正を追跡します。

2
Dan Monego

申し訳ありませんが、問題の直接的な経験はありませんが、いくつかコメントがあります。

2番目のオプションは、コードを中央のリポジトリに(可能な限り)まとめ、カスタマイズのために設計する(これもまた可能な限り)ことですが、長期的にはほとんど確実です。

問題は、どのようにそこに到達するか、そしてどのくらいかかるかです。

この状況では、リポジトリに一度に複数のアプリケーションのコピーを(一時的に)保持することはおそらく問題ありません。

これにより、カスタマイズを直接サポートするアーキテクチャに徐々に移行することができ、一気にカスタマイズする必要はありません。

2
William Payne

80%の機能をAと共有しているBの開発を開始するように求められたら、次のいずれかを行います。

  1. Aをクローンして変更します。
  2. AとBの両方が使用する機能をCに抽出します。
  3. Bとそれ自身の両方のニーズを満たすためにAを十分に構成可能にします(したがって、BはAに埋め込まれています)。

あなたは1を選択しましたが、それはあなたの状況にうまく適合していないようです。あなたの使命は、2と3のどちらが適しているかを予測することです。

0
Moshe Revah

私はそのようなアプリケーションを1つだけ作成しました。販売したユニットの90%はそのまま販売されたもので、変更はありませんでした。各顧客には独自のカスタマイズされたスキンがあり、そのスキン内でシステムを提供しました。コアセクションに影響するmodが発生したとき、IF分岐を使用してみました。同じセクションでmod#2が導入されたとき、将来の拡張に備えてCASE logicに切り替えました。これはマイナーな要求のほとんどを処理するように見えました。

その他のマイナーなカスタムリクエストは、ケースロジックを実装することで処理されました。

Modが2つの急進的なものである場合は、クローン(個別のインクルード)を構築し、CASEをラップして別のモジュールを含めました。

コアのバグ修正と変更はすべてのユーザーに影響を与えました。本番環境に移行する前に、開発段階で徹底的にテストしました。私たちは常に変更に伴うメール通知を送信し、金曜日に製品の変更を決して、決して、決して投稿しませんでした...決して。

私たちの環境はクラシックASPおよびSQL Serverでした。スパゲッティコードショップではありませんでした...すべてがインクルード、サブルーチン、関数を使用してモジュール化されていました。