精华内容
下载资源
问答
  • 设计模式的六大原则

    万次阅读 多人点赞 2019-05-16 17:50:03
    一、单一职责原则(Single Responsibility Principle) 二.开闭原则(Open-Closed Principle, OCP) 三、里氏代换原则(Liskov Substitution Principle, LSP) 四、依赖倒置原则(Dependence Inversion Principle,...

    目录

     

    一、单一职责原则(Single Responsibility Principle)

    二.开闭原则(Open-Closed Principle, OCP)

    三、里氏代换原则(Liskov Substitution Principle, LSP)

    四、依赖倒置原则(Dependence Inversion Principle,DIP)

    五、接口隔离原则(Interface  Segregation Principle, ISP)

    六、迪米特法则(Law of  Demeter, LoD)

    总结


    一、单一职责原则(Single Responsibility Principle)

    定义:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

    问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有 
    可能会导致原本运行正常的职责P2功能发生故障。

    单一职责原则告诉我们:一个类不能太“累”!在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。

    单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。

    SRP在类或接口中的使用:

    /**
    上面的的类图对应的接口入下
    */
    public interface IPhone{
      //拨通电话
      public void dial(String phoneNumber);
    
      //通话
      public void chat(Object o);
    
      //挂断电话
      public void hangup();
    }
    

    在看到这个接口的时候,我们都会认为这样的设计是没有问题的,拨通电话,通话,挂断电话写在同一个接口里面并没有什么错。但是,我们仔细分析,这个接口真的没有问题吗?单一职责原则要求一个接口或类只有一个原因引起变化,也就是说一个接口或一个类只有一个原则,它就只负责一件事。 但我们分析上面这个接口,却发现它包含了两个职责:一个时协议管理,一个是数据传送。dial()和hangup()两个方法实现的是协议管理,分别是拨通电话和挂机。chat()实现的是数据传送,把我们说的话转换成模拟信号或数字信号传递给对方,然后再把对方传递过来的信号还原成我们听得懂的语言。这里的协议接通和数据传送的变化都会引起该接口或实现类的变化。我们想一想,这两个职责会相互影响吗?不管是什么协议,协议接通只负责将电话接通就行,而数据传输只需要传输数据,不必要去管协议是如何接通的。所以通过分析,IPhone接口包含了两个职责,而且这两个职责的变化不互相影响,这就可以考虑分成两个接口。

    笼统地讲:是否需要拆分取决于变化:

    当变化发生,只影响其中一个职责,那就需要拆分

    如果变化都影响到这两个职责,那就不需要拆分。

    SRP也适用于方法:

    其实,单一职责原则不仅适用于类,接口,同样适用于方法中。这要举一个例子了,比如我们做项目的时候会遇到修改用户信息这样的功能模块,我们一般的想法是将用户的所有数据都接收过来,比如用户名,信息,密码,家庭地址等等,然后统一封装到一个User对象中提交到数据库,我们一般都是这么干的,就如下面这样:

    其实这样的方法是不可取的,因为职责不明确,方法不明确,你到底是要修改密码,还是修改用户名,还是修改地址,还是都要修改?这样职责不明确的话在与其他项目成员沟通的时候会产生很多麻烦,正确的设计如下:

    循单一职责原的优点有:

     1.可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;

     2. 提高类的可读性,提高系统的可维护性;

     3.变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

    二.开闭原则(Open-Closed Principle, OCP)

    定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展

    问题由来:任何软件都需要面临一个很重要的问题,即它们的需求会随时间的推移而发生变化。因为变化,升级和维护等原因,如果需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试,所以当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现使我们需要的。

    为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

    举例:

     实现画图表的功能,如饼状图和柱状图等,为了支持多种图表显示方式,原始设计方案如图下图所示:

    在ChartDisplay类的display()方法中存在如下代码片段:

    ......
    if (type.equals("pie")) {
    PieChart chart = new PieChart();
    chart.display();
    }
    else if (type.equals("bar")) {
    BarChart chart = new BarChart();
    chart.display();
    }
    ......

    在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。

          现对该系统进行重构,使之符合开闭原则。

          (1) 增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;

          (2)  ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。

          重构后结构如图2所示:

    我们引入了抽象图表类AbstractChart,且ChartDisplay针对抽象图表类进行编程,并通过setChart()方法由客户端来设置实例化的具体图表对象,在ChartDisplay的display()方法中调用chart对象的display()方法显示图表。如果需要增加一种新的图表,如折线图LineChart,只需要将LineChart也作为AbstractChart的子类,在客户端向ChartDisplay中注入一个LineChart对象即可,无须修改现有类库的源代码。 

    为什么使用开闭原则:

    第一:开闭原则非常有名,只要是面向对象编程,在开发时都会强调开闭原则

    第二:开闭原则是最基础的设计原则,其它的五个设计原则都是开闭原则的具体形态,也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。依照Java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。

    第三:开闭原则可以提高复用性

    在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来,而不是在一个类中独立实现一套业务逻辑。只有这样的代码才可以复用,逻辑粒度越小,被复用的可能性越大。为什么要复用呢?复用可以减少代码的重复,避免相同的逻辑分散在多个角落,减少维护人员的工作量以及系统变化时产生bug的机会。怎么才能提高复用率呢?设计者需要缩小逻辑粒度,直到一个逻辑不可以分为止。

    第四:开闭原则可以提高维护性 

    一款软件量产后,维护人员的工作不仅仅对数据进行维护,还可能要对程序进行扩展,维护人员最乐意的事是扩展一个类,而不是修改一个类。让维护人员读懂原有代码,再进行修改,是一件非常痛苦的事情,不要让他在原有的代码海洋中游荡后再修改,那是对维护人员的折磨和摧残。

    第五:面向对象开发的要求 

    万物皆对象,我们要把所有的事物抽象成对象,然后针对对象进行操作,但是万物皆发展变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到尽可能多变化的因素,然后留下接口,等待“可能”转变为“现实”。

    如何使用开闭原则

    第一:抽象约束 
        抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:

    1.通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。

    2.参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求

    3.抽象层尽量保持稳定,一旦确定就不要修改

    第二:元数据(metadata)控件模块行为 

    编程是一个很苦很累的活,那怎么才能减轻压力呢?答案是尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗的说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

    第三:制定项目章程 
        在一个团队中,建立项目章程是非常重要的,因为章程是所有开发人员都必须遵守的约定,对项目来说,约定优于配置。这比通过接口或抽象类进行约束效率更高,而扩展性一点也没有减少

    第四:封装变化 

     对变化封装包含两层含义:

    (1)将相同的变化封装到一个接口或抽象类中

    (2)将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。 封装变化,也就是受保护的变化,找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口。

    三、里氏代换原则(Liskov Substitution Principle, LSP)

    定义:里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。

    继承优点

    代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;提高代码的重用性;子类可以形似父类,但又异于父类;提高代码的可扩展性,实现父类的方法就可以“为所欲为”了;提高产品或项目的开放性。

    继承缺点

    继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;降低代码的灵活性。子类必须拥有父类的属性和方法;

    增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果;大片的代码需要重构。

    克服继承的缺点——里氏替换原则

    从整体上来看,利大于弊。

    里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。

    里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

    在使用里氏代换原则时需要注意如下几个问题:

     (1)子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。

    (2)  我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。

    (3) Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的

    系统需要提供一个发送Email的功能,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,原始设计方案如图所示:

          在对系统进行进一步分析后发现,无论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说两个send()方法中的代码重复,而且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构。

          在本实例中,可以考虑增加一个新的抽象客户类Customer,而将CommonCustomer和VIPCustomer类作为其子类,邮件发送类EmailSender类针对抽象客户类Customer编程,根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,因此将EmailSender中的send()方法的参数类型改为Customer,如果需要增加新类型的客户,只需将其作为Customer类的子类即可。重构后的结构如图所示:

            里氏代换原则是实现开闭原则的重要方式之一。在本实例中,在传递参数时使用基类对象,除此以外,在定义成员变量、定义局部变量、确定方法返回类型时都可使用里氏代换原则。针对基类编程,在程序运行时再确定具体子类。
     

    四、依赖倒置原则(Dependence Inversion Principle,DIP)

    定义:

    高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,其核心思想是:要面向接口编程,不要面向实现编程。

    依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。

    在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。

    在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。

    依赖倒置原则的作用

    (1)依赖倒置原则可以降低类间的耦合性。

    (2)依赖倒置原则可以提高系统的稳定性。

    (3)依赖倒置原则可以减少并行开发引起的风险。

    (4)依赖倒置原则可以提高代码的可读性和可维护性。

    依赖倒置原则的实现方法

    依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。

    (1)每个类尽量提供接口或抽象类,或者两者都具备。

    (2)变量的声明类型尽量是接口或者是抽象类。

    (3)任何类都不应该从具体类派生。

    (4)使用继承时尽量遵循里氏替换原则

    例子:

    现需要将存储在TXT或Excel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。在客户数据操作类中将调用数据格式转换类的方法实现格式转换和数据库插入操作,初始设计方案结构如图所示:

          在编码实现图所示结构时,Sunny软件公司开发人员发现该设计方案存在一个非常严重的问题,由于每次转换数据时数据来源不一定相同,因此需要更换数据转换类,如有时候需要将TXTDataConvertor改为ExcelDataConvertor,此时,需要修改CustomerDAO的源代码,而且在引入并使用新的数据转换类时也不得不修改CustomerDAO的源代码,系统扩展性较差,违反了开闭原则,现需要对该方案进行重构。

          在本实例中,由于CustomerDAO针对具体数据转换类编程,因此在增加新的数据转换类或者更换数据转换类时都不得不修改CustomerDAO的源代码。我们可以通过引入抽象数据转换类解决该问题,在引入抽象数据转换类DataConvertor之后,CustomerDAO针对抽象类DataConvertor编程,而将具体数据转换类名存储在配置文件中,符合依赖倒转原则。根据里氏代换原则,程序运行时,具体数据转换类对象将替换DataConvertor类型的对象,程序不会出现任何问题。更换具体数据转换类时无须修改源代码,只需要修改配置文件;如果需要增加新的具体数据转换类,只要将新增数据转换类作为DataConvertor的子类并修改配置文件即可,原有代码无须做任何修改,满足开闭原则。重构后的结构如图所示:

          在上述重构过程中,我们使用了开闭原则、里氏代换原则和依赖倒转原则,在大多数情况下,这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。

    五、接口隔离原则(Interface  Segregation Principle, ISP)

    定义:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

    根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,比如Java语言中的interface。对于这两种不同的含义,ISP的表达方式以及含义都有所不同:

     (1) 当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可以叫做“角色隔离原则”。

    (2) 如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。在面向对象编程语言中,实现一个接口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要将大接口中的方法根据其职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。

    接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

    (1)单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。

    (2)单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

    接口隔离原则的优点

    接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。

    (1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

    (2)接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。

    (3)如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。

    (4)使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。

    (5)能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

    接口隔离原则的实现方法

    在具体应用接口隔离原则时,应该根据以下几个规则来衡量。

    (1)接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。

    (2)为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。

    (3)了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。

    (4)提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

    下面通过一个简单实例来加深对接口隔离原则的理解:

    Sunny软件公司开发人员针对某CRM系统的客户数据显示模块设计了如图1所示接口,其中方法dataRead()用于从文件中读取数据,方法transformToXML()用于将数据转换成XML格式,方法createChart()用于创建图表,方法displayChart()用于显示图表,方法createReport()用于创建文字报表,方法displayReport()用于显示文字报表。

          在实际使用过程中发现该接口很不灵活,例如如果一个具体的数据显示类无须进行数据转换(源文件本身就是XML格式),但由于实现了该接口,将不得不实现其中声明的transformToXML()方法(至少需要提供一个空实现);如果需要创建和显示图表,除了需实现与图表相关的方法外,还需要实现创建和显示文字报表的方法,否则程序编译时将报错。

          现使用接口隔离原则对其进行重构。

          在图中,由于在接口CustomerDataDisplay中定义了太多方法,即该接口承担了太多职责,一方面导致该接口的实现类很庞大,在不同的实现类中都不得不实现接口中定义的所有方法,灵活性较差,如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量;另一方面由于客户端针对大接口编程,将在一定程序上破坏程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。因此需要将该接口按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可。

          通过使用接口隔离原则,本实例重构后的结构如图所示:

          在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。

    六、迪米特法则(Law of  Demeter, LoD)

     

    定义:迪米特法则(Law of  Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。

    迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学(Northeastern University)的一个名为迪米特(Demeter)的研究项目,由伊恩·荷兰(Ian Holland)提出,被 UML 创始者之一的布奇(Booch)普及,后来又因为在经典著作《程序员修炼之道》(The Pragmatic Programmer)提及而广为人知。

    迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

    迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

    在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与“陌生人”发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。

    迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。

    在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

    迪米特法则的优点

    迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。

    降低了类之间的耦合度,提高了模块的相对独立性。

    由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

    但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰

    迪米特法则的实现方法

    从迪米特法则的定义和特点可知,它强调以下两点:

    从依赖者的角度来说,只依赖应该依赖的对象。

    从被依赖者的角度说,只暴露应该暴露的方法。

    所以,在运用迪米特法则时要注意以下 6 点。

    在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。

    在类的结构设计上,尽量降低类成员的访问权限。

    在类的设计上,优先考虑将一个类设置成不变类。

    在对其他类的引用上,将引用其他对象的次数降到最低。

    不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。

    谨慎使用序列化(Serializable)功能。

    下面通过一个简单实例来加深对迪米特法则的理解:

    Sunny软件公司所开发CRM系统包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可简化为如图所示结构:

          在图中,由于界面控件之间的交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件。

          现使用迪米特对其进行重构。

          在本实例中,可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码,重构后结构如图所示:

    总结

    单一职责原则告诉我们实现类要职责单一

    里氏替换原则告诉我们不要破坏继承体系

    依赖倒置原则告诉我们要面向接口编程

    接口隔离原则告诉我们在设计接口的时候要精简单一

    迪米特原则告诉我们要降低耦合

    开闭原则是总纲,告诉我们要对扩展开放,对修改关闭

    展开全文
  • 设计模式大原则——SOLID

    万次阅读 多人点赞 2018-08-12 20:34:32
    设计模式的六大原则有: Single Responsibility Principle:单一职责原则 Open Closed Principle:开闭原则 Liskov Substitution Principle:里氏替换原则 Law of Demeter:迪米特法则 Interface Segregation ...

    SOLID

    设计模式的六大原则有:

    • Single Responsibility Principle:单一职责原则
    • Open Closed Principle:开闭原则
    • Liskov Substitution Principle:里氏替换原则
    • Law of Demeter:迪米特法则
    • Interface Segregation Principle:接口隔离原则
    • Dependence Inversion Principle:依赖倒置原则

    把这六个原则的首字母联合起来( L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计

    下面我们来分别看一下这六大设计原则。

    单一职责原则(Single Responsibility Principle)


    单一职责原则简称 SRP ,顾名思义,就是一个类只负责一个职责。它的定义也很简单:

    There should never be more than one reason for a class to change.

    这句话很好理解,就是说,不能有多个导致类变更的原因。

    那这个原则有什么用呢,它让类的职责更单一。这样的话,每个类只需要负责自己的那部分,类的复杂度就会降低。如果职责划分得很清楚,那么代码维护起来也更加容易。试想如果所有的功能都放在了一个类中,那么这个类就会变得非常臃肿,而且一旦出现bug,要在所有代码中去寻找;更改某一个地方,可能要改变整个代码的结构,想想都非常可怕。当然一般时候,没有人会去这么写的。

    当然,这个原则不仅仅适用于类,对于接口和方法也适用,即一个接口/方法,只负责一件事,这样的话,接口就会变得简单,方法中的代码也会更少,易读,便于维护。

    事实上,由于一些其他的因素影响,类的单一职责在项目中是很难保证的。通常,接口和方法的单一职责更容易实现。

    单一原则的好处:

    • 代码的粒度降低了,类的复杂度降低了。
    • 可读性提高了,每个类的职责都很明确,可读性自然更好。
    • 可维护性提高了,可读性提高了,一旦出现 bug ,自然更容易找到他问题所在。
    • 改动代码所消耗的资源降低了,更改的风险也降低了。

    里氏替换原则(Liskov Substitution Principle)

    里氏替换原则的定义如下:

    Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it. 
    

    翻译过来就是,所有引用基类的地方必须能透明地使用其子类的对象。

    里氏替换原则的意思是,所有基类在的地方,都可以换成子类,程序还可以正常运行。这个原则是与面向对象语言的 继承 特性密切相关的。

    这是为什么呢?由于面向对象语言的继承特性,子类拥有父类的所有方法,因此,将基类替换成具体的子类,子类也可以调用父类中的方法(其实是它自己的方法,继承于父类),但是如果要保证完全可以调用,光名称相同不行,还需要满足下面两个条件:

    • 子类中的方法的前置条件必须与超类中被覆写的方法的前置条件相同或更宽松。
    • 子类中的方法的后置条件必须与超类中被覆写的方法的后置条件相同或更严格。

    这样的话,调用就没有问题了。否则,我在父类中传入一个 List 类型的参数,子类中重写的方法参数却变为 ArrayList ,那客户端使用的时候传入一个 LinkedList 类型的参数,使用父类的时候程序正常运行,但根据 LSP 原则,替换成子类后程序就会出现问题。同理,后置条件也是如此。

    是不是很好理解?

    但是这个原则并不是这么简单的,语法上是肯定不能有任何问题。但是,同时,功能上也要和替换前相同。

    那么这就有些困难了,因为子类重写父类中的方法天经地义,子类中的方法不同于父类中的方法也是很正常的,但是,如果这么随便重写父类中的方法的话,那么肯定会违背 LSP 原则,可以看下面的例子:

    首先有一个父类,水果类:

    public class Fruit {
        void introduce() {
            System.out.println("我是水果父类...");
        }
    }

    其次,它有一个子类,苹果类:

    public class Apple extends Fruit {
        @Override
        void introduce() {
            System.out.println("我是水果子类——苹果");
        }
    }

    客户端代码如下:

    public static void main(String[] args) {
        Fruit fruit = new Fruit();
        HashMap map = new HashMap<>();
        fruit.introduce();
    }

    运行结果:

    我是水果父类...
    

    那么,如果按照 LSP 原则,所有父类出现的地方都能换成其子类,代码如下:

    public static void main(String[] args) {
        Apple fruit = new Apple();
        HashMap map = new HashMap<>();
        fruit.introduce();
    }

    那么运行结果就会变成:

    我是水果子类——苹果
    

    与原来的输出不同,程序的功能被改变了,违背了 LSP 原则。

    因此,可以看到, LSP 原则最重要的一点就是:避免子类重写父类中已经实现的方法。这就是 LSP 原则的本质。这里由于 Fruit 父类已经实现了 introduce 方法,因此子类应该避免再对其进行重写,如果需要增加个性化,就应该对父类进行扩展,而不是重写,否则也会违背开闭原则。

    一般来讲,程序中父类大多是抽象类,因为父类只是一个框架,具体功能还需要子类来实现。因此很少直接去 new 一个父类。而如果出现这种情况,那么就说明父类中实现的代码已经很好了,子类只需要对其进行扩展就会,尽量避免对其已经实现的方法再去重写。

    依赖倒置原则(Dependence Inversion Principle)


    依赖倒置原则的原始定义是这样的:

    High level modules should not depend upon low level modules. 
    Both should depend upon abstractions. 
    Abstractions should not depend upon details. Details should depend upon abstractions.
    

    翻译一下,就是下面三句话:

    • 高层模块不应该依赖底层模块,两者都应该依赖其抽象。
    • 抽象不应该依赖细节。
    • 细节应该依赖抽象。

    在Java语言中的表现就是:

    • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
    • 接口或抽象类不依赖于实现类。
    • 实现类依赖于接口或抽象类。

    简而言之,我们要尽可能使用接口或抽象类。也就是“面向接口编程” 或者说 “面向抽象编程” ,也就是说程序中要尽可能使用抽象类或是接口。

    可能现在还没有使用抽象的习惯,可以看一个例子:比如我们要写一个人吃苹果的程序,首先创建一个苹果类:

    public class Apple {
        public void eaten() {
            System.out.println("正在吃苹果...");
        }
    }
    

    然后写人的类:

    public class Person {
        void eat(Apple apple) {
            apple.eaten();
        }   
    }

    这样,在客户端中我们就可以这样调用:

    Person xiaoMing = new Person();
    Apple apple = new Apple();
    xiaoMing.eat(apple);

    但是这样就有一个问题,如果我不仅仅想吃苹果,还想吃橘子怎么办,在 Person 类中再加一个函数处理橘子?那么吃别的水果呢??总不能程序中每增加一种水果就要在其中增加一个函数吧。

    这时候就需要接口了(抽象类也是可以的)

    程序就可以这样更改,增加一个水果接口:

    public interface Fruit {
        void eaten(); 
    }

    让苹果类实现该接口:

    public class Apple implements Fruit {
        @Override
        public void eaten() {
            System.out.println("正在吃苹果...");
        }
    }

    然后将Person类中的函数的参数稍作修改:

    public class Person {
        void eat(Fruit fruit) {
            fruit.eaten();
        }
    }
    

    这样,客户端代码也稍作修改:

    Person xiaoMing = new Person();
    Fruit apple = new Apple();
    xiaoMing.eat(apple);

    这样改完之后,如果再增加一种新的水果,就不需要改变 Person 类了,方便吧。

    那么再回来说我们的依赖倒置原则,依赖就是类与类之间的依赖,主要是函数传参,上面的例子已经很明白地介绍了,参数要尽可能使用抽象类或接口,这就是“高层模块不应该依赖底层模块,两者都应该依赖其抽象” 的解释。那么如果要实现这个,就要求每个实现类都应该尽可能从抽象中派生,这就是上面的 “细节应该依赖抽象”

    简而言之,该原则主要有下面几点要求:

    • 每个类都尽量要有接口或抽象类,或者两者都有
    • 变量的表面类型尽量是接口或者抽象类(比如程序中的 Fruit apple = new Apple() Fruit 是表面类型, Apple 是实际类型)
    • 任何类都不应该从具体类中派生

    接口隔离原则(Interface Segregation Principle)


    首先声明,该原则中的接口,是一个泛泛而言的接口,不仅仅指Java中的接口,还包括其中的抽象类。

    首先,给出该原则的定义,该原则有两个定义:

    1. Clients should not be forced to depend upon interfaces that they don`t use.

      客户端不应该依赖它不需要的接口。

    2. The dependency of one class to another one should depend on the smallest possible.

      类间的依赖关系应该建立在最小的接口上。

    这是什么意思呢,这是让我们把接口进行细分。举个例子,如果一个类实现一个接口,但这个接口中有它不需要的方法,那么就需要把这个接口拆分,把它需要的方法提取出来,组成一个新的接口让这个类去实现,这就是接口隔离原则。简而言之,就是说,接口中的所有方法对其实现的子类都是有用的。否则,就将接口继续细分。

    看起来,该原则与单一职责原则很相像。确实很像,二者都是强调要将接口进行细分,只不过分的方式不同。单一职责原则是按照 职责 进行划分接口的;而接口隔离原则则是按照实现类对方法的使用来划分的。可以说,接口隔离原则更细一些。

    要想完美地实现该原则,基本上就需要每个实现类都有一个专用的接口。但实际开发中,这样显然是不可能的,而且,这样很容易违背单一职责原则(可能出现同一个职责分成了好几个接口的情况),因此我们能做的就是尽量细分。

    该原则主要强调两点:

    1. 接口尽量小。

      就像前面说的那样,接口中只有实现类中有用的方法。

    2. 接口要高内聚

      就是说,在接口内部实现的方法,不管怎么改,都不会影响到接口外的其他接口或是实现类,只能影响它自己。

    迪米特法则(Law of Demeter)


    迪米特法则也叫最少知道原则(Least Knowledge Principle, LKP ),虽然名称不同,但都是同一个意思:一个对象应该对其他对象有最少的了解。

    该原则也很好理解,我们在写一个类的时候,应该尽可能的少暴露自己的接口。什么意思呢?就是说,在写类的时候,能不 public 就不 public ,所有暴露的属性或是接口,都是不得不暴露的,这样的话,就能保证其他类对这个类有最少的了解了。

    这个原则也没什么需要多讲的,调用者只需要知道被调用者公开的方法就好了,至于它内部是怎么实现的或是有其他别的方法,调用者并不关心,调用者只关心它需要用的。反而,如果被调用者暴露太多不需要暴露的属性或方法,那么就可能导致调用者滥用其中的方法,或是引起一些其他不必要的麻烦。

    开闭原则(Open Closed Principle)


    开闭原则所有设计模式原则中,最基础的那个原则。首先,还是先来看一下它的定义:

    Software entities like classes, modules and functions should be open for extension but closed for modifications.
    

    翻译过来就是:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。

    开闭原则之前也提到过,在 LSP 中,我们说,要避免子类重写父类中已经实现的方法。这个时候,继承父类就是对其进行扩展,但没有进行修改。这就是开闭原则一个很好的体现。

    那是不是开闭原则与 LSP 原则混淆了呢?并不是, LSP 原则强调的是基类与子类的关系,只是其中的一种实现方式用到了开闭原则而已。

    那么开闭原则具体是什么呢?可以说,开闭原则贯穿于以上五个设计模式原则。开闭原则中的对扩展开放,就是说,如果在项目中添加一个功能的时候,可以直接对代码进行扩展;如果要修改某一部分的功能时,我们应该做的是,尽量少做修改(完全不修改是不可能的),但是修改的时候,要保留原来的功能,只是在上面扩展出新的功能,就像版本更新一样,更新后,依然支持旧版本。

    开闭原则是一个特别重要的原则,无论是在设计模式中还是在其他领域中,这都是一个非常基础的设计理念。

    总结

    总而言之,这六个设计模式原则是以后学习设计模式的基础,它们的共同目的就是 SOLID ——建立稳定、灵活、健壮的设计。

    但是原则归原则,有时候由于种种原因,这些条条框框是不得不去打破的,一味地遵循它是不会有好果子吃的(就像接口隔离原则,不可能创建那么多的接口)。因此我们应该正确使用这些原则,主要目的还是为了我们软件的稳定性、灵活性、健壮性和可维护性。

    注:本文所有原则定义皆出自《设计模式之禅》。

    展开全文
  • 学习6设计原则、23种设计模式

    万次阅读 多人点赞 2018-03-14 17:35:57
    了解设计模式的朋友们,想必都听说过“六设计原则”吧。其实最经典的 23 种设计模式中或多或少地都在使用这些设计原则,也就是说,设计模式是站在设计原则的基础之上的。所以在学习设计模式之前,很有必要对这些...

            了解设计模式的朋友们,想必都听说过“六大设计原则”吧。其实最经典的 23 种设计模式中或多或少地都在使用这些设计原则,也就是说,设计模式是站在设计原则的基础之上的。所以在学习设计模式之前,很有必要对这些设计原则先做一下了解。


    1、单一职责原则

    There should never be more than one reason for a class to change.

    理解:不同的类具备不同的职责,各司其职。做系统设计是,如果发现有一个类拥有了两种职责,那么就要问一个问题:可以将这个类分成两个类吗?如果真的有必要,那就分开,千万不要让一个类干的事情太多。

    总结:一个类只承担一个职责

    2、开放封闭原则

    Software entities like classes,modules and functions should be open for extension but closed for modifications.

    理解:类、模块、函数,可以去扩展,但不要去修改。如果要修改代码,尽量用继承或组合的方式来扩展类的功能,而不是直接修改类的代码。当然,如果能保证对整个架构不会产生任何影响,那就没必要搞的那么复杂,直接改这个类吧。

    总结:对软件实体的改动,最好用扩展而非修改的方式。

    3、里式替换原则

    Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

    理解:父类可被子类替换,但反之不一定成立。也就是说,代码中可以将父类全部替换为子类,程序不会出现异常,但反过来就不一定了。

    总结:在继承类是,务必重写(override)父类中所有的方法,尤其需要注意父类的protected方法(它们往往是让你重写的),子类尽量不要暴露自己的public方法供外界调用。

    4、最少知识原则

    Only talk to you immediate friends.

    理解:尽量减少对象之间的交互,从而减小类之间的耦合。在做系统设计时,不要让一个类依赖于太多其他的类,需尽量减小依赖关系,否则死都不知道怎么死的。

    总结:一定要做到:低耦合、高内聚。

    5、接口隔离原则

    The dependency of one class to another one should depend on the smallest possible interface.

    理解:不要对外暴露没有实际意义的接口。也就是说,尽量保证接口的实用性。当需要对外暴露接口时,需要再三斟酌,若没必要对外提供就删了吧,因为一旦提供了就意味着,将来要多做一件事情,何苦给自己找事做呢。

    总结:不要对外暴露没有实际意义的接口。

    6、依赖倒置原则

    High level modules should not depends upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.

    理解:高层模块不应该依赖于底层模块,而应该依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。应该面向接口编程,不该面向实现类编程。面向实现类编程相当于就事论事,那是正向依赖;面向接口编程,相当于透过现象看本质,抓住事务的共性,那就是反向依赖,即依赖倒置。

    总结:面向接口编程,提取出事务的本质和共性。


    将六大原则的英文首字母拼在一起就是SOLID(稳定的),所以也称之为SOLID原则。

    只有满足了这六大原则,才能设计出稳定的软件架构,但它们只是原则,有时还是需要学会灵活应变,千万不要生搬硬套,否则只会把简单问题复杂化,切记!

    23中设计模式转自:http://zz563143188.iteye.com/blog/1847029

    最经典的 23 种设计模式中或多或少地都在使用这些设计原则,也就是说,设计模式是站在设计原则的基础之上的。下面来详细介绍Java中23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析。

    总体来说23种设计模式分为三大类:

    创建型模式(5种):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

    结构型模式(7种):适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

    行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

    1、工厂方法模式

    1.1普通工厂模式:就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。

    举例如下:(我们举一个发送邮件和短信的例子)

    首先,创建二者的共同接口:

    [java]  view plain copy
    1. public interface Sender {  
    2.     public void Send();  
    3. }  

    其次,创建实现类:

    [java]  view plain copy
    1. public class MailSender implements Sender {  
    2.     @Override  
    3.     public void Send() {  
    4.         System.out.println("this is mailsender!");  
    5.     }  
    6. }  
    [java]  view plain copy
    1. public class SmsSender implements Sender {  
    2.   
    3.     @Override  
    4.     public void Send() {  
    5.         System.out.println("this is sms sender!");  
    6.     }  
    7. }  

    最后,建工厂类:

    [java]  view plain copy
    1. public class SendFactory {  
    2.   
    3.     public Sender produce(String type) {  
    4.         if ("mail".equals(type)) {  
    5.             return new MailSender();  
    6.         } else if ("sms".equals(type)) {  
    7.             return new SmsSender();  
    8.         } else {  
    9.             System.out.println("请输入正确的类型!");  
    10.             return null;  
    11.         }  
    12.     }  
    13. }  

    我们来测试下:

    1. public class FactoryTest {  
    2.   
    3.     public static void main(String[] args) {  
    4.         SendFactory factory = new SendFactory();  
    5.         Sender sender = factory.produce("sms");  
    6.         sender.Send();  
    7.     }  
    8. }  

    输出:this is sms sender!

    1.2多个工厂方法模式:是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。

    将上面的代码做下修改,改动下SendFactory类就行,如下:

    [java]  view plain copy public   class  SendFactory {  
        public  Sender produceMail(){  
    1.         return new MailSender();  
    2.     }  
    3.       
    4.     public Sender produceSms(){  
    5.         return new SmsSender();  
    6.     }  
    7. }  

    测试类如下:

    [java]  view plain copy
    1. public class FactoryTest {  
    2.   
    3.     public static void main(String[] args) {  
    4.         SendFactory factory = new SendFactory();  
    5.         Sender sender = factory.produceMail();  
    6.         sender.Send();  
    7.     }  
    8. }  

    输出:this is mailsender!

    1.3 静态工厂方法模式:将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

    [java]  view plain copy
    1. public class SendFactory {  
    2.       
    3.     public static Sender produceMail(){  
    4.         return new MailSender();  
    5.     }  
    6.       
    7.     public static Sender produceSms(){  
    8.         return new SmsSender();  
    9.     }  
    10. }  
    [java]  view plain copy
    1. public class FactoryTest {  
    2.   
    3.     public static void main(String[] args) {      
    4.         Sender sender = SendFactory.produceMail();  
    5.         sender.Send();  
    6.     }  
    7. }  

    输出:this is mailsender!

    总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。

    1.4抽象工厂模式:工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。

    请看例子:

    [java]  view plain copy
    1. public interface Sender {  
    2.     public void Send();  
    3. }  

    两个实现类:

    [java]  view plain copy
    1. public class MailSender implements Sender {  
    2.     @Override  
    3.     public void Send() {  
    4.         System.out.println("this is mailsender!");  
    5.     }  
    6. }  
    [java]  view plain copy
    1. public class SmsSender implements Sender {  
    2.   
    3.     @Override  
    4.     public void Send() {  
    5.         System.out.println("this is sms sender!");  
    6.     }  
    7. }  

    两个工厂类:

    [java]  view plain copy
    1. public class SendMailFactory implements Provider {  
    2.       
    3.     @Override  
    4.     public Sender produce(){  
    5.         return new MailSender();  
    6.     }  
    7. }  
    [java]  view plain copy
    1. public class SendSmsFactory implements Provider{  
    2.   
    3.     @Override  
    4.     public Sender produce() {  
    5.         return new SmsSender();  
    6.     }  
    7. }  

    在提供一个接口:

    [java]  view plain copy
    1. public interface Provider {  
    2.     public Sender produce();  
    3. }  

    测试类:

    [java]  view plain copy
    1. public class Test {  
    2.   
    3.     public static void main(String[] args) {  
    4.         Provider provider = new SendMailFactory();  
    5.         Sender sender = provider.produce();  
    6.         sender.Send();  
    7.     }  
    8. }  

    其实这个模式的好处就是,如果你现在想增加一个功能:发及时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!

    2、单例模式

    单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

    1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

    2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

    3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

    首先我们写一个简单的单例类:

    [java]  view plain copy
    1. public class Singleton {  
    2.   
    3.     /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */  
    4.     private static Singleton instance = null;  
    5.   
    6.     /* 私有构造方法,防止被实例化 */  
    7.     private Singleton() {  
    8.     }  
    9.   
    10.     /* 静态工程方法,创建实例 */  
    11.     public static Singleton getInstance() {  
    12.         if (instance == null) {  
    13.             instance = new Singleton();  
    14.         }  
    15.         return instance;  
    16.     }  
    17.   
    18.     /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    19.     public Object readResolve() {  
    20.         return instance;  
    21.     }  
    22. }  


    这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对getInstance方法加synchronized关键字,如下:

    [java]  view plain copy
    1. public static synchronized Singleton getInstance() {  
    2.         if (instance == null) {  
    3.             instance = new Singleton();  
    4.         }  
    5.         return instance;  
    6.     }  

    但是,synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个:

    [java]  view plain copy
    1. public static Singleton getInstance() {  
    2.         if (instance == null) {  
    3.             synchronized (instance) {  
    4.                 if (instance == null) {  
    5.                     instance = new Singleton();  
    6.                 }  
    7.             }  
    8.         }  
    9.         return instance;  
    10.     }  

    似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:

    a>A、B线程同时进入了第一个if判断

    b>A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();

    c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

    d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

    e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。

    所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:

    [java]  view plain copy
    1. private static class SingletonFactory{           
    2.         private static Singleton instance = new Singleton();           
    3.     }           
    4.     public static Singleton getInstance(){           
    5.         return SingletonFactory.instance;           
    6.     }   

    实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:

    [java]  view plain copy
    1. public class Singleton {  
    2.   
    3.     /* 私有构造方法,防止被实例化 */  
    4.     private Singleton() {  
    5.     }  
    6.   
    7.     /* 此处使用一个内部类来维护单例 */  
    8.     private static class SingletonFactory {  
    9.         private static Singleton instance = new Singleton();  
    10.     }  
    11.   
    12.     /* 获取实例 */  
    13.     public static Singleton getInstance() {  
    14.         return SingletonFactory.instance;  
    15.     }  
    16.   
    17.     /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    18.     public Object readResolve() {  
    19.         return getInstance();  
    20.     }  
    21. }  

    其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:

    [java]  view plain copy
    1. public class SingletonTest {  
    2.   
    3.     private static SingletonTest instance = null;  
    4.   
    5.     private SingletonTest() {  
    6.     }  
    7.   
    8.     private static synchronized void syncInit() {  
    9.         if (instance == null) {  
    10.             instance = new SingletonTest();  
    11.         }  
    12.     }  
    13.   
    14.     public static SingletonTest getInstance() {  
    15.         if (instance == null) {  
    16.             syncInit();  
    17.         }  
    18.         return instance;  
    19.     }  
    20. }  

    考虑性能的话,整个程序只需创建一次实例,所以性能也不会有什么影响。

    补充:采用"影子实例"的办法为单例对象的属性同步更新

    [java]  view plain copy
    1. public class SingletonTest {  
    2.   
    3.     private static SingletonTest instance = null;  
    4.     private Vector properties = null;  
    5.   
    6.     public Vector getProperties() {  
    7.         return properties;  
    8.     }  
    9.   
    10.     private SingletonTest() {  
    11.     }  
    12.   
    13.     private static synchronized void syncInit() {  
    14.         if (instance == null) {  
    15.             instance = new SingletonTest();  
    16.         }  
    17.     }  
    18.   
    19.     public static SingletonTest getInstance() {  
    20.         if (instance == null) {  
    21.             syncInit();  
    22.         }  
    23.         return instance;  
    24.     }  
    25.   
    26.     public void updateProperties() {  
    27.         SingletonTest shadow = new SingletonTest();  
    28.         properties = shadow.getProperties();  
    29.     }  
    30. }  

    通过单例模式的学习告诉我们:

    1、单例模式理解起来简单,但是具体实现起来还是有一定的难度。

    2、synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。

    到这儿,单例模式基本已经讲完了,结尾处,笔者突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?

    首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)

    其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。

    再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。

    最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

    3、建造者模式

    工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到的。我们看一下代码:

    还和前面一样,一个Sender接口,两个实现类MailSender和SmsSender。最后,建造者类如下:

    [java]  view plain copy
    1. public class Builder {  
    2.       
    3.     private List<Sender> list = new ArrayList<Sender>();  
    4.       
    5.     public void produceMailSender(int count){  
    6.         for(int i=0; i<count; i++){  
    7.             list.add(new MailSender());  
    8.         }  
    9.     }  
    10.       
    11.     public void produceSmsSender(int count){  
    12.         for(int i=0; i<count; i++){  
    13.             list.add(new SmsSender());  
    14.         }  
    15.     }  
    16. }  

    测试类:

    [java]  view plain copy
    1. public class Test {  
    2.   
    3.     public static void main(String[] args) {  
    4.         Builder builder = new Builder();  
    5.         builder.produceMailSender(10);  
    6.     }  
    7. }  

    从这点看出,建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工厂模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。

    4、原型模式

    原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。在Java中,复制对象是通过clone()实现的,先创建一个原型类:

    [java]  view plain copy
    1. public class Prototype implements Cloneable {  
    2.   
    3.     public Object clone() throws CloneNotSupportedException {  
    4.         Prototype proto = (Prototype) super.clone();  
    5.         return proto;  
    6.     }  
    7. }  

    很简单,一个原型类,只需要实现Cloneable接口,覆写clone方法,此处clone方法可以改成任意的名称,因为Cloneable接口是个空接口,你可以任意定义实现类的方法名,如cloneA或者cloneB,因为此处的重点是super.clone()这句话,super.clone()调用的是Object的clone()方法,而在Object类中,clone()是native的,具体怎么实现,我会在另一篇文章中,关于解读Java中本地方法的调用,此处不再深究。在这儿,我将结合对象的浅复制和深复制来说一下,首先需要了解对象深、浅复制的概念:

    浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。

    深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。

    此处,写一个深浅复制的例子:

    [java]  view plain copy
    1. public class Prototype implements Cloneable, Serializable {  
    2.   
    3.     private static final long serialVersionUID = 1L;  
    4.     private String string;  
    5.   
    6.     private SerializableObject obj;  
    7.   
    8.     /* 浅复制 */  
    9.     public Object clone() throws CloneNotSupportedException {  
    10.         Prototype proto = (Prototype) super.clone();  
    11.         return proto;  
    12.     }  
    13.   
    14.     /* 深复制 */  
    15.     public Object deepClone() throws IOException, ClassNotFoundException {  
    16.   
    17.         /* 写入当前对象的二进制流 */  
    18.         ByteArrayOutputStream bos = new ByteArrayOutputStream();  
    19.         ObjectOutputStream oos = new ObjectOutputStream(bos);  
    20.         oos.writeObject(this);  
    21.   
    22.         /* 读出二进制流产生的新对象 */  
    23.         ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());  
    24.         ObjectInputStream ois = new ObjectInputStream(bis);  
    25.         return ois.readObject();  
    26.     }  
    27.   
    28.     public String getString() {  
    29.         return string;  
    30.     }  
    31.   
    32.     public void setString(String string) {  
    33.         this.string = string;  
    34.     }  
    35.   
    36.     public SerializableObject getObj() {  
    37.         return obj;  
    38.     }  
    39.   
    40.     public void setObj(SerializableObject obj) {  
    41.         this.obj = obj;  
    42.     }  
    43.   
    44. }  
    45.   
    46. class SerializableObject implements Serializable {  
    47.     private static final long serialVersionUID = 1L;  
    48. }  
     
    要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。


    展开全文
  • 设计模式是五或六还是七大原则?设计模式有五大原则或七大原则之分 按五大原则划分:1、2、3和4(算一种)、5和6(算一种)、7 按六大原则划分:1、2、3、4、5和6(算一种)、7 按七大原则划分:1、2、3、4、5...

    设计模式是五大或六大还是七大原则?

    设计模式有五大原则或七大原则之分
    按五大原则划分:1、2、3和4(算一种)、5和6(算一种)、7
    按六大原则划分:1、2、3、4、5和6(算一种)、7
    按七大原则划分:1、2、3、4、5、6、7

    1.单一职责原则(Single Responsibility Principle,SRP):类的职责要单一,不能将太多的职责放在一个类中。(高内聚、低耦合)

    2.开闭原则( Open - ClosedPrinciple ,OCP ):对扩展开放,对修改关闭(设计模式的核心原则是)

    3.依赖倒转原则( Dependence Inversion Principle ,DIP ):要依赖抽象,而不要依赖具体的实现.

    4.里氏代换原则( Liskov Substitution Principle ,LSP ):任何基类可以出现的地方,子类也可以出现

    5.迪米特法则(Law of Demeter,LoD:系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度
    定义:又叫最少知识原则(Least Knowledge Principle或简写为LKP)几种形式定义:
    (1) 不要和“陌生人”说话。英文定义为:Don’t talk to strangers.
    (2) 只与你的直接朋友通信。英文定义为:Talk only to your immediatefriends.

    6.接口隔离法则(Interface Segregation Principle,ISL):客户端不应该依赖那些它不需要的接口。(这个法则与迪米特法则是相通的)

    7 .合成/聚合复用原则(Composite/Aggregate ReusePrinciple ,CARP):要尽量使用对象组合,而不是继承关系达到软件复用的目的

    其中合成/聚合复用原则又可以细分为二种
    聚合(Aggregation) 关系是关联关系的一种,是弱的关联关系。比如乐队(主唱,吉他手,贝斯手)
    组合(Composition) 关系是关联关系的一种,是强的关联关系。比如每名乐手(灵魂、身体)

    版权所有,转载请注明文章出处 http://blog/csdn.net/cadenzasolo

    展开全文
  • 小甲鱼零基础入门学习python笔记

    万次阅读 多人点赞 2019-08-14 11:06:30
    以简单的方式快速完成某些复杂的事情通常是创造脚本语言的重要原则,基于这项原则,使得脚本语言通常比 C 语言、C++语言 或 Java 之类的系统编程语言要简单容易。也让脚本语言另有一些属于脚本语言的特性: • 语法...
  • 设计模式:七大原则

    千次阅读 多人点赞 2021-03-01 14:39:25
    接口隔离原则5.1、原则介绍5.2、错误示范5.3、正确示范第六章 依赖倒转原则6.1、原则介绍6.2、错误示范6.3、正确示范第七章 其他两大原则7.1、合成复用原则7.2、最少知识原则 项目地址:...
  • RocketMQ

    万次阅读 多人点赞 2019-07-31 19:17:34
    RocketMQ并不遵循包括JMS规范在内的任何规范,但是参考了各种规范不同类产品的设计思想,自己有一套自定义的机制,简单来说就是使用订阅主题的方式去发送和接收任务,但是支持集群和广播种消息模式。开源项目地址...
  • 面向对象其它六大原则 单一职责原则-带你走梦幻西游(一) 依赖倒置原则(二) 开闭原则(三) 迪米特原则-带你走进梦幻西游(四) 接口隔离原则(六)里氏代换原则是由麻省理工学院(MIT)计算机科学实验室的...
  • 设计模式大原则--合成/聚合复用原则

    千次阅读 热门讨论 2014-02-17 13:58:05
    定义 尽量使用合成/聚合,尽量不要使用类继承。(Design to interfaces;Favor composition over inheritance;Find what varies andencapsulate it)
  • 设计模式系列:OOP设计6大原则

    千次阅读 2017-02-22 08:10:43
    用面向对象的语言来讲,OCP 是一个最抽象的接口,而其余的5大原则只是OCP 的子类接口,他们一起定义了 OOP 世界的开发标准,常用的23中设计模式更是只能算作这6大原则的实现抽象类,咱们开发的代码实践才是真正的...
  • 设计模式之6设计原则详解

    千次阅读 2019-06-29 22:56:16
    做系统设计是,如果发现有一个类拥有了种职责,那么就要问一个问题:可以将这个类分成个类吗?如果真的有必要,那就分开,千万不要让一个类干的事情太多。 总结:一个类只承担一个职责 2、开放封闭原则 Soft...
  • MySQL 面试题

    万次阅读 多人点赞 2019-09-02 16:03:33
    因为 MySQL 还会有部分内容和运维相关度比较高,所以本文我们分成部分【开发】【运维】部分。 对于【开发】部分,我们需要掌握。 对于【运维】部分,更多考验开发的知识储备情况,当然能回答出来是比较好的...
  • Spring入门第一讲——Spring框架的快速入门

    万次阅读 多人点赞 2017-04-08 00:27:34
    说到这里,我就不得不稍微讲一下面向对象设计的七大原则了,它不必强记,重在理解。 出现的这个问题该如何解决呢?解决方法是使用工厂设计模式进行解耦合操作。所以,我们需要创建一个工厂类,在工厂类中提供一个...
  • 单一职责原则 There should never be more than one reason for a class to change.  单一职责原则的好处: 类的复杂度降低,实现什么职责都有清晰明确的定义; 可读性提高 可维护性提高 变更引起的风险降低...
  • 前端面试题

    万次阅读 多人点赞 2019-08-08 11:49:01
    前端面试题汇总 ... 你做的页面在哪些流览器测试过?这些浏览器的内核分别是什么?...它和Standards模式有什么区别 21 div+css的布局较table布局有什么优点? 22 img的alt与title有何异同? strong与em的异同? 22 你能...
  • 测试开发笔记

    万次阅读 多人点赞 2019-11-14 17:11:58
    测试开发笔记 第一章 测试基础 7 什么是软件测试: 7 ★软件测试的目的、意义:(怎么做好软件测试) 7 ...4.测试过程(干什么,怎么干) 12 5.各阶段输入、输出标准以及入口、出口准则:(测试阶段过程要素) 1...
  • 【数据库学习】数据库总结

    万次阅读 多人点赞 2018-07-26 13:26:41
    原则:遵从概念单一化“一事一地”原则,即一个关系模式描述一个实体或实体间的一种联系。 规范的实质:概念的单一化。 规范化的方法:将关系模式投影分解成个或个以上的关系模式。 2,依赖和范式 1)依赖 ①...
  • 设计模式笔记---6设计原则

    千次阅读 2016-01-04 15:29:32
    设计模式笔记
  • 分布式系统概念

    万次阅读 多人点赞 2018-11-15 16:25:36
    国内来讲,移动互联网的爆发伴随着分布式系统的突现,移动互联网最大的特点是2(to)c的o2o产品越来越多,这跟传统2B的系统最大区别就是用户量的不同,2C系统的用户量远远要高于2b系统,这就对系统提出了各种各样的高...
  • Java设计模式-里氏替换原则

    千次阅读 2019-03-03 10:41:14
    里氏替换原则【Liskov Substitution Principle】
  • 设计模式概念和七大原则

    千次阅读 2018-07-17 00:17:05
    什么是设计模式 在GoF(Gang of Four)的书籍《Design Patterns - Elements of Reusable Object-Oriented Software(设计模式-可复用面向对象软件的基础)》中是这样定义设计模式的:Christopher Alexander说过:“每一...
  • Java设计模式-接口隔离原则

    千次阅读 2019-03-11 08:40:15
    接口隔离原则 【Interface Segregation Principle】   定义1:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上(Clients should not be forced to depend upon interfaces that ...
  • C#基础教程-c#实例教程,适合初学者

    万次阅读 多人点赞 2016-08-22 11:13:24
    注意,和我们使用过的绝多数编译器不同,在C#中编译器只执行编译这个过程,而在C和C++中要经过编译和链接个阶段。换而言之C#源文件并不被编译为目标文件.obj,而是直接生成可执行文件.exe或动态链接库.dll,C#...
  • 设计模式源码下载地址 设计模式源码下载地址 1 单一功能原则 ...单一功能原则(Single responsibility principle)规定每个类都...围观设计模式(1)--单一功能原则  2 里氏替换原则 在面向对象的程
  • 设计模式7设计原则

    千次阅读 2021-08-25 17:05:39
    1.开闭原则(Open Closed Principle,OCP) 软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification) 合成复用原则、里氏替换原则相辅相成,都是开闭...
  • 1.接口隔离原则:(Interface Segregation Principle, ISP) 定义:Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口)。或  The dependcy of one ...
  • 面向对象六大原则

    万次阅读 多人点赞 2015-11-30 00:10:44
    本文出自《Android源码设计模式解析与实战》中的第一章。 1、优化代码的第一步——单一职责原则单一职责原则的英文名称是...就像秦小波老师在《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你
  • 面向对象设计的七设计原则详解

    万次阅读 多人点赞 2018-10-03 12:32:21
    文章目录面向对象的七设计原则简述七大原则之间的关系一、开闭原则(The Open-Closed Principle ,OCP)概念理解系统设计需要遵循开闭原则的原因开闭原则的实现方法一个符合开闭原则的设计开闭原则的相对性二、 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 126,587
精华内容 50,634
关键字:

to4模式的两大原则