设计模式之美(0):模式运用的原则

    近几年来,一股狂热的模式潮、架构潮以不可阻挡之势倾刻席卷开发行业。多少人因此而练就一身深不可测的内功,收发自如。信意发功,即可信手拈花,飞沙走石,一片花瓣亦可断技穿墙。倘若浑身解数悉其毕生功力,自是地动山摇,风云色变,足以引导业界的标准,如此尽画江山,快意人生。然而,数尽天下豪杰,也非四海皆英雄。多少人在模式与架构面前迷失自己,走火入魔,甚至血脉倒流而亡。为模式而模式,为架构而架构,过度设计,本末倒置,导致系统过度复杂,庞大,性能迟缓,无法维护,加速了死亡,魂断模式。
    利用面向对象语言开发应用,如果想运用好设计模式,就需要坚持面向对象的几点原则。它们之间有个共同的目的,便是为了“可复用、易维护、低耦合、高内聚”。坚持面向对象的原则,会指导着开发者不断的修改设计,不断的判断选择合适的设计模式。不多说废话,下面让我们一起理解面向对象编程的几个原则,这里会以比较浅白的文字来表述,以求更多读者易于接受。

1、单一职责原则(Single Responsiblity Principle,简称SRP)

这一点很好理解,单一职责就是只有一个责任的意思。一个类只能承担一个职责,就一个类而言,应该仅有一个引起它变化的原因。不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。测试驱动的开发实践常常会在设计出现臭味之前就迫使我们分离职责。
    用浅显点的道理来说,开发者好比是管理者,而类就是其管理下的员工们。大家都应该知道,优秀的公司在岗位分配上总是很清晰,一个人分配一个岗位,根据员工的强项,给其分配相应的岗位工作。不会说让一个员工既做开发,又让他处理财务,那他不仅会累死,而且可能两个工作都不好。资源的最优分配是,这个人擅长于做什么,就让他只做这件事。当然这个比如可能不是很好。作为开发者,就要管理好你的代码,把自己看作是一个管理者,管理好你写出来的N个类。类,如果最简单,最单纯,我们很容易控制。如果不断给它分配职责,就会越来越复杂,以后对类的控制那种情景是相当恐怖的。
    举个例子说明一下:假设开发一个图书销售应用,需要往数据库里记录图书信息、员工信息和销售单信息。很自然的,在操作数据库这一层上(持久层),会有图书dao类(BookDao)、员工dao类(EmployeeDao)和销售单dao类(SalesDao)。这里我们假设有这么一个接口:

public interface IDao
{
    public void conn();  //连接数据库
    public void close();  //释放连接
    public void loadObject();  //加载对象信息
    public void deleteObject();  //删除对象信息
    public void saveOrUpdateObject();  //存储对象信息
}

    很明显的,我们看到IDao接口有两个职责,一个是负责与数据库创建连接和释放,另一个是负责对象的增删改查。而BookDao、EmployeeDao、SalesDao都实现了IDao接口。想像一下,如果某一天,conn()方法需要发生改变,比如业务量太大导致Access数据库无法满足了,升级为Sqlserver了,那么就要去同时改动BookDao、EmployeeDao、SalesDao三个类的conn方法。如果是在大型应用里面,几十个甚至上百个这样的dao类都是很有可能的。这是多么恐怖的恶梦。因此,职责一定要分离,单一职责这个原则是不能让步的。

2、开闭原则(Open-Close Principle,简称OCP)

开闭原则是面向对象可复用的基石,它主要是指:一个实体应该对扩展开放,对修改关闭。在设计一个模块的时候,应当是这个模块在不被修改的前提下被扩展。满足这个原则的系统在一个较高层次上实现了复用,也是易于维护的。试想如果因为要扩展一个模块的功能而对这个模块进行不断的修改,这种维护是多么的悲哀。扩展模块功能,我们应该只是将扩展实现代码增加进去或者修改配置代码,而不应该因为扩展而修改以前的代码。
    那么该怎么样做才能满足开闭原则呢?关键是抽象!因此在进行面向对象设计时要尽量考虑接口封装机制、抽象机制和多态技术。我们需要尽可能的预见了所有的可能扩展,然后把这部分封装在抽象类里(或者接口),并将其作为系统的抽象层。那么在任何扩展的情况下,我们都可以在不修改代码的情况下满足需求。在需要的情况下,我们还可以从继承这个抽象层而得到一个某种特定需求的实现,是多么方便的事情。当然,在进行封装的时候,我们需要找到一个可变性的封装,这样的封装才是有意义的。
    比如上面IDao中的conn()方法,假设某种变态的业务需要动用分布式数据库。那么可能存在sqlserver1和sqlserver2。因此conn()会根据需要连接到不同数据库。而conn()同时又是所有dao子类公用的一个方法。因此我们将其封装在BaseDao,并为conn提供参数,让其方便的在sqlserver1和sqlserver2之间切换。注意,实际开发的并不能这样设计,还需要更为抽象更为可复用的设计,这里仅以此例说明开闭原则的好处。封装后的代码如下:

public class BaseDao
{
    public void conn(String databaseString)
    {
        ......//这里以databseString进行数据库连接
    }
}

    以后对不同数据库的连接,仅需传送不同的databaseString便可实现。在实际开发中,我们还需考虑到跨数据库平台,且将数据库连接功能再度抽象,以配置注入的方式完成。

3、里氏替换原则(Liskov Substitution Principle,简称LSP)

这个原则是指:凡是使用基类型的地方,子类型一定适用,即子类可以替换基类。反之不成立。
    里氏替换原则是继承复用的基石,只有子类可以替换掉基类,软件单位的功能不受影响时,基类才能真正的被复用。而子类才能在基类的基础上增加新的功能。比如Base是基类,Sub是Base的子类,那么如果有方法method(Base b),则method(s)一定成立,其中s是Sub的对象。这样一来,基类Base所具有的功能,对于所有的子类Sub来说,都能在method()里自由调用。反之,如果是设计为method(Sub1 s1),而Sub1和Sub2均为Base的子类。结果就是这个method只能传Sub1的实例进去,而Sub2的任何实例都无法传进method运用了。

4、依赖倒置原则(Dependency Inversion Pricinple,简称DIP)

依赖倒转原则是:针对接口编程,要依赖于抽象,不要依赖于具体的实现。
    为什么叫依赖倒转原则呢? 在传统的过程性系统中,高层的模块依赖于低层次的模块,抽象层次依赖于具体层次。这样导致了底层的任何改变都会影响到上层。这样的软件系统没有可维护性而言。抽象层次应该不依赖于具体的实现细节,这样才能保证系统的可复用性和可维护性,这也就是所谓的倒转。在实际中如何应用这一原则呢?要针对借口编程,而不针对实现编程。那么当实现变化时,不会影响到其他的地方。在java中应当使用接口和抽象类来进行变量的类型声明,参数的类型声明,方法的返回值类型等。比如不应该这样声明:
Vector employee = new Vector();
而应该这样声明employee:
List employee = new Vector();
这样的好处是决定将Vector改成ArrayList时,代码的改动比较少。
    依赖倒置原则前提是假定所有的具体类都是变化的,有一些具体类可能是相当稳定,不会发生变化的。消费这个具体类的客户端就完全可以使用这个具体类,没必要为此发明一个抽象类型。以抽象的方式进行耦合是依赖倒转原则的关键。

5、接口隔离原则(Interface Segregation Principle,简称ISP)

采用多个与特定客户类有关的接口比采用一个通用的涵盖多个业务方法的接口要好。ISP原则是另外一个支持诸如COM等组件化的使能技术。缺少ISP,组件、类的可用性和移植性将大打折扣。这个原则的本质相当简单。如果你拥有一个针对多个客户的类,为每一个客户创建特定业务接口,然后使该客户类继承多个特定业务接口将比直接为客户加载所有方法有效。
     浅显点来说,使用多个专门的接口比使用统一的总接口要好。从客户的角度来说:一个类对另外一个类的依赖性应当是建立在最小的接口上的。如果客户端只需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。向客户端提供public方法意味着向客户端作出承诺,过多的承诺会给系统的维护造成不必要的负担。我们举个例子来说明一下,假设有一个媒体流处理器,它既能处理视频流,又能处理音频流。因此在接口设计上,我们有两种方案,一种是定义总接口处理两种媒体流,一种是将两种媒体流处理分别定义接口。代码如下:

//方案一
public interface IMedia
{
    public void dealVedio();  //处理视频流
    public void dealRadio();  //处理音频流
}

//方案二
public interface IVedio
{
    public void dealVedio();  //处理视频流
}
public interface IRadio
{
    public void dealRadio();  //处理音频流
}

     我们先采用第一种方案。假设我要开发个MP3播放器,而我知道你的媒体流处理技术很棒,因此我想引用你的技术。而你也很乐意地为我提供了接口IMedia,结果呢,我要实现这个接口,可是我只需要dealRadio(),我并不需要dealVedio(),这个时候视频处理就变成了多余的方法。接着呢,我又想搞闭路,我再引进你的视频处理技术。而你还是给我提供了IMedia接口。这时候,我就要翻桌子了,因此你又搞了个多余的dealRadio(),我还得去处理。可是,如果换为第二种方案的话,那就不一样了。我搞MP3,你就提供IRadio接口。我又搞闭路,你就提供IVedio接口。如果我冲动得想搞家庭影院,你把两个接口都提供给我就完事了,很灵活,也很适当。每次我需要时,你总能恰到好处为我提供所需的东西。

6、合成/聚合复用原则(Composition and Aggregation Reuse Principle,简称CARP)

合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部份,新的对象通过向这些对象的委派达到复用已有功能的目的。即尽量使用合成/聚集复用而不是继承复用。两个对象之间的关系,如Has和Is两种,Has-A代表一个类是另外一个类的一个组成部分或角色,而Is-A代表一个类是另外一个类的一种。如果两个类是“Has-A”关系那么应使用合成/聚集,如果是“Is-A”关系那么可使用继承。我们应尽量使用和成/聚合而不是继承来实现复用。我们还是举个简单例子来说明一下问题,以一个具有变态需求的DAO作例子。在某一项目中,需要既可直接使用JDBC的API来做数据库操作,又可利用Hibernate来实现对象的持久化操作。通常我们只采用其中一种时,都会有个BaseDao类,封装了一些通用的操作方法。如下:

public class BaseDao
{
    public void open();  //打开连接
    public void close();  //释放连接
    public void loadObject();  //加载对象信息
    public void deleteObject();  //删除对象信息
    public void saveOrUpdateObject();  //存储对象信息
}

    但此时,我们既需要JDBC,又需要Hibernate,该怎么办。如果使用继承,那么可能是这种情况,如下:

public class BaseDao
{
    public void open();  //打开连接
    public void close();  //释放连接
}
public class BaseJdbcDao extends BaseDao
{
    public void loadJdbcObject();  //加载对象信息
    public void deleteJdbcObject();  //删除对象信息
    public void saveOrUpdateJdbcObject();  //存储对象信息
}
public class BaseHibernateDao extends BaseJdbcDao
{
    public void loadHibernateObject();  //加载对象信息
    public void deleteHibernateObject();  //删除对象信息
    public void saveOrUpdateHibernateObject();  //存储对象信息
}

    终于问题解决了。可是前面说了,这是个变态需求,有一天,发现EJB3的持久化方案其实不错,并且在审定之后,认为可以将持久化技术升级到EJB3,但又想保持以往的jdbc和hibernate支持。难道这个时候还想再用继承去实现吗?如果用继承,又该往那个类插入新的子类实现呢,系统会变得相当复杂,相当难以理解且难以维护。可是,如果使用聚合来实现的话,如下代码:

public interface IJdbcDao
{
    public void loadObject();  //加载对象信息
    public void deleteObject();  //删除对象信息
    public void saveOrUpdateObject();  //存储对象信息
}
public interface IHibernateDao
{
    public void loadObject();  //加载对象信息
    public void deleteObject();  //删除对象信息
    public void saveOrUpdateObject();  //存储对象信息
}
public class BaseDao
{
    private IJdbcDao jdbcDao;
    private IHibernate hbnDao;
}

//新需求,需要EJB3持久化支持,因此仅需封装EJB3操作,再在BaseDao中对其引用
public interface IEJBDao
{
    public void loadObject();  //加载对象信息
    public void deleteObject();  //删除对象信息
    public void saveOrUpdateObject();  //存储对象信息
}
//改后的BaseDao,马上就拥有EJB实现
public class BaseDao
{
    private IJdbcDao jdbcDao;
    private IHibernate hbnDao;
    private IEJBDao ejbDao;
}

    我们会发现,使用聚合方式来实现,问题变得可解决了,简单多了。而且很容易适应需求的变化,尽管这个需求不怎么符合现实,很变态。我们的设计还是能够轻松的应对。对于以后的维护,也会显得得心应手。

7、迪米特法则(Law of Demeter,简称LoD)

迪米特法则又叫最少知识原则、不要和“陌生人”讲话原则,要求一个类(软件实体)应该尽可能少的和其它类(软件实体)发生作用。不要向间接对象(陌生人)发送消息(讲话)。只应该向以下直接对象(朋友)发送消息:this对象(自身)、方法的参数对象、this的属性直接引用的对象、作为this属性的集合中的元素对象、在方法中创建的对象。这样做的目的是为了避免客户与间接对象和对象之间的对象连接产生耦合。利用一个“中间人”是“迪米特法则”解决问题的办法。我们同样举个例子来理解一下迪米特法则,假设我们(Customer)要买今天的伙食,肉(meat)、菜(vegetable)、面(flour),同时有面商(FlourSaler)、肉商(MeatSaler)、菜商(VegetableSaler)。如果我们直接去和这三个江湖老商买东西,那就要跑来跑去的了,买了面,再跑去买肉和菜,那会累死人。这时候我们多么期待能有个小超市(SuperMarket),它已经收集了这几类的商品,而我们只需要直接向这个小超市购买就可以买到我们所需要的,那岂不是轻松多了。请看下面代码比较:

public interface ISaler
{
    public void sale();
}
public class MeatSaler implements ISaler
{
    public void sale(){ ... }  //售肉
}
public class VegetableSaler implements ISaler
{
    public void sale(){ ... }  //售菜
}
public class FlourSaler implements ISaler
{
    public void sale(){ ... }  //售面粉
}
public class SaleAction
{
    ISaler saler;
    //每买样东西,就要跟一个ISaler的实现实例打交道,累不累啊
    //买肉
    saler = new MeatSaler();
    saler.sale();
    //买菜
    saler = new VegetableSaler();
    saler.sale();
    //买面粉
    saler = new FlourSaler();
    saler.sale(); 
}


//以下是修改后的代码
public class SuperMarket
{
    public void sale(String[] args)
    {
        for(String arg : agrgs)
            this.loadSaler(arg).sale();
    }
    private ISaler loadSaler(String arg)
    {
        ISaler saler;
        //假设这里已经实现方法loadSaler(arg),方法的功能为根据arg自动找到ISaler实现类,并返回一个实例
        // meat      : MeatSaler
        // vegetable : VegetableSaler
        // flour     : FlourSaler
        saler = ......
        return saler;
    }
}
public class SaleAction
{
    SuperMarket sm = new SuperMarket();
    sm.sale( {"meat","vegetable","flour"} );
}

在这里,这个小超市就是所谓的“中间人”。同理,在进行设计的时候,我们要避免一个对象直接与多个对象进行交流(通信),否则会在往后造成很大的麻烦,维护的困难。当然,必须正视一点,这个“中间人”尽管给我们带来了方便,但由于它持有对许多陌生人(对象)的引用,因此它必然带来性能上的问题。如果运用不当,给这个“中间人”挤进一两百个陌生人(对象),那同样是很恐怖的事情。

 

    最后,要指出一点很重要,切莫把面向对象原则和设计模式奉为神旨般看待,过分沉迷而不可自拔。我们必须承认一点,坚持以上原则,运用设计模式,必然会导致系统复杂性的增加,有时会伴随着部分性能的损耗,但换来的将是易维护可复用的系统应用。良好的设计,通常都会将底层基础应用封装得很好,尽管系统结构复杂度增加了,但对于开发者而言,这些底层基础被封装后就可以不必去关心了,因此复杂度的增加对于开发者来说应是透明的,对于架构维护者才需要是清晰的。那么开发者应该感觉到的是,应用的开发过程或维护过程变轻松了。达到这样效果的架构设计,便是优秀的设计了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值