application architecture







from https://qiita.com/lobin-z0x50/items/39131a4f47ed7c5df443

最もコスパの良い3つの施作

変更に強いアーキテクチャとは、いくつもの概念や指標の組み合わせと、複雑な理論の上に成り立っている。
そういったいくつもの重要な概念のうち、最もコスパの良い(経験上これだけは絶対にゆずれない、費用対効果の高い)項目をあげるとすると、以下の三つだ。
  1. 適切なモジュール分割
  2. 依存関係を一方向にする
  3. テストコード
誤解の無いよう再度断っておくが、この3つだけで変更に強いアーキテクチャが必ず実現できるというものではない。
上記は昨今のソフトウェアの大規模化やフレームワークの流行、実装方式のトレンドなどを鑑みて、ソフトウェア設計者が最も考慮すべきで最も効果の高い項目、といった位置づけで選出したものある。
ある程度の設計経験がある方なら感覚で分かると思うが、実は上記3項目には相関がある
つまり、テストコードを書くためには、モジュール間が疎結合になっている必要があり、モジュール間を疎結合にするためには適切なモジュール分割が行なわれている必要がある、ということだ。
したがって 1 → 2 → 3 の順に実現していく必要がある。このとき、例えば 2 までしか達成できなくとも、それなりに大きな効果がある。

1. 適切なモジュール分割

説明のためにひとつ身近な例を挙げよう。
一般的な MVC フレームワークをモジュールという観点で考えると、Model, View, Controller にモジュールが分かれている。
図1.png
それぞれの役割分担は以下のようになっている。
  • M: データアクセス、業務ロジック
  • V: 画面の表示、UI
  • C: M, V の制御
Model が DB に対応していて(本当はそれだけではない、後述)、Controller から呼び出すわけだが、業務ロジックを直接 Controller に書くのはよくない、みたいな意識をなんとなく持っている人はいるだろうか。
実際、DBアクセス処理や外部APIの呼び出しなどを Controller 内に直接ずらずらと書き連ねていくのはよろしくない。
※WindowsForms の人は、Form の中にずらずらと業務ロジックを書いてあるような状況と考えて欲しい。
小規模なアプリケーションでは何ら問題ないが、エンタープライズシステムなどといったある程度の規模のソフトウェアでこういう作り方を続けていくと、そのうち Controller が肥大化し、苦しくなってくる(下図)1
図2.png
かといって Controller の代わりに Model に業務ロジックを実装していっても、システムの規模が大きくなれば同じように苦しくなってくる。
当然テストを書くことなんて到底不可能だ。
ではどうしたらよいのか。

「サービス」の導入

一つの解決策は、 M, V, C 以外に、S: Service というモジュールを導入することだ2
Service モジュール内に、業務や機能の分類ごとにサービスクラスを作っていき、業務ロジックはすべてそこに追い出してしまう。
Controller は単にサービスクラスを呼び出すだけ。で、サービスの出力を View に渡したり、処理結果(または例外キャッチ)を元に画面遷移したりという風に本来の Controller の仕事に専念すればよい。
図3.png
"MVCSアーキテクチャ" という言葉はないのだが、まぁそんなイメージだ。3
注意して欲しいのは、Service モジュールは、View や Controller に依存しないように構成しなくてはならない(後述)。
Model には依存してかまわない4。図の中で Controller の下に Service が描いてあるのはそういう意味でもある。

サービスモデル

こうやって小さなプロダクトを完成させたとして、それを機能拡張して改修して、と続けていくと、サービスモジュール内のあるサービスクラスが大規模化してくることが起こりうる。そしてクラスの入出力データも大規模化してくる。
たとえば、検索処理を行って検索結果一覧を返すサービスがあったとする。
その入力は、検索条件を表す画面からのポストデータ(というか連想配列)で、その出力は Model の配列だったとしよう。
ところが機能拡張により検索条件が複雑化して、入力データが連想配列では苦しくなってくる。
そういうときには、サービスの入力データを表す新しいモデルクラスを定義し、それを使ってデータを受け渡すようにする。
MVCフレームワークの Model とは区別したいので、それぞれに適切な呼び名をつけよう。
ここまでの説明上の流れでは、MVCフレームワークの Model はデータベースモデルを指しているとしているので、これを「EntityModel」と呼ぶ。
そしてサービスの入力データは、なんでも良いのだが、ここでは「ServiceModel」とでもしておこう。
図4.png
※なお、この図からは Model を表す箱は「データ」を意味する平行四辺形で描くことにした。
視覚的に分かりやすくするためであり、それ以上の深い意図はない。

ビューモデル

次に、サービスの出力データが複雑化してくるケースだ。
これもサービスクラスが大きくなってくると当然のように起こりうる。
この場合も同様に、それを表す新しいモデルを定義し、それを使ってデータを受け渡すようにする。
名前は、どうしようか(名前はとても重要だ。いつも命名にそれなりの時間を要する)。
サービスの出力はビューに出力するためのデータであるとすると、「ViewModel」とでも呼ぶことにする。
図5.png
ビューモデルを使うアーキテクチャとして MVVM 5 があるが、まさにそれと同じようなイメージだ。この構成は Ajax を使うときにもよくマッチする。 View がブラウザ側で動いているような構成になるが、ViewModel をそのまま Json エンコードしてフロントに返してやれば、VueJS などで直接バインディングするだけで画面に値が反映される。とても分かりやすくなるしラクになるので、オススメのアーキテクチャパターンの一つだ6
ViewModel という名前に違和感があるなら、違った観点から命名してもよい。
例えば、WebAPI の出力を表しているのなら、WebApiResultModel としてもいいだろう。
このように、データの意味に基づいて〇〇モデルというように命名して、入出力データを整理していけばいいのだ。
※(2019-01-02 追記)個人的に Model という言葉は抽象的すぎると思っているので、例えば MVC の M がDBを表しているモデルなら EntityModel と呼ぼう、みたいに、具体的に〇〇モデルと呼び分ける方が整理しやすくなるし、理解しやすくなると思っている。
この時、サービスの入力と出力を同一のモデルに統合してしまわないこと。
本当に同じ意味のデータならそれで構わないが、意味として異なるのであれば別々のモデルとして定義するべきだ。
たまに見かける失敗例の一つなので、注意して欲しい(僕も何度かやらかした記憶があるが、それは秘密にしておこう)。

バッチ処理

さてここで少し視点を変えて、バッチ処理についても考えてみたい。
業務システムに於いて必ずといっていい程存在するのが、スケジュールバッチや夜間バッチなどと呼ばれるいわゆる "バッチ処理" だ。
現場の肌感覚で説明するといった以上、バッチ処理についても触れざるを得ない(こういった現場目線での具体例が専門書には少ないように思う)。
MVCに於けるコントローラと同様に、実はバッチ処理についても、バッチ本体に直接業務ロジックをダラダラと書き連ねるべきではない。
業務ロジックはサービスクラスとして分離独立させ、バッチからはそのサービスを呼ぶだけ、という様な構成が好ましい。
つまりバッチ本体は単純なコントローラーと同じような役割となるべきだ。
図6.png
こうすることで、バッチ処理についてもアーキテクチャの統一感をもたらし、モジュール間の疎結合を実現し、テストコードで保護するための基本構成が得られるようになる。

2. 依存関係を一方向にする

まず依存関係について少し掘り下げて説明しておこう。
あるクラス A とクラス B があるとする。
A が B を利用(呼び出しや参照)しているとすると、A は B に依存していることになる。次に、B からは A を一切利用していないとすると、B は A に依存していないことになる7
クラスだけでなくモジュールに於いても同様に依存関係を考えることが出来る。
先程少し触れたように、Service は View や Controller に依存しないようにしなくてはならない。
つまり、各モジュールは下位のモジュールにのみ依存するようにして欲しい。
言葉で説明するとイマイチぴんと来ないかもしれないので、下図を参照して頂きたい。
これまで提示してきた図を、
モジュールごとの上下関係(レイヤー)と依存関係に着目して書き直してみたものだ。
図8.png
このように、図の上の方にあるモジュールから下にあるモジュールを参照したり呼び出したりするのは構わないが、その逆はダメだ。
また、同列のモジュール間で相互に依存するようなことが無いように注意する8
モジュール間(そしてクラス間)の依存関係を上から下へ一方向にし、レイヤーを構成させるわけだ。
要点としては、ある下位のレイヤー(とその下)だけを取り出して、その部分だけでも構造が成り立つような仕組みになっている必要がある。
例えば Service とその依存関係にある下位のモジュール(ServiceModel, ViewModel, EntityModel)を取り出して、その部分だけで構造が成り立つようになっているのが分かるだろうか9
これがもし、Service から Controller への依存があったりすると、そうはいかない。Controller と Service が奇麗に分離されていないということになる。

3. テストコード

ここまでの構成が出来れば、とりあえずテストコードを書くことが出来る。
サービスクラスが下位のモジュールにしか依存していないので、そこだけ切り出してきて全く別のコードベースから呼び出すことが出来るはずだからだ(下図)10
図7.png
これまでモジュール分割だの依存関係だのと厳しい制約のもとソフトウェアの構造を整理してきた目的は、ここにある。

テストコードを書くことのメリット

今さらかもしれないが、テストコードを書くことのメリットをおさらいしておこう。
  • コード修正の度にデグレが発生していないか、何度でもテストを実行して検証することができる11
  • テストが必ず通る状態でリリースするため、品質が向上する
  • テストコードを書くこと自体にコストがかかるが、長い目で見ると全体的なコストは必ず下がる
  • テストコードを読むことでそのクラスの使い方(仕様)が分かる
  • 開発チームからすると、テストが通っているという絶対的な安心感
  • そもそもテストコードを書ける構造になっていることに価値がある
最後の項目が、最初に述べた 1, 2, 3 の相関関係の裏返しとなっている。
つまり、奇麗な構造になっていないとテストが書けないのだ。
しかしながらテストコードのないソフトウェアが依然として多い。大規模SIプロジェクトや、パッケージソフトではそのほとんどがテストコード無しという現状である。
一方で、テストコードを書かない文化というものもあるようで、あえてそのような選択をしている開発チームであればテストは書かなくてもよいと思う。しかし、テストを書いたこともないのに書かない選択をしているとしたら、是非この機会にチャレンジしてみて欲しい。
特に、プロダクトの初期構築の段階に携わっている場合は、テストコードを導入するチャンスだ。最初にテストコードが書ける構造を整備してテストを書いておかないと、後になってからテストを導入することは非常に骨の折れる作業なのだ。理由は、先述の通りソフトウェアの構造が奇麗になっていないとそもそもテストが書けないため、構造改革に甚大な労力を要するためだ。それを裏返せば、テストコードを維持し続けることでソフトウェアの構造が奇麗に保たれる、という効果が得られるのだ12

サービス間の依存関係への対処

さて話を続けよう。プロダクトの構築が終わって、さらに開発が続いていったとして、コードベースが発展し大規模化してくると、サービスクラスが多くなりサービス間の依存関係が増えてくる。その度に、それらの依存関係を少しづつ減らしていく努力が必要だ。そうしないと、テストコードを書くのがだんだん苦しくなってくる。
例えばインスタンス生成の依存関係を解消するにはいくつか対応策があるが13、それらの詳細な解説は他のサイトにお任せするとして、ここでは利用の依存関係を解消するために(王道とも言われる)インターフェースを抽出する方法を説明する。

依存したサービスを切り離す

説明のための例として、ここに「商品受注サービス」があるとする。これは読んで字のごとく商品の受注処理を行うビジネスロジックだと思って欲しい。
そしてその中では、受注データの納期をセットするために「納期検索サービス」を呼び出しているとする。
これらの依存関係を図で表すとこうなる。
図9.png
ここで「商品受注サービス」のテストを書くことを考えてみよう。
このサービス(クラス)のテストを書くためには、内部で利用している納期検索サービスのためのデータ(例えば納期マスタ)を用意しておく必要がある。そうなるとテストコードを書く量が増えることになる。
それに納期検索サービスにバグがある状態だったり、開発工程の関係でまだ実装されていないサービスだったりするかもしれない。
そもそもこれは単体テストというより、結合した状態でのテストになってしまっているのだ14
テストコードを書く負荷を減らすためには、テストの粒度を下げる必要がある。すなわち何とかしてこれを単体テストにする必要がある。
そこでインターフェースの出番だ。

インターフェースの抽出

まず納期検索サービスの中で必要なメソッドをインタフェースに抽出する。
そして「商品受注サービス」から「納期検索サービス」に依存するのではなく、そのインタフェースに依存するように変更する(下図)15
図10.png
これで果たして、単体テストが書けるようになる。
この後は、サンプルとして C# での実装例まで見てみてほしい。Java でも大体同じような感じだ。

C# での例(簡易テストコード)

いざテストを書くと言っても、書いたことない人にとっては全くどうしていいのか分からないことと思う。
Qiitaを読んでいるような意識高い系の人はそんなことないのかも知れないが。
まず心理的ハードルを下げるために一言いっておくと、重厚長大なテストフレームワークを無理に使う必要はないのだ。
エンタープライズアプリケーションの開発現場に於いて、テストフレームワークを新規導入するには時間と手間がかかるし、開発チームの他のメンバーに使い方を覚えてもらうための学習コストもかかるし、テストコードの書き方や適用範囲などの標準化まで考慮しないと、実際にテストコードを書く運用を始めることは難しいだろう(あと、大人の事情によって色々ハードルがありますよね)。
ここでは、簡易的にテストコードを書く方法もあるということを紹介するため、本質とはズレるかもしれないが、テストフレームワークを使わずに独立した実行モジュールを自作することでテストを書いてみる。C# を選択したのは好みの問題だ。
Javaでいうところの main() 関数を含むクラスを自分で書いて、コンパイルして java コマンドで実行する、.NET でいうところのコンソールアプリケーションを作って .exe を実行する、みたいなイメージだ。
納期検索サービス
// 納期の検索条件
public class DeliveryScheduleSearchConditionModel
{
    public string ProductCode { get; set; }  // 商品CD
}

// 納期の検索結果
public class DeliveryScheduleSearchResultModel
{
    public DateTime DeliveryScheduleDate { get; set; }  // 納期日付
}


// 納期検索サービスを表すインターフェース(本当のオブジェクト指向なら商品受注サービスの方に置く)
interface IDeliveryScheduleSearchSvc
{
    // 検索処理
    DeliveryScheduleSearchResultModel Search(DeliveryScheduleSearchConditionModel cond);
}

// 納期検索サービス
public class DeliveryScheduleSearchSvc : IDeliveryScheduleSearchSvc
{
    // 検索処理の実装
    public DeliveryScheduleSearchResultModel Search(DeliveryScheduleSearchConditionModel cond)
    {
        // 省略
    }
}
商品受注サービス
// 商品受注サービスの入力データ
public class ReceiveOrderInputModel
{
    public string ProductCode { get; set; }  // 商品CD
    public decimal Quantity { get; set; }  // 数量
    public decimal Amount { get; set; }  // 価格

    // 他にもいっぱい
}

// 商品受注サービスの処理結果
public class ReceiveOrderResultModel
{
    public ReceiveSatus Status { get; set; }  // 受注状態

    public DateTime DeliveryScheduleDate { get; set; }  // 納期
}

// 商品受注サービス
class ReceiveOrderSvc
{
    IDeliveryScheduleSearchSvc _deliveryScheduleSearchSvc;  // 納期検索サービスのインターフェース

    // コンストラクタ
    public ReceiveOrderSvc(IDeliveryScheduleSearchSvc deliveryScheduleSearchSvc)
    {
        this._deliveryScheduleSearchSvc = deliveryScheduleSearchSvc;
    }

    // 受注処理
    public ReceiveOrderResultModel Exec(ReceiveOrderInputModel input)
    {
        // 前処理(省略)

        // 納期検索サービスの入力データを作る
        var deliveryScheduleSearchcond = new DeliveryScheduleSearchConditionModel
        {
            ProductCode = input.ProductCode
        };
        // 納期検索サービスを呼ぶ
        var searchResult = _deliveryScheduleSearchSvc.Search(deliveryScheduleSearchcond);


        // 他にもいろいろ処理(省略)


        return new ReceiveOrderResultModel
        {
            DeliveryScheduleDate = searchResult.DeliveryScheduleDate  // 納期検索サービスの出力データを使う
        };
    }
}
テストコード
// テストコード(コンソールアプリケーション)
class TestProgram
{

    // テスト用の納期検索サービス(モック)
    class DeliveryScheduleSearchSvcForTest : IDeliveryScheduleSearchSvc
    {
        // 検索条件
        public DeliveryScheduleSearchConditionModel Cond;

        // 検索処理
        public DeliveryScheduleSearchResultModel Search(DeliveryScheduleSearchConditionModel cond)
        {
            this.Cond = cond;  // 引数を保存(テストコードで内容を検証できるように)

            // 都合のいいようにデータを作って返却する
            return new DeliveryScheduleSearchResultModel
            {
                DeliveryScheduleDate = new DateTime(2018, 12, 7)
            };
        }
    }

    static void Main(string[] args)
    {
        // 初期化処理(フレームワークやデータベースの初期化など)

        // 納期検索サービスのモックを作る
        var mock = new DeliveryScheduleSearchSvcForTest();

        // 商品受注サービスの入力データ
        var input = new ReceiveOrderInputModel
        {
            // 受注内容をセット
        };

        // 商品受注サービスにモックを渡してインスタンス生成
        var targetSvc = new ReceiveOrderSvc(mock);

        // 商品受注サービスを呼び出す
        var result = targetSvc.Exec(input);

        // 結果を検証
        AssertEqual(ReceiveSatus.SUCCESS, result.Status);
        AssertEqual(new DateTime(2018, 12, 7), result.DeliveryScheduleDate);
        // 以下、result の内容を検証する
    }

    // 検証用のメソッド(あくまで簡易な実装例。本当はもっと考慮すべきことがある)
    static void AssertEqual(object expected, object actual)
    {
        if (!object.Equals(expected, actual)) throw new Exception($"Assertion error! expected: {expected}, but actually: {actual}");
    }
}
こんな風に自力でテストを書けるはずだ16
余談になるが、Rubyなどのダックタイピングが出来る言語で同じような構造を考える際にはインターフェースを明示的に定義することはないが、僕の頭の中ではインターフェースがあるものとしてクラスを構成しているような感触がある。
動的言語の力によりインターフェースを定義しなくてよいのでコードの記述量が少なくなり、慣れればスッキリした気分で書けるが、その裏を返せば頭の中にインターフェースが見えない人にとっては、理解に時間を要したり、そもそも理解できないこともあるのだろうと思う。
そういう意味では、開発者のレベルに応じてプログラミング言語を選定することも大切なんだなと思う。

あとがき

変更に強いアーキテクチャの本質は、テストフレームワークを使うことでも、優秀な設計ツールを使うことでもない。
これまで見てきたように、適切にモジュール分割をして依存関係を整理し、それらをテストコードで保護していく事が本質だ。
それを実施するために必要な概念を整理して知識としてまとめたものがオブジェクト指向設計論であり、テストフレームであり、ドメイン駆動開発などの方法論だと僕は思っている。
方法論だけを論じても意味がない。本質を見極めて、現実世界での困りごとに適切に適用できるようになりたいものだ。
そうはいっても、テストフレームワークを使う方がずっと楽にテストが書けて運用しやすいので、自力で実装する例はあくまでお試し、こんなことも出来ますよ、という参考までにとどめておいて欲しい。一般的に流行しているようなフレームワークはとりあえず触れてみるなどして、継続して自分の知識をアップデートしていくべきだ。
ここまで、3ステップでの施策として構造設計の課題点と解決策を駆け足で見てきたが、そもそもそれらをどこまで適用するかの見極めも重要である。小規模でラピッドなプロトタイプ開発であれば、最初からコントローラに全部処理を書いてしまってもいいし、中規模まで発展することを見込むなら、ユニットテストだけでなくインテグレーションテストまで最初から用意する、みたいにケースバイケースで判断していくしかない。これにはやはりある程度の経験が必要だろう。
また、この記事では出来るだけ専門用語を避け、平易な表現で説明してきたが、向学の志がある方のために注釈にて専門用語の紹介を書いておいた。興味があれば自分で調べてみて欲しいと思う。
長くなったが、この記事によって一人でも多くの人が「テストコードを導入してみよう」と思うキッカケになれば幸いに思う。
現場からは以上だ。

  1. Fat Controller とも呼ばれ、アンチパターンの一つだ 
  2. Service という言葉には別の意味があるかもしれないが、SOA(サービス指向アーキテクチャ)という考え方があり、システムの規模が巨大化していくにつれて最終的には SOA に近い形態を目指すこととなるため、僕は最初からサービスという概念で設計していっている。 
  3. ここでいうサービスモジュールは、DDD(ドメイン駆動設計)ではドメインモデルという言葉で表されるものだ。 
  4. 本当はデータアクセスを抽象化する「リポジトリパターン」というアーキテクチャパターンもある。とりあえずここではテストコードからも実際のDBにアクセスする前提としている。 
  5. Model-View-ViewModel アーキテクチャ。Microsoft の WPF を始めフロントエンドライブラリでよく用いられる。DBに対応した Model とは別に、View に対応した ViewModel を用意し、画面項目にバインディングするような考え方。 
  6. この場合は MVC フレームワークの View はほぼ使わないことになる。VueJS や AngularJS などのフロントエンドで MVVM を実現するフレームワークを導入するなら、バックエンドは WebAPI としての役割に徹する方が分かりやすくなる。 
  7. 依存関係には度合いがあり、例えばUMLではクラスの継承(is-a), インスタンス生成(create)、所有(has-a)、利用(uses-a)というように分類わけされている。このうち利用(uses-)が最も弱い依存関係だとされており、インスタンス生成(create)は最も強い結合、プログラミング言語でいう new する事にあたる。new するには相手の実体を知っている必要があるためだ。 
  8. 循環参照といってアンチ設計パターンの一つだ 
  9. クリーンアーキテクチャという概念がこれに近い。実際、クリーンアーキテクチャという言葉は最近知ったので、それに影響を受けているわけではない。自分の勉強不足に反省。。。 
  10. テスト用にDBを分けて、テストコードからテスト用DBにアクセスするというやり方になる。本当はもっと色々考慮した方がいい事があるのは重々承知であるが、とりあえずテストコードを書くことは出来る状態になっている、ということの本質をここでは伝えたい。 
  11. リグレッションテスト(回帰テスト)という 
  12. "奇麗" の基準に個人差があるので賛否両論あるところかと思うが、少なくともここで言わんとしているモジュール分割と依存関係の一方向性は維持できるはずと考えている 
  13. 興味のある方は ファクトリーパターンや、DI(Dependency Injection: 依存性の外部注入)などのキーワードでググってみて欲しい。 
  14. もちろん結合テストのためのテストコードというものも存在する。Selenium などのテストフレームワークでは、ブラウザを実際にテストコードで操作してUIからバックエンドまでモジュール結合した状態でシナリオテストを行うようなことが出来る。インテグレーションテスト、e2eテスト(End to End Test)というキーワードで調べてみよう。 
  15. オブジェクト指向でいう依存関係逆転の原則である。その原則に厳密に従うなら、インタフェースは実は商品受注サービスの側のモジュールに属している必要がある。 
  16. なお、サンプルコード中にコメントで書いてあるように、実際にはフレームワークの初期化やDBの初期化などのお膳立てが必要となる。例えば EntityFramework なら App.config を用意するだけで何とかなるし、高級なフレームワークほど外部コードから呼び出すための手順などが整備されているようなので、詳細はフレームワークのドキュメントを参照されたい。実際、Java の Play2 + JPA の案件でこのようにして自力で簡易テストコードを書いたりしたことがあった。 




























from https://juejin.im/post/5d66a4875188250d9432a973

软件架构万字漫谈:业务架构、应用架构与云基础架构



软件架构万字漫谈:业务架构、应用架构与云基础架构

本部分节选自《软件架构设计
软件开发就是把一个复杂的问题分解为一系列简单的问题,再把一系列简单的解决方案组合成一个复杂的解决方案。而软件开发中最大的挑战,就是即能够快速高效地针对需求、环境的变化做出改变,也能够持续提供稳定、高可用的服务。而软件架构,就是软件系统的骨骼与框架。
所谓架构,见仁见智,很难有一个明确或标准的定义;但架构并非镜花水月或阳春白雪,有系统的地方就需要架构,大到航空飞机,小到一个电商系统里面的一个功能组件,都需要设计和架构。抽象而言,架构就是对系统中的实体以及实体之间的关系所进行的抽象描述,是对物/信息的功能与形式元素之间的对应情况所做的分配,是对元素之间的关系以及元素同周边环境之间的关系所做的定义。架构能将目标系统按某个原则进行切分,切分的原则,是要便于不同的角色进行并行工作,结构良好的创造活动要优于毫无结构的创造活动。
软件架构的核心价值,即是控制系统的复杂性,将核心业务逻辑和技术细节的分离与解耦。软件架构是系统的草图,它描述的对象是直接构成系统的抽象组件;各个组件之间的连接则明确和相对细致地描述组件之间的通信。在实现阶段,这些抽象组件被细化为实际的组件,比如具体某个类或者对象。在面向对象领域中,组件之间的连接通常用接口来实现。架构师的职责是努力训练自己的思维,用它去理解复杂的系统,通过合理的分解和抽象,理解并解析需求,创建有用的模型,确认、细化并扩展模型,管理架构;能够进行系统分解形成整体架构,能够正确的技术选型,能够制定技术规格说明并有效推动实施落地。

软件架构分类

在笔者的知识体系中,实际上将架构分为业务架构、应用架构、云基础架构这几大类,业务架构主要着眼于控制业务的复杂性,基础架构着眼于解决分布式系统中存在的一系列问题。无论何种架构,都希望能实现系统的可变的同时保障业务的高可用。另一个层面,根据企业中职责的划分,我们往往可以将软件架构,及关联的架构师划分为以下几类:
  • 业务架构/解决方案架构:核心是解决业务带来的系统复杂性,了解客户/业务方的痛点,项目定义,现有环境;梳理高阶需求和非功能性需求,进行问题域划分与领域建模等工作;沟通,方案建议,多次迭代,交付总体架构。
  • 应用架构:根据业务场景的需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性要求(性能、安全、稳定性等)。
  • 数据架构:专注于构建数据中台,统一数据定义规范,标准化数据表达,形成有效易维护的数据资产。打造统一的大数据处理平台,包括数据可视化运营平台、数据共享平台、数据权限管理平台等。
  • 中间件架构:专注于中间件系统的构建,需要解决服务器负载,分布式服务的注册和发现,消息系统,缓存系统,分布式数据库等问题,同时架构师要在 CAP 之间进行权衡。
  • 运维架构:负责运维系统的规划、选型、部署上线,建立规范化的运维体系。
  • 物理架构:物理架构关注软件元件是如何放到硬件上的,专注于基础设施,某种软硬件体系,甚至云平台,包括机房搭建、网络拓扑结构,网络分流器、代理服务器、Web 服务器、应用服务器、报表服务器、整合服务器、存储服务器和主机等。

架构模式与架构风格

软件架构设计的一个核心问题是能否使用重复的架构模式,即能否达到架构级的软件重用。也就是说,能否在不同的软件系统中,使用同一架构。当我们讨论软件架构时,常常会提及软件架构模式(Architectural Pattern)与软件架构风格(Architectural Style)。
软件架构模式往往会用于具体地解决某个具体的重复的架构问题,而架构风格则是对于某个具体的架构设计方案的命名。软件架构风格是描述某一特定应用领域中系统组织方式的惯用模式;架构风格反映了领域中众多系统所共有的结构和语义特性,并指导如何将各个模块和子系统有效组织成一个完整的系统。
在笔者的系列文章中,CRUD、分层架构、六边形架构、洋葱架构、REST 以及 DDD,都算是架构风格;而 CQRS、EDA、UDLA、微服务等则被划分到架构模式中。

系统复杂性的来源与应对

在软件开发中,程序员往往能够脱离现实规律的束缚,创造出天马行空的世界,其也是最具有创造力的活动之一。编程唯一需要的是创造力思维和思维组织能力,这意味着在软件开发过程中最大限制是理解我们正在创建的对象。随着软件的演进,加入更多的功能点,系统变得越来越复杂:各个模块(Module)间存在着各种微妙的依赖关系。系统的复杂性随着时间积累,对于程序员来说,修改系统时考虑周全所有的的相关因素变得越来越困难。这就会使软件开放进度变缓慢,并且引入 Bug,而导致会进一步延缓开发进度,增加开发成本。在任何一个系统的生命周期中,复杂性不可避免会增加;系统越大,需要更多的人开发,管理系统复杂性的工作就越困难。
Eric Evans 在 Domain‐Driven Design 一书中吐槽了所谓的意大利面式架构,即代码确实做了有用的事,但很难解释它是如何去执行的;他认为造成这种窘境的主要原因是,将领域问题的复杂度与技术细节的复杂度混合在了一起,最终导致整体复杂度的指数级增长。


复杂性不是凭空而来,很多时候也不是刻意为之,这也就意味着复杂性的增加往往不会以我们的主观意志为转移。就像房间里的大象,我们无法逃避,也不能视而不见。复杂性的来源可能是:
  • 吸积与持续迭代:增量式设计意味着软件设计永不结束,设计在系统的生命周期中持续发生,程序员要时刻考虑设计问题。增量开发也意味着持续重构。一个系统的初始设计几乎从来都不是最好的方案。随着经验的增加,必然会发现更好的设计方案。
  • 交互且无扩展性设计:当吸积效应导致的大规模系统,结合了交互这个特性,会使技术系统更加复杂。一个技术系统除了作用于自身,还会与其它大量系统产生交互。比如下单购买一件商品,那么订单系统,商品系统,支付系统,物流系统,卡券系统就会交互协作。这样吸积的复杂性,由于交互特性的出现,会呈现几何级数上升。
  • 不合理的业务封装:不合理的业务封装是一个相对宽泛的概念,其具体的表现譬如面向过程而不是对象、分层不合理等。
  • 缺乏统一语言:典型的敏捷开发的结构,流水线上的各个角色往往会专注于自己负责的环节,精细化的分工也限制了每个角色的全局视角;虽然我们经常提倡所谓的主人翁意识,但是在落地时又很难去推进。
  • 缺乏约束与规范:在团队协作开发的背景下,缺少规范和约束会严重损害架构的一致性(Consistency),代码的可维护性将急剧下降。可能规范在实现层面就是命名、分包等不影响代码运行的小问题,但是千里之堤,溃于蚁穴,正是这些微末的不注意导致了整体复杂性的雪崩。
复杂性的应对永远不会是一劳永逸,我们需要不断地推陈出新,是动态、渐进的重塑自己对软件系统的认识,不断认识问题和寻找更优解的持续迭代。第一个控制复杂性的途径是代码简单,意图清晰(Obvious)。例如: 减少特殊场景的处理,或变量命名一致性都能降低系统复杂性。另一种方式就是对复杂问题的抽象然后分而治之。

领域驱动设计


领域驱动设计

本部分节选自《领域驱动设计
DDD 领域驱动设计,起源于 2004 年著名建模专家 Eric Evans 发表的他最具影响力的著名书籍:《Domain-Driven Design – Tackling Complexity in the Heart of Software》,Eric Evans 在该书中只是提供了一套原始理论,并没有提供一套方法论,因此多年来对于 DDD 也是见仁见智。更早些时候 MartinFowler 曾经提出贫血模型与充血模型的概念,他认为我们大多数系统以 POJO 作为模型,只有普通的 getter、setter 方法,没有真正的行为,好像缺少血液的人,在 Evans 看来,DDD 中模型都是以充血形式存在,也就是说在 DDD 中,我们设计的模型不仅包含描述业务属性,还要包含能够描述动作的方法,不同的是,领域中一些概念不能用在模型对象,如仓储、工厂、服务等,如强加于模型中,将破坏模型的定义。

领域驱动设计架构

领域驱动设计的战略核心即是将问题域与应用架构相剥离,将业务语义显现化,把原先晦涩难懂的业务算法逻辑,通过领域对象(Domain Object),统一语言(Ubiquitous Language)转化为领域概念清晰的显性化表达出来。
  • 统一语言,软件的开发人员/使用人员都使用同一套语言,即对某个概念,名词的认知是统一的,建立清晰的业务模型,形成统一的业务语义。将模型作为语言的支柱。确保团队在内部的所有交流中,代码中,画图,写东西,特别是讲话的时候都要使用这种语言。例如账号,转账,透支策略,这些都是非常重要的领域概念,如果这些命名都和我们日常讨论以及 PRD 中的描述保持一致,将会极大提升代码的可读性,减少认知成本。。比如不再会有人在会议中对“工单”、“审核单”、“表单”而反复确认含义了,DDD 的模型建立不会被 DB 所绑架。
  • 面向领域,业务语义显性化,以领域去思考问题,而不是模块。将隐式的业务逻辑从一推 if-else 里面抽取出来,用通用语言去命名、去写代码、去扩展,让其变成显示概念;很多重要的业务概念,按照事务脚本的写法,其含义完全淹没在代码逻辑中没有突显出来。
  • 职责划分,根据实际业务合理划分模型,模型之间依赖结构和边界更加清晰,避免了混乱的依赖关系,进而增加可读性、可维护性;单一职责,模型只关注自身的本职工作,避免“越权”而导致混乱的调用关系。通过建模,更好的表达现实世界中的复杂业务,随着时间的发展,不断增加系统对实际业务的沉淀,也将更好的通过清晰的代码描述业务逻辑,模型的内聚增加了系统的高度模块化,提升代码的可重用性,对比传统三层模式中,很有可能大量重复的功能散落在各个 Service 内部。

微服务与云原生架构

本部分节选自《微服务与云原生

服务衍化

单体分层架构

在 Web 应用程序发展的早期,大部分工程是将所有的服务端功能模块打包到单个巨石型(Monolith)应用中,譬如很多企业的 Java 应用程序打包为 war 包,最终会形成如下的架构:


巨石型应用易于搭建开发环境、易于测试、易于部署;其缺陷也非常明显,无法进行局部改动与部署,编译时间过长,回归测试周期过长,开发效率降低等。集中式架构分为标准的三层:数据访问层、服务层和 Web 层。
在 Web2.0 时代刚刚流行的时候,互联网应用与企业级应用并没有本质的区别,集中式架构分为标准的三层:数据访问层、服务层和 Web 层。
  • 数据访问层用于定义数据访问接口,实现对真实数据库的访问;
  • 服务层用于对应用业务逻辑进行处理;
  • Web 层用于处理异常、逻辑跳转控制、页面渲染模板等。

SOA 面向服务架构

SOA(Service-Oriented Architecture) 面向服务架构,是在互联网应用规模迅速增长,集中式架构已无法做到无限制地提升系统的吞吐量的背景下,产生的涉及模块化开发、分布式扩展部署等相对宽泛的概念。


SOA 是一个组件模型,它将应用程序的不同功能单元(称为服务)通过这些服务之间定义良好的接口和契约联系起来。SOA 中的接口独立于实现服务的硬件平台、操作系统和编程语言,采用中立的方式进行定义。这使得构建在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。面向服务架构,它可以根据需求通过网络对松散耦合的粗粒度应用组件进行分布式部署、组合和使用。服务层是 SOA 的基础,可以直接被应用调用,从而有效控制系统中与软件代理交互的人为依赖性。
实施 SOA 的关键目标是实现企业 IT 资产的最大化作用。要实现这一目标,就要在实施 SOA 的过程中牢记以下特征:可从企业外部访问、随时可用、粗粒度的服务接口分级、松散耦合、可重用的服务、服务接口设计管理、标准化的服务接口、支持各种消息模式、精确定义的服务契约。


服务消费者(Service Consumer)可以通过发送消息来调用服务,这些消息由一个服务总线(Service Bus)转换后发送给适当的服务实现。这种服务架构可以提供一个业务规则引(Business Rules Engine),该引擎容许业务规则被合并在一个服务里或多个服务里。这种架构也提供了一个服务管理基础(Service Management Infrastructure),用来管理服务,类似审核,列表(billing),日志等功能。此外,该架构给企业提供了灵活的业务流程,更好地处理控制请求(Regulatory Requirement),例如 Sarbanes Oxley(SOX),并且可以在不影响其他服务的情况下更改某项服务。
由于分布式系统十分复杂,因此产生了大量的用于简化分布式系统开发的分布式中间件和分布式数据库,服务化的架构设计理念也被越来越多的公司所认同。如下是 Dubbo 官方文档公布了一张有关 SOA 系统演化过程的图片:


MSA 微服务架构

微服务(Microservices Architecture Pattern)由 Martin Fowler 在 2014 年提出的,是希望将某个单一的单体应用,转化为多个可以独立运行、独立开发、独立部署、独立维护的服务或者应用的聚合,从而满足业务快速变化及分布式多团队并行开发的需求。如康威定律(Conway’s Law)所言,任何组织在设计一套系统(广义概念)时,所交付的设计方案在结构上都与该组织的通信结构保持一致,微服务与微前端不仅仅是技术架构的变化,还包含了组织方式、沟通方式的变化。


对于微服务,不同背景的人也有不同的见解,对于熟悉 SOA 的开发者,微服务也可以认为是去除了 ESB 的 SOA 的一种实现方案;ESB 是 SOA 架构中的中心总线,设计图形应该是星形的,而微服务是去中心化的分布式软件架构。SOA 更多强调重用,而微服务偏向于重写。SOA 偏向水平服务,微服务偏向垂直服务;SOA 偏向自上而下的设计,微服务偏向自下而上的设计。


微服务与微前端原理和软件工程,面向对象设计中的原理同样相通,都是遵循单一职责(Single Responsibility)、关注分离(Separation of Concerns)、模块化(Modularity)与分而治之(Divide & Conquer)等基本的原则。从巨石型应用到微服务的衍化也并非一蹴而就,如下图也演示了简单的渐进式替代过程:




Cloud Native 云原生架构

云原生是通过构建团队、文化和技术,利用自动化和架构来管理系统的复杂性和解放生产力。 — Joe Beda,Heotio CTO,联合创始人


Pivotal 是云原生应用的提出者,并推出了 Pivotal Cloud Foundry 云原生应用平台和 Spring 开源 Java 开发框架,成为云原生应用架构中先驱者和探路者。早在 2015 年 Pivotal 公司的 Matt Stine 写了一本叫做迁移到云原生应用架构的小册子,其中探讨了云原生应用架构的几个主要特征:符合 12 Factors 应用、面向微服务架构、自服务敏捷架构、基于 API 的协作以及抗脆弱性。2015 年 Google 主导成立了云原生计算基金会(CNCF),起初 CNCF 对云原生(Cloud Native)的定义包含以下三个方面:应用容器化、面向微服务架构、应用支持容器的编排调度。


云原生应用程序简单地定义为从头开始为云计算架构而构建应用程序;这意味着,如果我们将应用程序设计为预期将部署在分布式、可扩展的基础架构上,我们的应用程序就是云原生的。随着公共云将承载越来越多的算力,未来云计算将是主流的 IT 能力交付方式,CNCF 也对云原生进行了重新定义:云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用;云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式 API。
  • Codeless 对应的是服务开发,实现了源代码托管,你只需要关注你的代码实现,而不需要关心你的代码在哪,因为在整个开发过程中你都不会感受到代码库和代码分支的存在。
  • Applicationless 对应的是服务发布,在服务化框架下,你的服务发布不再需要申请应用,也不需要关注你的应用在哪。
  • Serverless 对应的则是服务运维,有了 Serverless 化能力,你不再需要关注你的机器资源,Servlerless 会帮你搞定机器资源的弹性扩缩容
这些技术组合搭配,能够构建容错性好、易于管理和便于观察的松耦合系统;再结合可靠的自动化手段,云原生技术能够使工程师轻松地对系统作出频繁和可预测的重大变更。由此可见,云原生是保障系统能力灵动性地有效抓手;云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。微服务架构非常适合云原生应用程序;但是,云原生同样存在着一定的限制,如果你的云原生应用程序部署在 AWS 等公有云上,则云原生 API 不是跨云平台的。


云原生应用的关键属性包括了:使用轻量级的容器打包、使用最合适的语言和框架开发、以松耦合的微服务方式设计、以 API 为中心的交互和协作、无状态和有状态服务在架构上界限清晰、不依赖于底层操作系统和服务器、部署在自服务、弹性的云基础设施上、通过敏捷的 DevOps 流程管理、自动化能力、通过定义和策略驱动的资源分配。云原生是分布式应用当下重要的发展路径,其终态应当是 Distributionless,所有与分布式相关的问题由云平台解,分布式应用的开发会跟传统应用的开发一样方便,甚至更加便捷。

云基础架构


应用基础架构变迁

虚拟机

虚拟机由某些特定的硬件和内核虚拟化组成,运行客户操作系统。称为管理程序的软件创建虚拟化硬件,其可以包括虚拟磁盘,虚拟网络接口,虚拟 CPU 等。虚拟机还包括可以与此虚拟硬件通信的宾客内核。管理程序可以托管,这意味着它是一些在主机操作系统(MacOS)上运行的软件,如示例中所示。它也可以是裸机,直接在机器硬件上运行(替换你的操作系统)。无论哪种方式,管理程序方法都被认为是重量级的,因为它需要虚拟化多个部分(如果不是全部硬件和内核)。


VM 需要硬件虚拟化才能实现机器级隔离,而容器则只需要在同一操作系统内进行隔离操作。 随着隔离空间数量的增加,开销差异变得非常明显。

容器

在过去几年里,云平台发展迅速,但其中困扰运维工程师最多的,是需要为各种迥异的开发语言安装相应的运行时环境。虽然自动化运维工具可以降低环境搭建的复杂度,但仍然不能从根本上解决环境的问题。


Docker 的出现成为了软件开发行业新的分水岭,容器技术的成熟也标志着技术新纪元的开启。Docker 提供了让开发工程师可以将应用和依赖封装到一个可移植的容器中的能力,这项举措使得 Docker 大有席卷整个软件行业并且进而改变行业游戏规则的趋势,这像极了当年智能手机刚出现时的场景——改变了整个手机行业的游戏规则。Docker 通过集装箱式的封装方式,让开发工程师和运维工程师都能够以 Docker 所提供的镜像分发的标准化方式发布应用,使得异构语言不再是捆绑团队的枷锁。


容器是包含应用程序代码,配置和依赖关系的软件包,可提供运营效率和生产力。容器为我们提供了可预测的,可重复的和不可变的运行预期,容器的兴起是 DevOps 即服务的一个巨大推动因素,可以克服当今面临的最大安全障碍。容器化通过在操作系统级别进行虚拟化来使应用程序可移植,从而创建基于内核的隔离的封装系统。容器化的应用程序可以放在任何地方,无需依赖项运行或需要整个 VM,从而消除了依赖关系。
作为独立的单元,容器能够在任何主机操作系统,CentOS,Ubuntu,MacOS,甚至是像 Windows 这样的非 UNIX 系统中运行。容器还充当标准化的工作或计算单元。一个常见的范例是每个容器运行单个 Web 服务器,数据库的单个分片或单个 Spark 工作程序等,只需要扩展容器的数量就能够便捷地扩展应用。每个容器都有一个固定的资源配置(CPU,RAM,线程数等),并且扩展应用程序需要只扩展容器的数量而不是单个资源原语。当应用程序需要按比例放大或缩小时,这为工程师提供了更容易的抽象。容器也是实现微服务架构的一个很好的工具,每个微服务只是一组协作容器。例如,可以使用单个主容器和多个从容器来实现 Redis 微服务。

Kubernetes 与编排

随着虚拟化技术的成熟和分布式架构的普及,用来部署、管理和运行应用的云平台被越来越多地提及。IaaS、PaaS 和 SaaS 是云计算的三种基本服务类型,分别表示关注硬件基础设施的基础设施即服务、关注软件和中间件平台的平台即服务,以及关注业务应用的软件即服务。容器的出现,使原有的基于虚拟机的云主机应用,彻底转变为更加灵活和轻量的容器与编排调度的云平台应用。


然而容器单元越来越散落使得管理成本逐渐上升,大家对容器编排工具的需求前所未有的强烈,Kubernetes、Mesos、Swarm 等为云原生应用提供了强有力的编排和调度能力,它们是云平台上的分布式操作系统。容器编排是通常可以部署多个容器以通过自动化实现应用程序的过程。像 Kubernetes 和 Docker Swarm 这样的容器管理和容器编排引擎,使用户能够指导容器部署并自动执行更新,运行状况监视和故障转移过程。
Kubernetes 是目前世界范围内关注度最高的开源项目,它是一个出色的容器编排系统,用于提供一站式服务。Kubernetes 出身于互联网行业巨头 Google,它借鉴了由上百位工程师花费十多年时间打造的 Borg 系统的理念,安装极其简易,网络层对接方式十分灵活。Kubernetes 和 Mesos 的出色表现给行业中各类工程师的工作模式带来了颠覆性的改变。他们再也不用关注每一台服务器,当服务器出现问题时,只要将其换掉即可。业务开发工程师不必再过分关注非功能需求,只需专注自己的业务领域即可。而中间件开发工程师则需要开发出健壮的云原生中间件,用来连接业务应用与云平台。
Kubernetes、Service Mesh 和 Serverless 三者共同演绎不同层次的封装和向上屏蔽下面的细节。Kubernetes 引入了不同的设计模式,实现对各种云资源全新、有效和优雅的抽象和管理模式,让集群的管理和应用发布变成了件相当轻松且不易出错的事。被广泛采用的微服务软件架构将分布式应用的各种复杂度迁移到了服务之间,如何通过全局一致、体系化、规范化和无侵入的手段进行治理就变成了微服务软件架构下至关重要的内容。Kubernetes 细化的应用程序的分解粒度,同时将服务发现、配置管理、负载均衡和健康检查等作为基础设施的功能,简化了应用程序的开发。而 Kubernetes 这种声明式配置尤其适合 CI/CD 流程,况且现在还有如 Helm、Draft、Spinnaker、Skaffold 等开源工具可以帮助我们发布 Kuberentes 应用。


Service Mesh 通过将各服务所共用和与环境相关的内容剥离到部署于每个服务边上的 Sidecar 进程而轻松地做到了。这一剥离动作使得服务与平台能充分解耦而方便各自演进与发展,也使得服务变轻而有助于改善服务启停的及时性。Service Mesh 因为将那些服务治理相关的逻辑剥离到了 Sidecar 中且作为独立进程,所以 Sidecar 所实现的功能天然地支持多语言,为上面的服务采用多语言开发创造了更为有利的条件。通过 Service Mesh 对整个网络的服务流量进行技术收口,让异地多活这样涉及流量调度的系统工程实现起来更加优雅、简洁与有效,也能更加方便地实现服务版本升级时的灰度、回滚而改善安全生产质量。由于技术收口,给服务流量的治理和演进、排错、日志采集的经济性等疑难问题创造了新的发展空间。

延伸阅读



您可以通过以下导航来在 Gitbook 中阅读笔者的系列文章,涵盖了技术资料归纳、编程语言与理论、Web 与大前端、服务端开发与基础架构、云计算与大数据、数据科学与人工智能、产品设计等多个领域:
此外,你还可前往 xCompass 交互式地检索、查找需要的文章/链接/书籍/课程;或者在 MATRIX 文章与代码索引矩阵中查看文章与项目源代码等更详细的目录导航信息。最后,你也可以关注微信公众号:『某熊的技术之路』以获取最新资讯。





from http://www.uml.org.cn/zjjs/2016052302.asp

在首席架构师手里,应用架构如此设计
 
 4389 次浏览     评价:     
 2016-5-23 
 
无架构,不系统,架构是大型系统的关键。从形上看,架构是系统的骨架,支撑和链接各个部分;从神上看,架构是系统的灵魂,深刻体现业务本质。
架构可细分为业务架构、应用架构、技术架构,业务架构是战略,应用架构是战术,技术架构是装备。其中应用架构承上启下,一方面承接业务架构的落地,另一方面影响技术选型。
如何针对当前需求,选择合适的应用架构,如何面向未来,保证架构平滑过渡,这个是软件开发者,特别是架构师,都需要深入思考的问题。本文基于作者在大型互联网系统的实践和思考,和大家一起探讨应用架构的选型。
本文主要内容包括:
应用架构本质
. 单体式
. 分布式
. SOA架构
. SOA落地方式
. 应用架构进化
. 应用架构本质
. 应用作为独立可部署的单元,为系统划分了明确的边界,深刻影响系统功能组织、代码开发、部署和运维等各方面,应用架构定系统有哪些应用、以及应用之间如何分工和合作
分有两种方式,一种是水平分,按照功能处理顺序划分应用,比如把系统分为web前端/中间服务/后台任务,这是面向业务深度的划分。另一种是垂直分,按照不同的业务类型划分应用,比如进销存系统可以划分为三个独立的应用,这是面向业务广度的划分。
应用的合反映应用之间如何协作,共同完成复杂的业务case,主要体现在应用之间的通讯机制和数据格式,通讯机制可以是同步调用/异步消息/共享DB访问等,数据格式可以是文本/XML/JSON/二进制等。
应用的分偏向于业务,反映业务架构,应用的合偏向于技术,影响技术架构。分降低了业务复杂度,系统更有序,合增加了技术复杂度,系统更无序。
应用架构的本质是通过系统拆分,平衡业务和技术复杂性,保证系统形散神不散。
系统采用什么样的应用架构,受业务复杂性影响,包括企业发展阶段和业务特点;同时受技术复杂性影响,包括IT技术发展阶段和内部技术人员水平。业务复杂性(包括业务量大)必然带来技术复杂性,应用架构目标解决业务复杂性的同时,避免技术太复杂确保业务架构落地
常见的应用架构有多种,下面根据系统拆分方式,以及如何平衡业务与技术的角度进行分析,讨论各自的适用性,给企业应用架构选型提供参考。
单体式应用
架构模型
系统只有一个应用,相应地,代码放在一个工程里管理;打包成一个应用;部署在一台机器;在一个DB里存储数据。单体式应用的架构如下图所示:
单体式应用采用分层架构,按照调用顺序,从上到下一般为表示层、业务层、数据访问层、DB层,表示层负责用户体验,业务层负责业务逻辑,数据访问层负责DB层的数据存取。
单体应用在水平方向上,上下层之间职责划分清晰;但垂直方向上缺乏清晰的边界,上下层模块之间是多对多的依赖关系,比如业务模块1
(图中BO1)可能调用数据层所有模块DAO1~3, DAO1也可能被业务层所有模块BO1~3调用。
单体应用通过水平分层,降低了业务复杂性;同时模块之间是进程内部调用,技术实现简单。
但单体应用对系统的切分不彻底,只有水平切分,并且是逻辑上,因此适合业务比较单一,但深度上比较复杂的系统,比如TCP/IP网络通讯,从应用层/传输层/网络层/链路层,层层推进,类似这样的系统可以方便地增加水平层次去适配。
对于广度上复杂的业务,由于缺乏垂直切分,强行把不同业务绑定在一起,整个系统神散形不散,带来一系列问题。比如OTA网站包含机票/酒店/旅游等多个垂直业务板块,每块都比较独立,就不适合放在一起开发维护。
优缺点
单体式应用的优点和缺点都很鲜明,如下图所示。
单体式应用的优点明显:
现有IDE都是集成开发环境,非常适合单体式应用,开发、编译、调试一站式搞定。
一个应用包含所有功能,容易测试和部署。
运行在一个物理节点,环境单一,运行稳定,故障恢复简单。
单体式应用的缺点也明显:
业务边界模糊,模块职责不清晰,当系统逐渐变大,代码依赖复杂,难以维护。
所有人同时在一个工程上开发,容易发生代码修改冲突,依赖复杂导致项目协调困难,并且局部修改影响不可知,需要全覆盖测试,需要重新部署,难以支持大团队并行开发。
当系统很大时,编译和部署耗时。
应用水平扩展难,一方面状态在应用内部管理,无法透明路由;另一方面,不同模块对资源需求差异大,当业务量增大时,一视同仁地为所有模块增加机器导致硬件浪费。
作者之前曾在一家大型电商公司工作,当时整个网站是一个单体应用,有数百万行代码,有专门的团队负责代码合并,有专门的团队负责编译脚本开发,有一套复杂的火车模型协调不同团队,整套流程体系很精密很复杂,但这何尝不是单体应用的无奈和代价。
分布式架构应用
架构模型
在分布式应用架构中,应用相互独立,每个应用代码独立开发,独立部署,应用通过有限的API接口互相关联。API接口属于应用一部分,一般和表示层处于同一层次,两者共享业务逻辑层,API和表示层采用同样的web端技术,通讯协议一般使用HTTP,数据格式是JSON,应用集成方式比较简化。
分布式架构首先对系统按照业务进行垂直切分,对广度上复杂的业务实现物理解耦,应用内部还是水平切分,对深度上复杂的业务实现逻辑解耦。分布式架构也可以解决业务量大的问题,对于某些高并发/大流量系统,把系统切分为不同应用,针对资源需求特点(比如CPU/IO/存储密集型),通过加强硬件配置满足不同应用的需求,避免一刀切方式带来的资源浪费。
技术上,API采用标准的HTTP/JSON进行通讯,调用双方实现难度都不大,但是API
一般是“裸奔”的,在系统层面,调用依赖关系不透明,调用可靠性缺乏保障,因此只适用应用之间依赖链路少,调用量不大的系统,即应用之间耦合确实够松的系统。
优缺点
分布式架构每个应用内部高内聚,独立开发、测试和部署,支持开发敏捷;同时应用之间松耦合,业务边界清晰,业务依赖明确,支持大项目并行开发,实现业务敏捷。
在分布式架构中,应用的表示层和API没有物理分离,需要同时满足自身业务需求和关联业务需求,相互影响,比如API接口会随着外部应用的需求经常变化,这会导致整个应用重新部署。
运行时,API以HTTP/REST方式通过网络对外提供接口,其通信可靠性和数据的封装性相对于进程内调用比较差,如果没有一些可靠的技术机制,如路由保障/失败重试/监控等,
裸奔的API方式将严重影响系统整体可用性。
SOA架构
架构模型
广义上,SOA也是分布式应用架构一种,但内涵不同。
这里有两种类型“应用”,应用1~N是前端应用,面向用户,服务1~N是service,只提供业务逻辑和数据。这些应用都是独立部署,前端应用不再通过API直接关联,而是通过后端服务共享业务逻辑。
此外相对于“裸奔”的API,SOA架构提供配套的服务治理,包括服务注册、服务路由、服务授权、服务降级、服务监控等等。这些功能通过专门的中间件支持,有中心化和去中心化两种方式,具体技术实现机制和适用场景,网上有很多专门介绍,这里就不展开了。
SOA架构在分布式架构垂直切分的基础上,进一步把原来单体应用的业务逻辑层独立成service,做到物理上的彻底分离
每个service专注于特定职责,实现系统核心业务逻辑,service之间通过互相调用,可以完成复杂业务逻辑,解决业务深度上的问题;同时service面向众多的应用,以共享的方式支持逻辑复用。所以,SOA架构既体现业务的分,又体现业务的合,更多地从业务整体上考虑系统拆分。
相比分布式应用架构,基于SOA的系统有大量的service应用,整个系统基于服务调用,所以对服务依赖的透明性和服务调用的可靠性提出很高要求,需要专门的SOA框架支持,还需要配套的监控体系和自动化的运维系统支持,技术复杂性高,SOA架构可以集中体现一个企业的IT技术能力。
优缺点
SOA架构优缺点如下图所示:
相比较普通API方式,SOA架构更进一步:
1)每个service都是浓缩的精华,聚焦某方面核心业务,同时以复用的方式供整个系统共享。
2)服务作为独立的应用,独立部署,接口清晰,很容易做自动化测试和部署。
3)服务是无状态的,很容易做水平扩展;通过容器虚拟化技术,实现故障隔离和资源高效利用,业务量大的时候,加机器即可。
4)基于SOA的系统可以根据服务运行情况,灵活调控服务资源,包括服务上下架、服务升降级等,使系统真正具备可运营的能力。
当然天下没有免费的午餐,SOA也带来了额外复杂性和弊端:
1)系统依赖复杂,给开发/测试/部署带来一系列挑战。
2)端到端的调用链路长,可靠性降低,依赖网络状况、服务框架及具体service的质量。
3)分布式数据一致性和分布式事务支持困难,一般通过最终一致性简化解决。
4)端到端的测试和排障复杂,SOA对运维提出更高要求。
SOA落地方式
在实践中,SOA架构不断深入发展,具体落地形式也多种多样。
1)面向外部SOA
SOA的前身是web service,web service初衷是企业之间通过Internet进行互联,如下图所示:
每个公司把自己的优势资源通过web service发布,如图中天气服务/机票服务/酒店预订服务来自不同公司,其他企业可以直接调用服务或整合多个服务,实现企业间资源共享。
2)面向应用SOA
面向应用SOA把原单体应用里的业务逻辑层剥离出来,作为单独的服务对外提供。
举一个电商的例子,这里有两个应用,顾客使用的商品详情页,展示商品的信息、商品库存,商品价格;内部客服人员使用的客服系统,根据顾客来电要求,修改订单,同时也需要获取商品的基本信息、价格信息等。
经过SOA改造后,应用架构如下图所示:
这里的service实现底层数据对于前端页面的透明化,表示层和业务逻辑各自独立维护,同时全部业务逻辑对其他应用开放,新应用可以自由整合来自多个服务的接口,快速支持业务创新。
面向应用的SOA架构对系统进行物理上的水平切分,结果是表示层单飞,逻辑层对外全开放。但每个service是原系统业务逻辑的封装,接口设计面向原应用的业务case,如果其它应用的需求有差异,则自己创建service访问底层数据。如此导致service职责不够聚焦,类似的接口分散化,同时底层数据表被多方访问,数据模型修改困难,数据一致性难以保障。
最终系统整体依赖复杂,容易形成网状结构,修改时,往往牵一发动全身。
3)微内核SOA
每个企业都有自己的核心数据,比如对于电商系统来说,用户/商品/订单/库存/价格都是核心数据,称之为主数据。微内核SOA聚焦各类主数据,封装相关表的所有访问,架构示意如下:
每个服务独占式地封装对应主数据表的访问,这些服务构成系统的基础服务,一起组成系统的微内核,供所有上层应用共享。
微内核服务是原子服务,接口粒度比较细,可以在其上构造聚合服务,为上层应用提供粗粒度服务。可以是信息聚合,比如图中商品聚合服务整合商品的基本信息/库存/价格;也可以是流程聚合,比如下单接口,调用来自多个服务的接口,共同完成复杂的下单操作。
这里服务是分层次的,聚合服务是上层,基础服务是底层,依赖规则如下:
上层服务可以调用同层服务和基础服务
基础服务是原子服务,不可相互调用
前端应用可调用聚合服务和跨层调用基础服务
微内核的微表示服务数量有限,接口粒度细;内核表示这些基础服务处于调用底层,负责核心数据和业务,这和操作系统的内核概念上类似,主数据相当于核心的硬件,如cpu/内存/外存等,微内核的各个基础服务分别负责这些核心资源的管理,屏蔽底层的复杂性,对上层应用提供统一入口和透明化访问。
最近微服务很火,其特征是职责单一、接口粒度细、轻量级通讯协议等,微内核SOA架构有微服务的形,同时有业务内核的神,是架构形散神不散思想的很好体现,这个在淘宝、京东、一号店等大型电商系统都已有丰富实践。
4)方式比较
面向企业外部SOA,业务场景有特殊性,不深入分析,这里主要比较面向应用SOA和微内核SOA的区别,一个大型B2C电商系统,应用和主数据是多对多依赖关系,如下图所示:
面向应用的服务从特定应用出发,整合应用对相关数据的访问需求;微内核服务从特定主数据为中心,收敛各个业务对数据的访问需求。
在面向应用的服务设计下,数据表的访问入口是发散的,来自多个应用,这带来一系列弊端:
1)数据模型碎片化
每个应用都会基于自己的需求,往表里加字段,很多电商的商品表/订单表有多达200个字段,都是野蛮增长,缺少控制的结果。
2)数据模型修改困难
类似的访问需求散布在多个服务,缺乏整合,同时表schema修改会影响很多服务和应用。
3)连接资源利用率低
多个服务直连数据库,并且每个服务会尽可能多地配置连接数,在应用数量多,业务并发量大的情况下,往往导致数据库连接数不够。
微内核SOA通过收敛对主数据的访问,保证数据模型一致性、优化接口和有效利用数据库连接资源。同时通过服务分层,简化系统依赖关系。更为重要的是,微内核服务保证了业务模型的一致性,比如电商系统的商品体系,包含单品/系列商品/组合商品/搭售商品/虚拟商品等一系列复杂概念,这些复杂逻辑在基础商品服务里处理,对上层业务透明一致。
如果业务模式简单,应用数量少,特定主数据的访问绝大多数(比如说80%)来自某个应用,则服务设计以应用为中心是可以的,不利影响比较小。
对于大型互联网系统,业务广度和深度都复杂,但无论多复杂的系统,主数据类型都是有限的,可以通过聚焦有限的基础业务,以此支持无限的应用业务,结果是底层业务模型稳定,上层业务可以灵活扩展。
面向应用的服务设计是SOA初级阶段,从具体业务着手,自底向上,难度小;微内核服务设计是SOA高级阶段,从全局着手,对业务和数据模型高度抽象,自顶向下,难度大。
值得注意的是,在提供基础服务同时,每个应用也可以创建自己需要的服务(但主数据的访问必须通过基础服务),所以微内核的服务和面向应用的服务可以有机结合在一起,当业务应用变得很多,并且不断增长,可以考虑逐步往基础服务过渡,整合特定主数据有关的业务逻辑。
顺便提一下,应用架构会影响组织架构,如果采用面向应用的服务设计,具体service一般由相关应用的团队负责;如果是微内核的服务设计,一般由单独的共享服务部门负责所有基础服务开发,和各个业务研发部门并列,保证设计的中立性和需求响应的及时性。
应用架构的进化
软件是人类活动的虚拟,业务架构是生产活动的体现,应用架构是具体分工合作关系的体现。
单体应用类似原始氏族时代,氏族内部有简单分工,氏族之间没有联系;分布式架构类似封建社会,每个家庭自给自足,家庭之间有少量交换关系;SOA架构类似工业时代,企业提供各种成品服务,我为人人,人人为我,相互依赖。微内核的SOA架构类似后工业时代,有些企业聚焦提供水电煤等基础设施服务,其他企业在之上提供生活服务,依赖有层次。
业务架构是生产力,应用架构是生产关系,技术架构是生产工具。业务架构决定应用架构,应用架构需要适配业务架构,并随着业务架构不断进化,同时应用架构依托技术架构最终落地。
企业一开始业务比较简单,比如进销存,此时面向内部用户,提供简单的信息管理系统(MIS),支持数据增删改查即可,单体应用可以满足要求。
随着业务深入,进销存每块业务都变复杂,同时新增客户关系管理,以更好支持营销,业务的深度和广度都增加,这时需要对系统按照业务拆分,变成一个分布式系统。
更进一步,企业转向互联网+战略,拓展在线交易,线上系统和内部系统业务类似,没必要重做一套,此时把内部系统的逻辑做服务化改造,同时供线上线下系统使用,变成一个简单的SOA架构。
紧接着业务模式越来越复杂,订单、商品、库存、价格每块玩法都很深入,比如价格区分会员等级,访问渠道(无线还是PC),销售方式(团购还是普通)等,还有大量的价格促销,这些规则很复杂,容易相互冲突,需要把分散到各个业务的价格逻辑进行统一管理,以基础价格服务的方式透明地提供给上层应用,变成一个微内核的SOA架构。
同时不管是企业内部用户,还是外部顾客所需要的功能,都由很多细分的应用提供支持,需要提供portal,集成相关应用,为不同用户提供统一视图,顶层变成一个AOA的架构(application orientated architecture)。
随着业务和系统不断进化,最后一个比较完善的大型互联网应用架构如下图所示:
最终整个系统化整为零,形神兼备,支持积木式拼装,支持开发敏捷和业务敏捷?
应用架构,需要站在业务和技术中间,在正确的时间点做正确的架构选择,保证系统有序进化





























留言

熱門文章