• 设计模式【转自JackFrost的博客】


    首先,感谢作者对知识的分享

    使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编制真正工程化,是软件工程的基石脉络,如同大厦的结构一样。

    文章结构:1.单一职责原则(SRP);2.里氏替换原则(LSP);3.依赖倒置原则(DIP);4.接口隔离原则(ISP);5.迪米特原则(LoD);6.开闭原则(OCP)。7.以计算器设计为例子综合展示各大原则以及相关设计模式。

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

    定义:应该有且仅有一个原因引起类的变更。也就是一个接口或类只负责一件事。

    使用原因:假设一个类负责两大职责,那么当业务变化,一个职责需要变化时就要修改这个类,所以就可能导致另一个职责会发生变化。

    优点:1.类的复杂度降低,实现什么职责都又清晰明确的定义; 2.可读性高,复杂性降低;3.可维护性高,易维护;4.变更引起的风险降低。(一个接口修改只对应的实现类有影响,对其他的接口无影响,对系统扩展性、维护性有多大帮助)。

    例子:

    //这样设计的话,有个后果可以看见:如果我要让帅哥类拥有一个跳舞的技能,那么我要修改哪里呢??修改People类?那就全崩了。所以我们应该把帅哥类拆分出来成为另一个类来继承People。
    public class People{
        public void say(String name){
            System.out.println("我是"+name);
        }
    }
    //场景类
    public class Client{
        public static void main(String []args){
            People people=new People();
            people.say("帅哥");
            people.say("美女");
            people.say("大叔");
        }
    }

     

    单一职责原则使用建议:接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

    但是呢,类的设计也要结合实际,很多时候非单一职责的情况的,我们就要去妥协一下子:

    1.只有逻辑足够简单,才可以在代码级别上违背SRP;
    2.只有类中方法数量足够少,才可以在方法级别上违背SRP;
    3.实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是要遵循SRP。

    二、里氏变换原则(Liskov Substitution Principle,LSP原则):

    定义:所有引用基类的地方必须能透明地使用其子类对象。即:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或寻常。(子类可完整实现父类的业务)。

    设计原因:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

    这个定义有四层含义:

     (一)、子类必须完全实现父类的方法:

    也就是在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明违背LSP原则。(在编写类的实现时用父类对象,调用时传入其子类实现向上转型即可完成设计。是传入子类对象,在方法实现向上转型成父类)。

    针对这一含义,实际的开发有些注意点:

    1.业务增多时,类的设计思路:(1)(不推荐)在moudle类中增加instanceof的判断(即每增加所有与这个类的父类有关系的类都要修改)。(2)(推荐,维护人员经常干)新增类脱离继承,简历一个独立的父类,可与原来的那个父类建立关联委托关系。

    2.具体应用场景类的设计关键:子类是否能完整地实现父类的业务。(如果子类不能完整实现父类的方法,或者父类的某些方法在子类中已经发生变化,则应该简历断开父子继承关系,采用依赖、聚集、组合等关系。)

    (二)子类可以有自己的个性:

    也就是子类可以有自己的方法和属性。

    注意:在调用那个类时,不要使用父类对象传递。因为向下转型是不安全的。

    (三)覆盖或实现父类的方法时输入参数可以被放大。

    相关概念:(1)方法重载(Overload)子类拥有父类的所有属性和方法,方法名相同,但输入参数又不相同。要求方法的输入参数类或数量不相同,在LSP原则下,就是子类的输入参数宽于或等于父类的输入参数。(2)方法覆写:父类和子类的同名方法的输入参数是相同的,而且两个方法的范围值为子类的小于父类的。

    注意

    1.子类中方法的前置条件(传递的参数)必须与父类中被覆写的方法的前置条件相同或者更宽松。

    2.如果父类的输入参数类型宽于子类的输入参数类型,那么会出现父类存在的地方,子类就未必可存在。因为一旦把子类作为参数传入,调用者可能进入子类的方法。

    (四)、覆写或实现父类的方法时输出结果可以被缩小:

    含义:父类的一方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么LSP原则要求S必须小于或等于T。也就是说S和T是同一类型或者S是T的子类。

    LSP原则的目的:增强程序的健壮性,使版本升级时,即使增加子类,原有的子类还可以继续运行。

    实际项目做法:项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。可是!!使用LSP原则的时候要尽量避免子类的“个性”,因为LSP原则只可以正着用不能反着用。

    例子:

    public class Father {
        public Collection doSomeing(HashMap map){
            System.out.println("父类被执行。。。");
            return map.values();  
            }
    }
    public class Son extends Father{
        public Collection doSomeing(Map map){
            System.out.println("到子类啦");
            return map.values();
        }
    }
    //场景类一。
    public class Client{
        public static void invoker(){
            Father f=new Father();     //这里就是直接使用父类的对象
            HashMap map=new HashMap();
            f.doSomeing(map);
        }
        public static void main(String []args){
            invoker();
        }
    }
    //跟场景类一结果一样的场景二:
    //分析:为啥会一样结果呢??因为我们设计子类的时候,输入的参数的范围相对于父类扩大了,子类就可以代替父类传递到父类的调用者中,子类的方法就不会被执行啦。但是呢,如果想让子类的方法被执行,就必须覆写父类的方法了。
    public class Client{
        public static void invoker(){
            Son son=new Son();
            HashMap map=new HashMap();
            son.doSomeing(map);
        }
        public static void main(String []args){
            invoker();
        }
    }
    
    

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

    定义:简而言之,就是面向接口编程。(1)模块间的依赖通过抽象发生,实现类之间不发生之间的依赖关系,其依赖关系是通过接口或抽象类产生的;(2)接口或抽象类不依赖于实现类;(3)实现类依赖接口或抽象类。

    设计原因:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

    DIP原则优点:减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

    相关概念: java中一个变量有两种类型:表面类型和实际类型。表面类型是定义的时候赋予的类型,实际类型是对象的类型。

    使用:分析!!分析两个类是否有依赖关系,有则制定出两者之间的接口(或抽象类)即可独立开发。

    依赖的三种写法 :

    (一)、在类中通过构造函数声明依赖对象。(按照依赖注入的说法,叫做构造函数注入)。

    public interface IDriver{
        public void drive();       //是司机就会开车.这是个司机接口
    }
    public class Driver implements IDriver{
        private ICar car;
        public Driver (ICar _car){    //构造函数注入依赖
            this.car=_car;
        }
        //司机主要职责就是开车
        public void drive(){
            this.car.run();  //对象事务一级级地传递,传递到车对象
        }
    }
    
    

    (二)在抽象中设置Setter方法声明依赖关系(依照依赖注入的说法,这叫Setter依赖注入)。

    public interface IDriver{
        public interface IDriver{
            public void setCar(ICar car); //车辆型号
            public void drive();
        }
        public class Driver implements IDriver{
            private ICar car;
            public void setCar(ICar car){   //setter函数注入依赖
                this.car=car;   
            }
            public void driver(){
                this.car.run();
            }
        }
    }

     

    
    

    (三)接口声明依赖对象

    //分析:在IDriver中,通过传入ICar接口实现**抽象之间**的依赖关系。Driver实现类也传入了ICar接口,而是什么类型Car就需要在高层声明了。
    public interface ICar {
        //是汽车就会跑
        public void run();
    }
    public interface IDriver{
        //是司机就会开车
        public void drive(ICar car);   //在接口就注入依赖,注入的是接口之间的依赖
    }
    publi class Driver implements IDriver{
    //司机主要职责就是开车
        public void drive(ICar car){
            car.run();
        }
    }
    
    

    使用DIP原则的规则以及注意点

    (一)、每个类尽量都又接口或抽象类,或者两者都具备。

    (依赖倒置的基本要求,有了抽象才可能依赖倒置)

    (二)变量的表面类型尽量是接口或者抽象类。

    (但不是所有类型都要抽象,比如一个工具类一般不需接口或抽象类)

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

    (比如:项目处于开发状态,不应从具体类派生出子类。但有维护的口占情况,这时则可以不考虑,但要求不超过两层继承)。

    (四)尽量不要覆写基类的方法

    (如果基类是一个抽象类,而且方法已经实现,子类应尽量不要覆写。类间依赖的是抽象,覆写抽象方法对依赖的稳定性有影响。)

    (五)结合里氏替换原则使用

    (原则:接口负责定义public属性和方法,并且声明与其他对象的的依赖关系,抽象类负责公共构造部分的实现,是实现类准确地实现业务逻辑,同时在适当的时候对父类进行细化)。

    DIP原则是6个设计原则中最难实现的原则,是实现开闭原则的重要途径。


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

    定义:客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。(一句话:接口尽量细化,同时接口中的方法尽量少。)

    相关定义:接口分类:(一)实例接口。在Java中声明一个类,然后new产生一个实例,它是对一个类型的事物的描述(一个角度:Java中的类也是一种接口)。(二)类接口。Java中经常使用的interface关键字定义的接口。

    做法:把大接口分解为几个小接口。(例子:美女的接口分解为外貌美女、气质型美女)

    注意:单一职责要求的是类和接口职责单一,注重的是职责!!!这属于业务逻辑的划分。

    接口隔离原则是对接口进行规范约束:(四层含义)开发注意点

    (1)接口要尽量小。

    根据接口隔离原则拆分接口时,首先必须满足单一职责原则!

    (2)接口要高内聚。

    高内聚:提高接口、类、模块的处理能力,减少对外的交互。要求接口–尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利。

    (3)定制服务:

    原因:模块之间必然会有耦合,有耦合就要有相互访问的接口(可能是Java中的interface,也可能为一个类或单纯数据交换)设计时需要为各个访问者(客户端)定制服务。
    定义:就是单独为一个getItem提供优良的服务
    要求做法:只提供访问者需要的方法。

    项目开发时接口设计规则:

    (一)一个接口只服务于一个子模块或业务逻辑

    (二)通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉“,而不是肥嘟嘟的一堆方法。

    (三)已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。

    (四)了解环境,不要盲从。每个项目或产品都有特定的环境因素,别看到大师做接口就做接口。环境不同,接口拆分的标准不同,有一点点的妥协原则的。

    例子:

    //外貌型美女
    public interface GoodBodyGirl{
        //美丽的外貌
        public void goodLooking{
        }
        //好身材
        public void niceFigure{
        }
    }
    //气质型美女
    public interface GreatTemperametGirl{
        //气质
        public void greatTemperament();
    }
    
    

    (五)迪米特原则(Law Of Demeter,LOD原则):

    定义:一个对象应该只对其他对象有最少的了解。(也就是说你的类多复杂都不关我的事,我只关心你的public事物)

    设计原因:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

    含义有四层:

    (一)只和朋友交流:类与类的联系不要跨越。(也就是我想叫我的朋友的朋友去干活,就必须通过我的朋友去告诉我的朋友的朋友)

    类与类之间的关系是建立在类间的,而不是方法之间,因此一个方法尽量不引入一个类中不存在的对象。

    (二)朋友之间也是有距离的:一个雷公开的public属性或方法越多,修改时设计的面也就越大,变更引起的风险扩散也越大。

    注意:尽量不公布太多public方法和非静态的public变量,多使用private–>类的高内聚。

    技巧:一个类暴露过多方法,而发现这堆方法有关系时,可以设计一个public方法,把那堆方法统率在新的public方法,并设置为private。

    (三)是自己的就是自己的:

    方法设计原则:如果一个方法放在本类中,即不增加类间的关系,也对本类不产生负面影响,就可以放置在本类中。

    (四)谨慎使用Serializable(序列化机制,具体看我另一篇博客输入输出机制)。

    例子:一个项目使用RMI(远程方法调用)方式传递VO值对象,其实就是个网络请求过程,那么这个对象就必须实现Serializable接口。(也就是把需要网络传输的对象进行序列化,否则会出现Not Serializable Exception)。

    核心观念:类间解耦,实现弱耦合。

    可能导致的后果:然而其结果会产生大量中转类或跳转类,导致系统的复杂性提高,为维护带来难度。所以使用LOD原则时要反复权衡。跳转不超过两次。

    例子:

    //场景是:老师叫班长清点女生人数,班长就去清点了。
    //分析:所以这个场景,根据LOD原则,老师是不能调用女生的类的。然后我们知道只能够让班长去调用女生。而老师去调用班长。
    public class Teacher{
        public void commond(GroupLeader groupleader){
            groupleader.countGirls();
        }
    }
    public class Girl{
    
    }
    public class GroupLeader{
        private List<Girl> listGirls;
        //传递全班的女生进来
        public GroupLeader(List<Girl> _listGirls){
            this.listGirls = _listGirls;
        }
        //清查女生数量
        public void countGirls(){
            System.out.println("女生数量是:"+this.listGirls.size());
        }
    }
    
    

    六、开闭原则:

    定义:一个软件实体应通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

    设计原因:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

    相关概念:

    软件实体:(1)项目或软件产品中按照一定逻辑规则划分的模块。(2)抽象和类。(3)方法。

    业务变化:(三种类型)

    (1)逻辑变化。

    只变化一个逻辑而不涉及其他模块,可通过修改原有类中的方法的方式来完成。但是有个前提条件:所有依赖或关联类都按照相同逻辑处理。

    (2)子模块变化:

    一个模块变化,必会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化。

    (3)可见视图变化:

    可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应。因为多了一项显示数据,意味着你的其他部分代码也要增多一项这样的数据。

    针对软件产品的需求业务变化:有三大扩展方案

    (一)修改接口,增加方法:(否定)

    后果:所有拥有该接口的实现类必须作修改,场景类也要修改。

    否定这个方案的原因:接口应该是稳定且可靠的,不应经常发生变化,否则接口作为契约的作用就失去了。

    (二)修改实现类:(不推荐)

    做法:通过class文件替换的方式完成业务变化。

    后果:修改了的信息可能会导致其他类获得的信息不对称。

    (三)通过扩展实现变化:(推荐)

    做法:增加子类,覆写方法,高层次的模块(static静态模块区)通过新增子类产生新的对象。

    代码例子:

    public interface Book{
        //书籍有名称
        public String getName();
        //书籍有售价
        public int getPrice();
        //书籍有作者
        public String getAuthor();
    }
    public class NovelBook implements Book{
        //书籍名称
        private String name;
        //书籍的价格
        private int price;
        //书籍的作者
        private String author;
        //通过构造函数传递书籍数据
        public NovelBook(String _name,int _price,String _author){
            this.name = _name;
            this.price = _price;
            this.author = author;
        }
        public String getAuthor(){
            return this.author;
        }
        public String getName(){
            return this.name;
        }
        public int getPrice(){
            return this.price;
        }
    }
    //然后到了阅读月啦,我们书店就要打折啦!!怎么办??正如我们刚刚提到的三种方案。
    //分析:如果选择第一种方案,在接口里面新增方法,那么我们的小说类NovelBook也要修改,而且违背接口的稳定性原则。如果选择第二种方案,直接修改实现类,也就是修改NovelBook的类啦,是的,我们可以做一个打折的方法,然后完成打折的业务扩展,但是!!其他业务就有可能有影响,比如设计模式里面说的采购人员也看价格,而当这个方法实现了,采购人员只会看到打折后的价格。
    //所以我们采用第三种方案。来完成业务扩展。
    public class OffNovelBook extends NovelBook{
        public offNovelBook(String _name,int _price,String _author){
            super(_name,_price,_author);
        }
        @Override
        public int getPrice(){
            //原价
            int selfPrice = super.getPrice();
            int offPrice = 0;
            if(selfPrice>4000){
                offPrice = selfPrice * 9/10;
            }else{
                offPrice = self * 8/10;
            }
            return offPrice;
        }
    }
    
    

    开闭原则是最基础的一个原则,也就是说前五个原则是指导设计的工具和方法,而开闭原则才是精神领袖。可以说,只要遵守前五大原则,设计出来的软件就会符合开闭原则。

    思想核心:用抽象构建框架,用实现扩展细节。

    联系其他原则:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。


    七、计算器(结合了六大设计原则与工厂模式)

    //这个就是运算类的工厂模式。因为运算太多了,如果每个都在运算类里面再写个几行,那样会导致运算类的繁重累赘而且不清晰。以多态特性来设计。
    public class OperationFactory{
        public static Operation createOperate(String operate){
            Operation oper = null;
            swich(operate){
                case "+":
                    oper = new OperationAdd();
                    break;
                case "-":
                    oper = new OperationSub();
                    break;
                case "*":
                    oper = new OperationMul();
                    break;
                case "/":
                    oper = new OperationDiv();
                    break;
            }
            return oper;
        }
    }
    //而这个运算类,把他给抽象处理就体现出 用抽象构建架构,用实现扩展细节了。
    //分析:我们把运算类的核心抽取出来:两个数,一个结果,还需一个运算规则。这几个就是本质!而运算规则有很多种,我们需要交给子类去实现
    //根据 单一职责原则--我们把每个运算规则的类分开成了单独的类。加减乘除均为一个类。
    //根据 里氏替换原则--加减乘除的四个类均继承一个抽象出来的运算父类,获得一样的特性。两个数,一个结果。
    //根据 依赖导致原则--面向接口编程,也就是面对抽象的结果编程。而我们抽象出的运算父类,拥有各个运算规则子类的共同特征。
    //根据 接口隔离原则--呃,,这里只作出了一步抽象,没有多余的接口。
    //根据 迪米特原则--松耦合。当然啦,都把加减乘除分离开来成一个类了,以后你想要修改加法,比如根据职位不同去加不同的薪水,这样更改,你只需改加法这个类。
    //根据 开闭原则。核心--用抽象构建框架,用实现扩展细节。以上原则都实现了,这不就好了咩。
    public abstract class Operation{
        private double numberA = 0;
        private double numberB = 0;
        private double result = 0;
        public void setNumberA(Double _numberA){
            this.numberA = _numberA;
        }
        public double getNumberA(){
            return numberA;
        }
        public void setNumberB(Double _numberB){
            this.numberB = _numberB;
        }
        public double getNumberB(){
            return numberB;
        }
        public abstract double getResult();
    }
    //加法类
    class OperationAdd extends Operation {
        public double getResult(){
            result = numberA + numberB;
            return result;
        }
    }
    //减法类
    class OperationSub extends Operation {
        public double getResult(){
            result = numberA - numberB;
            return result;
        }
    }
    //乘法类
    class OperationMul extends Operation {
        public double getResult(){
            result = numberA - numberB;
            return result;
        }
    }
    //除法类
    class OperationDiv extends Operation {
        public double getResult(){
            try{
                result = numberA / numberB;
            }catch(Exception e){
                System.out.println("除数不能为0");
                e.printStackTrace();
            }
            return result;
        }
    }
    //场景类代码:
    Operation oper;
    
    oper = OperationFactory.createOperate(“+”);
    
    oper.setNumberA(1.0);
    
    oper.SetNumberB(2.0);
    
    double result = oper.GetResult();

     


    好了,设计模式-六大设计原则(附加实际开发建议以及计算器例子)讲完了。本博客借鉴了一部分设计模式之禅,这是一本很好的书,只是我在这里总结以及写出自己的理解。欢迎在下面指出错误,共同学习!

    转载自:【JackFrost的博客】

  • 相关阅读:
    Linux C 字符串函数 sprintf()、snprintf() 详解
    Linux C 字符串输入函数 gets()、fgets()、scanf() 详解
    Linux C 字符串函数 strlen()、strcat()、strncat()、strcmp()、strncmp()、strcpy()、strncpy() 详解
    Linux C 字符函数 getchar()、putchar() 与 EOF 详解
    Linux C popen()函数详解
    【重装系统】线上Linux服务器(2TB)分区参考方案
    Ubuntu 开机进入命令行模式
    oracle视图总结(创建、查询、改动、删除等)
    UVa 637
    Intent 的Flag属性(Activity在栈位置的主宰者)
  • 原文地址:https://www.cnblogs.com/dayang12525/p/6294051.html
Copyright © 2020-2023  润新知