一、设计模式的分类
创建型模式:
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。结构型模式:
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式行为模式:
策略模式、模板方法模式、观察者模式、责任链模式、迭代子模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
二、设计模式六大原则
1.开闭原则:
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。
所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。2.里氏替换原则:
该原则的核心思想就是在程序当中,如果将一个父类对象替换成它的子类对象后,该程序不会发生异常。这也是该原则希望达到的一种理想状态。
通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
里氏代换原则是开闭原则的重要方式之一,由于使用父类对象的地方都可以使用子类对象,因此在程序中尽量使用父类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。3.依赖倒置原则:
高层模块不应该依赖低层模块,二者都应该依赖其抽象
抽象不应该依赖细节,细节应该依赖抽象
依赖倒转(倒置)的中心思想是面向接口编程
依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成4.接口隔离原则:
当一个接口太大时,我们需要将它分割成一些细小的接口,使用该接口的客户端只需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事情,干该干的事请
在使用接口隔离原则时我们要控制接口的颗粒度,颗粒度不能太大,也不能太小。如果太小就会造成接口泛滥,不利于维护;
接口如果太大就会违背接口隔离原则,灵活性较差,使用起来不方便。一般来说接口中仅包含某业务模块的方法即可,不应该有其他业务模块的方法。5.单一职责:
对一个类而言,应该仅有一个引起它变化的原因。 如果存在多于一个动机去改变一个类,那么这个类就具有多于一个的职责,就应该把多余的职责分离出去,再去创建一些类来完成每一个职责。6.迪米特原则(最少知道原则):
一个对象应该对其他对象有最少的了解。通俗来说就是,一个类对自己需要耦合或者调用的类知道的最少,你类内部怎么复杂,我不管,那是你的事,我只知道你有那么多公用的方法,我能调用。
迪米特原则核心观念就是:类间解耦,弱耦合。
从迪米特法则的定义和特点可知,它强调以下两点:
从依赖者的角度来说,只依赖应该依赖的对象。
从被依赖者的角度说,只暴露应该暴露的方法。
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点:
降低了类之间的耦合度,提高了模块的相对独立性。
由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
三、如何理解组合优于继承
假设我们要设计一个关于鸟的类。我们将“鸟”这样一个抽象的事物概念,定义为一个抽象类AbstractBird。所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。
我们知道,大部分鸟都会飞,那我们可不可以在 AbstractBird抽象类中,定义一个fly()方法呢?答案是否定的。尽管大部分鸟都会飞,但也有特例,比如鸵鸟就不会飞。
鸵鸟继承具有fly()方法的父类,那鸵鸟就具有“飞”这样的行为,这显然不对。如果在鸵鸟这个子类中重写fly() 方法,让它抛出UnSupportedMethodException异常呢?
虽然可以解决问题,但不优雅。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,全部都去重写fly()方法,抛出异常,完全属于代码重复。理论上这些不会飞的鸟根本就不应该拥有fly()方法,让不会飞的鸟暴露fly()接口给外部,增加了被误用的概率。
要解决上面的问题,就得让AbstractBird类派生出两个更加细分的抽象类:会飞的鸟类AbstractFlyableBird和不会飞的鸟类AbstractUnFlyableBird,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类
这样一来,继承关系变成了三层。但是如果我们不只关注“鸟会不会飞”,还要继续关注“鸟会不会叫”,将鸟划分得更加细致时呢?两个关注行为自由搭配起来会产生四种情况:会飞会叫、不会飞会叫、会飞不会叫、不会飞不会叫。如果继续沿用刚才的设计思路,继承层次会再次加深。
如果继续增加“鸟会不会下蛋”这样的行为,类的继承层次会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。因此可以看出:继承最大的问题就在于:继承层次过深、继承关系过于复杂时会影响到代码的可读性和可维护性。复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承、组合以及委托三种具体的实现形式。
对于上面提到的继承带来的问题,可以利用组合(composition)、接口、委托(delegation)三个技术手段一块儿来解决。
接口表示具有某种行为特性。针对“会飞”这样一个行为特性,我们可以定义一个Flyable接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义Tweetable接口、EggLayable接口
不过,接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍layEgg()方法,并且实现逻辑几乎是一样的(可能极少场景下会不一样),这就会导致代码重复的问题。那这个问题又该如何解决呢?有以下两种方法。
1.使用委托:
针对三个接口再定义三个实现类,它们分别是:实现了fly()方法的 FlyAbility类、实现了tweet()方法的TweetAbility类、实现了layEgg()方法的 EggLayAbility类。然后,通过组合和委托技术来消除代码重复。
2.使用Java8的接口默认方法
在Java8中,我们可以在接口中写默认实现方法。使用关键字default定义默认接口实现,当然这个默认的方法也可以重写。继承主要有三个作用:表示is-a关系、支持多态特性、代码复用。而这三个作用都可以通过其他技术手段来达成。比如is-a关系,我们可以通过组合和接口的has-a关系来替代;多态特性我们也可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。如何判断该用组合还是继承
尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。从上面的例子来看,继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。