求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
系统架构师-基础到企业应用架构系列(三)
 

2010-10-11 作者:CallHot 来源:CallHot的blog

 

系列之---系统设计规范与原则[上篇]

一、上章回顾

在上篇中我们讲解了几类UML2.0语言新推出的建模图形,总体来说通过这些图形能更详细的将某类信息表达出来。在这里我们简单回顾上篇讲解的内容。

image

上图中已经简单介绍了上章讲述的内容,具体内容请看:系统架构师-基础到企业应用架构-系统建模[下篇]

二、摘要

本章将主要的简单介绍在系统架构中的设计模式及相应规范准则。并结合相应的代码来说明如何遵循系统架构中的一些基本的设计规范及准则。而我们将在本文介绍几类常用的设计规范,我们先来看看结构化设计的二个基本原则:

image

当然既然提出了基本的准则,那么我们如何来满足准则呢,并且能更好的设计呢?我们可以通过如下手段来达到这样的要求:

image

当然图中演示了功能分离的策略,通过把需求按照不同的功能详细的划分开来,每个功能都是独立的,当然我们这里也可以成为是关注点。下面我们将针对这些原则进行详细的讲解。

三、本章内容

1、上章回顾。

2、摘要。

3、本章内容。

4、设计规范及原则。

5、如何满足设计要求。

6、本章总结。

7、系列进度。

8、下篇预告。

四、设计规范及准则。

1、高内聚

首先我们来看看内聚的含义:软件含义上的内聚其实是从化学中的分子的内聚演变过来的,化学中的分子间的作用力,作用力强则表现为内聚程度高。在软件中内聚程度的高低,标识着软件设计的好坏。

我们在进行架构设计时的内聚高低是指,设计某个模块或者关注点时,模块或关注点内部的一系列相关功能的相关程度的高低。

例如:下单模块:

image

一般情况下,下单模块都会有如下的信息,订单的信息,产品的信息及谁下的单(买家信息)。这是基本的,那么我们设计的时候就要把相关的功能内聚到一起。当然这是从大功能(下单管理)上来说,当然这些模块还可以再细化分成产品、订单、会员等子模块。

例如我们在设计数据库操作辅助类提供的方法有:

image

通过这样的方式,那么这个组件只负责数据库操作。这样带来的好处也是显而易见的。高内聚提供了更好的可维护性和可复用性。而低内聚的模块则表名模块直接的依赖程度高,那么一旦修改了该模块依赖的对象则无法使用该模块,必须也进行相应的修改才可以继续使用。

低内聚的模块设计的坏处有:首先模块的功能不单一,模块的职责不明确,比较松散,更有甚者是完成不相关的功能。这样的设计往往是不可取的。可以通过重构来完善。

下面我们来说下高内聚的简单解释:什么样的模块算是高内聚,并且能够在系统中很好的使用。

image

那么我们在设计的过程中如何去完成高内聚呢?

image

以上基本上讲述了高内聚的好处,并且阐述了如何实现高内聚的步骤和原则。下面我们来说说可能高内聚带来的坏处。

高内聚有时候也不是说所有的情况都采用这样的原则,当然高内聚还是要适度的,下面来举例说明:例如内聚性要求强的话就像Windows32中系统提供的API,里面的函数太多了,都放在一个Dll中,那么每个函数完成一个功能。这样强大的功能,会比较复杂,所以并不是完全的高内聚越高越好,还是要看实际的需要。

当然维护起来也不是特别的方便。

2、低耦合

首先我们来看看低耦合的定义:低耦合是用来度量模块与模块直接的依赖关系。耦合当然也可以这样简单的理解,我想懂电脑的应该都知道,CPU与主板之间的关系,CPU如果是特殊的CPU必须使用特殊的主板来支持,那么如果说这个CPU不唯一依赖唯一主板,那么就认为这个CPU与主板的关系是低耦合的关系。

下面我们来举例说明低耦合的设计与高耦合的设计:

  image

这是一个简单的低耦合的设计,电器与插座之间是低耦合的关系,就算我替换了不同的插座,电器依然可以正常的工作。因此简单的描述如下,就是A模块与B模块存在依赖关系,那么当B发生改变时,A模块仍然可以正常工作,那么就认为A与B是低耦合的。

image

1、笔记本接音响可以正常的使用。

2、笔记本接专配耳机正常的使用。

对应一般的音响来说,笔记本是通用的,音响和笔记本直接的关系是低耦合的,但是笔记本和耳机却是高耦合的,只有专配的耳机才能和笔记本互联使用,而不是通用的,所以说笔记本和专配耳机存在着较强的依赖关系。当然最简单的方式就是笔记本提供统一的耳机接口,可以满足一般性的需求。

下面我们将来分析如何构建低耦合的设计。

image

总结

  上面我们已经讲解了低耦合和高内聚的二个原则,通过这2个原则我们知道,满足这2个原则是衡量一个架构设计好坏的一个参考标准。下面我们将来讲解通过功能分离的方式来满足上面的2个原则。

五、如何满足设计要求

1、如何按功能进行模块化的分离。

我们在将一个系统进行功能划分时,我们一般如下来进行:

首先、我们先把功能职责划分成独立的单元。

例如现在有个B2C系统,那么我们按照B2C的需求,如下分析:

image

我们这里简单的分析下B2C应该具有的功能模块,当然这些模块的划分中,有些模块还可以继续的分离,当然我这里只是实例分析出来。

2、对分离出来的模块化进行抽象,例如我们以支付为例。

  image

这里通过支付接口向外提供服务。那么外界模块不关心支付系统模块的变化,只需要调用接口即可,如果具体的支付方式,比如支付宝的方式发生改变,在调用支付服务的模块中也不需要做任何的修改就可以正常的提供服务。显然这样的方式是不错的实现方式。

通常情况下我们在系统分离式只是以接口的方式提供服务,供其他的模块进行使用。在模块内部有大量的信息是不要向外部暴露的,所以模块在设计时访问域的定义就要划分好,防止因为访问域的定义而对模块的信息造成破坏。

下面我们来看下功能分离在不同的设计理念下都是什么样的表现:

image

上面只是实体性的分析了功能分离的好处及应用的广度,当然我们在后续会结合实例来讲解如何来实现这样的软件设计模式。当然这只是软件的架构设计,那么如果细化到具体的实现呢?我们如何去设计每个功能点呢?这就是下章我们要讲解的内容了,那么本文先列出二种常见的方式。

image

下篇我们将针对设计原则中的实现方式,进行详细的剖析与具体实现进行举例讲解,希望大家多提意见。

六、本章总结。

本章中主要简单的讲述了软件设计的二个基本的规范与原则:

1、高内聚:描述了模块内部的一系列功能的相关程度,对于功能之间相关度不高或者根本没有相关性的功能包含在模块中的做法是不可取的。

2、低耦合:描述了模块直接的依赖、感知程度,耦合的衡量标准是从低到高,一般来说耦合度越低越好。

当然有些特殊情况下,可能这二个原则也有矛盾的地方,当然我们还是要根据项目的实际需要及情况进行抉择,当然这二个原则是为了设计提供更好的扩展性、可读性、可维护性、极高的可复用性,所以要求我们在设计时尽量去满足这二个基本原则。而帮我去达到这二个准则的途径就是通过功能分离及细化来实现这样的准则。下一篇我们将深入的分析与举例来说明实现这二个原则的各种规范及准则。

系列之---系统设计规范与原则[下篇]

一、上章回顾

上章我们主要讲述了系统设计规范与原则中的具体原则与规范。如何实现满足规范的设计,我们也讲述了通过分离功能点的方式来实现,而在软件开发过程中的具体实现方式简单的分为面向过程与面向对象的开发方式,而目前更多的是面向对象的开发设计方式。具体的内容请看下图:

 

上图描述了软件设计的原则:低耦合,高内聚,并且简单说明了,如何实现这2个原则,通过分离关注点的方式。我们把功能称之为关注点。

二、摘要

本文将通过实例来讲解如何通过分离功能点,并且讲解分离关注点实现相应功能点时应该注意的问题。比如说一些相关的重要部分的内容。分离功能点是实现软件功能的一项重要基础,随着软件复杂度的不断提高,传统分离关注点的技术是只从一种方式去分离关注点,例如按照功能或者按照结构等等,使得越来越多的关注点得不到有效、充分的分离。因此有效、充分的分离关注点就是我们更好的实现软件功能的重要标准,那么我们如果想实现这个目的,就必须对软件同时从多种方式进行分解,因为分解的越详细,那么系统的设计就越清晰,那么就更容易满足设计原则需求。通过分离关注点能够使软件的复杂度降到最低,同时可理解性得到提高。

本文将会举例说明如何同时按照多种方式去分离关注点。因为本文中的内容都是本人对工作过程中的经验与总结,不足之处在所难免,还请大家多多提出自己的意见和建议,错误之处,在所难免,请大家批评指出。

三、本章大纲

1、上章回顾。

2、摘要。

3、本章大纲。

4、分离关注点的多种方式。

5、相关设计方法。

6、本章总结。

7、系列进度。

8、下篇预告。

四、分离关注点的多种方式

我的理解是分离关注点(功能点)的方式有以下几种及每种划分的原则,下面我们将会讲解如何按照不同的方式去划分一个系统,通过抽象功能点来降低软件系统的复杂度,并且提高系统的可理解度。

 

 

1、按模型来划分

这里的模型划分分为概念模型与物理模型:当然这里的概念模型就是抽象模型,例如我们平时说的功能的分离,我们以B2C的产品管理来说吧,产品管理里面至少拥有的功能是选择产品分类,选择产品单位,产品的扩展属性,产品的所属品牌等相关属性信息。那么我们闲来说说概念模型就是说是抽象模型,那么我们通过图形化的方式来描述

这是添加一个产品必须具备的四个相关功能点的分离。

那么我们在物理模型上如何去实现产品管理的物理模型呢?下面我们来看看。

  简单的解释就是具体的每个功能点的具体业务设计模型,物理模式是概念模型的实现。

2、按层次来划分

层次可以简单的分为分层分离的方式与横切分离的方式,那么来举例说明,我们都知道横切和纵切,就是说看待的问题的角度,下面来举例说明如何以这2种方式来分离功能点。

当然我们这里仍然以B2C系统为例来说明这样的情况。我们这里先来看分层分离的方式来处理。

 

我们的B2C可以简单按照如下方式进行分层,业务逻辑层与界面通过服务层来调用,这样可以避免UI层与业务层之间的耦合,而业务逻辑层通过数据访问层与数据库进行交互。当然可能我这里的部分设计还存在不合理之处,还请大家多多提出宝贵意见,好让这个设计更加完善。

那么我们下面来看下横切分离方式的情况,我们知道,我们系统中可能会对管理员或者任何操作人员的操作的详细信息进行详细的记录,那么我们就会通过日志的方式来处理,横切的方式就是系统从头到尾的任何一个功能处都会用到,这是一个横向分离关注点的过程。那么我们在设计系统操作日志时就会记录相应的操作日志或者系统的错误日志等等相关信息。

操作日志与错误日志贯穿每个分层结构、分离关注点横向分离的方法实现就是AOP(面向方面编程)。当然我们后面会介绍AOP的具体实现方式细节。

五、相关设计方法

本节将会详细的阐述分层与横切分离关注点的二种编程方式的实现,通过编程方法实现关注的不同切面来分析设计方法的实现。这里介绍的二种编程方法是面向对象的编程方法实现分层方式的分离关注点与面向切面的编程方法实现横切分离关注点的方式。

1、面向对象设计

首先、面向对象作为一种编程思想,我想在园子里面的大多数同仁都比较熟悉,我这里也不详细谈面向对象的设计,这里我们只是谈谈面向对象设计中的几个原则和需要注意的方面。

我们知道面向对象的编程思想是把世界万物都看作是对象,而复杂的功能可以看作对象与对象之间的关系组成。那么我们在分离关注点后,那么每个关注点可以进一步细化为多个对象及对象之间的关系。

那么我们来看看面向对象设计中的几个基本的原则,并且分别的举例说明:

a、首先必须先从分离关注点中分析出对象及对象之间的关系。例如我们以B2C系统中的店铺管理来说。

图中简单描述了店铺管理中应有的对象。

图中简单的描述了对象之间的关系,店铺信息依赖店铺等级与店铺类型信息,店铺认证信息依赖店铺信息。

b、对象分离出来之后,那么我们先来看看对象对应的类的几个相关原则:

(1)、(SRP)单一职责原则,简单来说就是一个类只提供一种功能和仅有一个引起它变化的因素。如果我们发现一个类具有多个引起它变化的因素时就必须想办法拆分成单独的类。下面来举例说明。我们这里以ORM中的实体接口层来说。

01 public interface IEntity 
02
03 /// <summary> 
04 /// 保存 
05 /// </summary> 
06 /// <returns>返回影响的行数</returns> 
07 int Save(); 
08 /// <summary> 
09 /// 删除 
10 /// </summary> 
11 /// <returns>返回影响的行数</returns> 
12 int Delete();
13 /// <summary> 
14 /// 写入日志信息 
15 /// </summary> 
16 /// <param name="message">写入信息</param> 
17 /// <returns>返回影响的行数</returns> 
18 int WriteLog(string message); 
19 }

很明显这里的写入日志与前面的对实体的持久化的操作明显不搭边,这样的可能会造成2中引起类发生改变的因素时就必须分离,那么就必须把它抽出来,单独定义一个接口。修改后结果如下:

01 public interface IEntity 
02
03 /// <summary> 
04 /// 保存 
05 /// </summary> 
06 /// <returns>返回影响的行数</returns> 
07 int Save(); 
08 /// <summary> 
09 /// 删除 
10 /// </summary> 
11 /// <returns>返回影响的行数</returns> 
12 int Delete(); 
13 }
14 public interface Logger 
15
16 /// <summary> 
17 /// 写入日志信息 
18 /// </summary> 
19 /// <param name="message">写入信息</param> 
20 /// <returns>返回影响的行数</returns> 
21 int WriteLog(string message); 
22 }

(2)、(OCP)开发封闭原则:简单来说就是不能修改现有的类,而需要在这个类的功能之上扩展新的功能,这时通过开放封闭原则来实现这样的要求。该原则使我们不但能够拥抱变化,同时又不会修改现有的代码。而这个原则的实现可以简单来说就是我们将一系列发生变化的类的行为抽象为接口,然后让这些类去实现我们定义的接口,调用者通过接口进行操作。例如我们以MP3播放器来说。

01 public interface IMP3 
02
03 /// <summary> 
04 /// 播放 
05 /// </summary> 
06 /// <returns>返回操作是否成功</returns> 
07 bool Play(); 
08 /// <summary> 
09 /// 停止 
10 /// </summary> 
11 /// <returns>返回操作是否成功</returns> 
12 bool Stop(); 
13 }

定义2个不同的实现

01 /// <summary> 
02 ///  台电播放器 
03 /// </summary> 
04 public class TD : IMP3 
05 {
06 #region IMP3 成员
07 public bool Play() 
08
09 return true
10 }
11 public bool Stop() 
12
13 return true
14 }
15 #endregion 
16 }
17 /// <summary> 
18 ///  惠普播放器 
19 /// </summary> 
20 public class HP : IMP3 
21 {
22 #region IMP3 成员
23 public bool Play() 
24
25 return true
26 }
27 public bool Stop() 
28
29 return true
30 }
31 #endregion 
32 }

通过一个测试类来模拟接口调用,通过依赖注入的方式实现。

01 public class Test 
02
03 IMP3 mp3 = null;
04 public Test(IMP3 mp) 
05
06 mp3 = mp; 
07 }
08 public bool Play() 
09
10 return mp3.Play(); 
11 }
12 public bool Stop() 
13
14 return mp3.Stop(); 
15
16 }

具体的测试代码我就不书写了,我想大家都知道了。

(3)、(LSP)替换原则:简单的来说就是基类出现的地方,扩展类都能够进行替换,那么前提就是我们不能修改基类的行为。也就是说基类与扩展类可以互相相容。在面向对象中可能会认为很容易实现,不过我们要注意有时候我们从父类中继承的行为有可能因为子类的重写而发生变化,那么此时可能就不满足前面说的不改变基类本身的行为。我们最熟悉的多态其实这样的情况就不满足这个原则。需要注意的时,对调用者来说基类与派生类并不相同,我们简单来说明。

01 public class Test 
02
03 private int tempValue=0; 
04 public void TestA() 
05
06 tempValue = 2; 
07 }
08 public virtual void TestB() 
09
10 tempValue = 9; 
11
12 }
13 public class Test1 : Test 
14
15 public override void TestB() 
16
17 //如果调用该方法,那么tempValue的值可以和基类中的得到的值是相同的,如果不显示的调用几类方法,那么这个值将丢失 
18 //则不满足替换原则。 
19 base.TestB(); 
20
21 }

通过上面的简单代码可知,里氏替换原则中需要注意的地方:当对具有virtual关键字和saled关键字的类或者方法需要特别注意,因为这些关键字会对继承类的行为造成一定的影响,当然上面的例子中只是说了重写的情况,还有new的情况,就是把父类中的方法隐藏,同样都是不满足里氏替换原则的。本例中我们的tempValue是私有类型的变量,那么在基类中可以访问到,派生类中却无法访问,所以我们要注意,在处基类替换时需要注意继承的成员函数的访问域,建议的方式是虚方法访问的类的成员变量尽量使用保护类型,这样可以防止丢失的情况。当然基类中有虚方法访问了基类中定义的私有变量,那么如果在继承类中如果想不丢失该基类中该虚方法对其内部的私有变量的访问,那么可以在继承类中通过“base.(函数名)”的形式来显示调用基类方法,可以保持基类的行为。

  (4)、(DIP)依赖倒置原则:简单来说就是依赖于抽象而不应该依赖于实现,这样的目的就是降低耦合性。简单的来说就是让二个类之间的依赖关系通过接口来解耦,让一个类依赖一个接口,然后让另外一个类实现这个接口,通过构造注入或者属性注入的方式来实现依赖。简单来说就是抽象不依赖于细节,细节依赖于抽象。下面我们来举例说明:

01 /// <summary> 
02 /// 汽车制动系统 
03 /// </summary> 
04 public interface IControl 
05
06 int UpSpeed(); 
07 bool Brake(); 
08 }
09 /// <summary> 
10 /// 其他服务 
11 /// </summary> 
12 public interface IServer 
13
14 bool Radio(); 
15 bool GPS(); 
16 }

具体的使用

01 /// <summary> 
02 /// 汽车 
03 /// </summary> 
04 public class Car 
05
06 private IControl control = null
07 private IServer server = null;
08 public Car(IControl con, IServer ser) 
09
10 control = con; 
11 server = ser; 
12 }
13 public void Start() 
14
15 control.UpSpeed(); 
16 }
17 public void Play() 
18
19 server.Radio(); 
20 }
21 public void Map() 
22
23 server.GPS(); 
24
25 }

上面简单的举例说明,并没有给出具体的实现,只要实现上面的2个接口即可,这里就不详细说明了,希望大家能够明白。错误之处还请大家指出。

(5)、(ISP)接口隔离原则:简单的来说就是客户不关心细节的东西,他就只关心自己能够得到的服务,而面向对象的原则我想大家都知道,通过接口的方式来提供服务。因此我们提供给客户的是一系列的接口,那么这样就具有很好的低耦合性。我们来简单的举例说明:以ORM中的简单的持久化操作来说

01 public interface IORM 
02
03 /// <summary> 
04 /// 保存 
05 /// </summary> 
06 /// <returns>返回影响的行数</returns> 
07 int Save(); 
08 /// <summary> 
09 /// 删除 
10 /// </summary> 
11 /// <returns>返回影响的行数</returns> 
12 int Delet(); 
13 /// <summary> 
14 /// 获取所有列表 
15 /// </summary> 
16 /// <returns>返回一个DataTable</returns> 
17 DataTable GetList(); 
18 }

这里我们让一个实体来继承实现。

01 public class ORM : IORM 
02 {
03 #region IORM 成员
04 public int Save() 
05
06 return 1; 
07 }
08 public int Delet() 
09
10 return 1; 
11 }
12 public System.Data.DataTable GetList() 
13
14 return new System.Data.DataTable(); 
15 }
16 #endregion 
17 }

业务层的实现

01 public class Entity 
02
03 private IORM orm = null;
04 public Entity(IORM orm1) 
05
06 orm = orm1; 
07
08 public int Save() 
09
10 return orm.Save(); 
11 }
12 public int Delete() 
13
14 return orm.Delet(); 
15
16 }

这里我就不贴出来测试代码的实现了,后面我会把代码贴出来下载,如果有兴趣的同仁可以下载看看。当然我这里只是抛砖引玉,不足之处,还请大家多多指出。面向对象设计中还有很多的原则,这里也不会一一复述。这里只是冰山一角,还希望大家多多提出宝贵意见。

2、面向切面编程

面向切面编程,其实就是面向方面编程,其实这个概念很早之前就提出了,但是并没有广泛的流行,这个是比较让人不解的地方,我平时其实使用的也是比较少的。不过我们在系统架构中却是非常有用,特别是在关注点的分离的过程中起到很大的作用。AOP的主要目的呢,是将横切的关注点与核心的分层形式的或者说是功能组件的分离。下面我们来看看AOP中的如何实现方面编程。

面向方面编程中的方面并不是直接有编译器来实现,而是通过某种特殊的方式将方面合并到常规的源代码中,然后通过编译器编译的方式。我们知道一个方面就是一个横切关注点,在实现方面的过程中,我们通常需要定义连接点与基于这个连接点上的通知来完成。下面我们来看看AOP的处理源代码的模型:

AOP一般都是有框架提供注入的功能,而这里的代码注入功能与我们在面向对象的依赖注入不同。这里的注入是将方面的代码植入到常规代码片段中。

下面我们先来介绍AOP中的连接点与通知。

连接点:用来标识某个类型中的植入某个方面的位置,而连接点可以是某个方法的调用,属性访问器,方法主体或者是其他。连接点一般用来标识注入某个方面的类型中的代码位置。

通知:用来标识注入到类型中植入方面的具体的代码。简单来说就是要注入的方面代码。

目前在.NET中已提供AOP的植入基础功能。PIAB就是AOP在.NET下的一种实现方式。下面我们来简单的说说,当然园子里面不少的大牛也讨论过这个IAB的相关介绍及用法。大家可以参考这些作者的文章。

一般我们在.NET平台下有2种注入方面代码的方式,下面以图例来说明:

可能具体的实现方案这里枚举的并不全面。但是一般采取植入的方式就这2类了,运行期实现的方案较多,编译期实现则需要有第三方提供方面植入工具,完成编译前的代码植入,并且必须保证植入的代码是可以编译通过的。

如果想详细了解PIAB请参考 :大牛 Artech的PIAB系列 《EnterLib PIAB深入剖析》系列博文汇总

六、本章总结

  本章详细的阐述了软件设计的规范与原则的实现方式,通过面向对象与面向方面编程来分离实现关注点,并且在实现过程中遵循的原则等。并且分析了分离关注点中的分离方法与角度,通过多种方式及多角度的分离关注点,当然本文只是抛砖引玉,不足之处,还请大家多多提出宝贵意见。鄙人将在后续文章中持续改进,谢谢!



专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...