Java笔记-----(15)设计模式
- (1)设计模式的六大原则
- (1.1)合成复用原则(Composite Reuse Principle,CRP)
- (1.2)单一职责原则(Single responsibility principle,SRP)
- (1.3)里氏替换原则(Liskov Substitution Principle,LSP)
- (1.4)依赖倒置原则(Liskov Substitution Principle,LSP)
- (1.5)接口隔离原则(Interface Segregation Principle, ISP)
- (1.6)迪米特法则(Law of Demeter,LoD)
- (1.7)开闭原则(Open Close Principle,OCP)
- (1.8)总结
- (2)单例模式
- (3)工厂方法模式
- (4)模板方法模式
- (5)抽象工厂模式
- (6)代理模式
- 策略模式
- 责任链模式
- 观察者模式
- 装饰模式
- 适配器模式
- 建造者模式
设计模式是一种思想,并不是一门具体的技术。设计模式就是在软件开发过程中所总结形成的一系列准则。当遇到一些场景的时候,使用恰当的设计模式可以使得软件设计更加健壮,增强程序的扩展性。
设计模式的两大主题是:系统复用和系统拓展。 (填空题)
(1)设计模式的六大原则
也有七大原则的:
(1.1)合成复用原则(Composite Reuse Principle,CRP)
在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
参考文章:
设计模式的七大原则
(1.2)单一职责原则(Single responsibility principle,SRP)
单一职责规定了一个类应该只有一个发生变化的原因。如果一个类承担了多个职责,则会导致多个职责耦合在一起。但部分职责发生变化的时候,可能会导致其余职责跟着受到影响,也就是说程序耦合性太强,不利于变化。
单一职责的优点:
- 降低了类的复杂度,每一个类都有清晰明确的职责。
- 程序的可读性和可维护性都得到了提高。
- 降低业务逻辑变化导致的风险,一个接口的修改只对相应的实现类有影响,对其他接口无影响。
(1.3)里氏替换原则(Liskov Substitution Principle,LSP)
里氏替换是指所有父类可以出现的地方,子类就都可以出现,使用子类来替换父类,调用方不需要关心目前传递的父类还是子类。
通过继承,实现了代码的共享和复用。但是继承是强侵入性的,子类必须拥有父类所有的非私有属性和方法,在一定程度上降低了子类的灵活性。
通过里氏替换原则,可以将子类对象做为父类对象来使用,屏蔽了不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。里氏替换之后,父类的对象就可以根据当前赋值给它的子类对象的特性以不同的方式运作。
public class LiSiTest {
public static void main(String[] args) {
// 通过传入子类对象来替换父类
eat(new Dog());
eat(new Cat());
}
private static void eat(Animals animal) {
animal.eat();
}
}
abstract class Animals {
abstract void eat();
}
class Dog extends Animals {
@Override
void eat() {
System.out.println("我是小狗,喜欢吃大肉");
}
}
class Cat extends Animals {
@Override
void eat() {
System.out.println("我是小猫,喜欢吃小鱼干");
}
}
里氏替换原则的优点:
里氏替换原则可以增强程序的健壮性,子类可以任意增加和缩减,都不需要修改接口参数。在实际开发中,实现了传递不同的子类来完成不同的业务逻辑。
(1.4)依赖倒置原则(Liskov Substitution Principle,LSP)
依赖倒置原则是指高层模块不应该依赖于底层模块,抽象不应该依赖细节,细节应该依赖抽象。在Java中,接口和抽象类都是抽象,而其实现类就是细节。也就是说,应该做到面向接口编程,而非面向实现编程。
案例,饲养员喂养:
开始的时候,饲养员只需要喂养小狗即可。代码实现如下:
public class DIPTest {
public static void main(String[] args) {
Dog dog = new Dog();
Feeder feeder = new Feeder();
// 饲养员喂食小狗
feeder.feed(dog);
}
}
// 定义小狗类
class Dog{
public void eat(){
System.out.println("小狗在吃东西,,,,,,");
}
}
// 定义饲养员类
class Feeder{
public void feed(Dog dog){
dog.eat();
}
}
这是一个面向实现编程的案例,可以看到,上层模块依赖了下层模块,并没有做到面向接口编程。当饲养员想要喂食其余动物的时候,发现其并不具备相应的能力,因为feed方法中的参数是一个具体的实现,是一个细节。
然后,再来看下,如何做到依赖倒置,实现面向接口编程。
代码实现如下:
public class DIPTest {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Feeder1 zhangsan = new Feeder1();
// 饲养员zhangsan喂食小狗,小狗
zhangsan.feed(dog);
zhangsan.feed(cat);
// ---------------------
// 接下来是另一位饲养员lisi,它们的工作方式不同
Feeder2 lisi = new Feeder2();
// 饲养员zhangsan喂食小狗,小狗
lisi.feed(dog);
lisi.feed(cat);
}
}
// 定义一个抽象
interface IAnimal{
void eat();
}
// 定义实现,小狗类
class Dog implements IAnimal{
public void eat(){
System.out.println("小狗在吃东西,,,,,,");
}
}
// 定义实现,小猫类
class Cat implements IAnimal{
public void eat(){
System.out.println("小猫咪在吃东西,,,,,,");
}
}
// 定义饲养员接口
interface IFeeder{
public void feed(IAnimal animal);
}
// 定义1号饲养员
class Feeder1 implements IFeeder{
@Override
public void feed(IAnimal animal) {
animal.eat();
}
}
// 定义2号饲养员
class Feeder2 implements IFeeder{
@Override
public void feed(IAnimal animal) {
this.eat();
animal.eat();
}
public void eat(){
System.out.println("工作之前,我的先填饱肚子");
}
}
面向接口编程之后,可以定义多个拥有不同工作方式的饲养员,并且每个饲养员都可以喂养多个动物,只要该动物实现IAnimal接口即可。
依赖导致原则的好处:
- 依赖倒置通过抽象(接口或抽象类)使各个类或模块的独立,实现模块间的松耦合。
- 面向接口编程可以使得当需求变化的时候,程序改动的工作量不至于太大。
(1.5)接口隔离原则(Interface Segregation Principle, ISP)
接口隔离原则是指客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
接口隔离原则的使用原则:
- 根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
- 接口需要高内聚,提高接口,类和模块的处理能力,减少对外的交互。
- 定制服务,单独为一个个体提供优良服务(只提供访问者需要的方法)。
- 接口设计要有限度,接口设计的太小,容易造成开发难度增加或者可维护性降低。
(1.6)迪米特法则(Law of Demeter,LoD)
迪米特法则也叫最少知识原则,是指一个对象应该对其依赖的对象有最少的了解。该类不需要知道其依赖类的具体实现,只需要依赖类给其提供一个公开对外的public方法即可,其余一概不需要了解。
迪米特法则的核心就是解耦合,减弱类间的各个耦合,提高类的复用率。
(1.7)开闭原则(Open Close Principle,OCP)
开闭原则是指一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。也就是说,通过开闭原则,可以通过扩展行为来实现新的功能,而不是通过修改已有的代码。开闭原则可以帮助构建一个稳定,灵活的软件系统。
(1.8)总结
六大设计原则是在进行程序设计和软件开发过程中应该去重点参考和遵循的准则。但是,鉴于复杂的业务逻辑场景以及多变的业务需求,往往不可能做到遵循全部的准则。所以,六大设计原则只是一个参考,具体设计中需要灵活使用各个原则,争取设计出优雅的软件架构。
- 合成复用原则:在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
- 单一职责原则:类或者接口要实现职责单一
- 里氏替换原则:使用子类来替换父类,做出通用的编程
- 依赖倒置原则:面向接口编程
- 接口隔离原则:接口的设计需要精简单一
- 迪米特法则:降低依赖之间耦合
- 开闭原则:对扩展开放,对修改关闭
(2)单例模式
参考:单例模式|菜鸟教程
单例模式是指在一个系统中,一个类有且只有一个对象实例。
单例模式的实现:单例模式从创建方式上又分为饿汉式和懒汉式两种。
单例模式的优点:单例模式保证了一个类在一个系统中有且只有一个对象实例,减少了系统内存和性能的开销。
单例模式的使用场景:创建一个对象需要消耗太多的资源或者在一个系统中不适合创建多个对象实例的情况下,可以采用单例模式设计实现。
(2.1)饿汉式的单例模式
实现如下:
class Single{
private static final Single s = new Single();
private Single(){} //构造函数私有化
public static Single getInstance(){
return s;
}
}
饿汉式的单例模式在程序初始化的时候即创建了对象,在需要的时候可以直接返回该对象实例。
(2.2)懒汉式的单例模式
实现如下:
class Single{
private static Single s = null;
private Single(){} //构造函数私有化
public static Single getInstance(){
if(null==s)
s = new Single();
return s;
}
}
懒汉式的单例模式是在真正需要使用到的时候,才会去创建实例对象。
上边实现的是最简单的单例模式,只适合于单线程环境下使用。因为多个线程并发环境下,还是会创建多个实例对象。
(2.3)多线程环境下的懒汉式单例模式(DCL,双检锁实现)
通过加入synchronized内部锁
来解决多线程环境下的线程安全问题,代码实现如下所示:
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(null==s){
synchronized(Single.class){
if(null==s)
s = new Single();
}
}
return s;
}
}
一般情况,上边加入synchronized的单例模式实现已经是比较准确的了,但是还可能发生重排序问题。
重排序:
Instance instance = new Instance()
都发生了啥?
具体步骤如下三步所示:
- 在堆内存上分配对象的内存空间
- 在堆内存上初始化对象
- 设置instance指向刚分配的内存地址
第二步和第三步可能会发生重排序,导致引用型变量指向了一个不为null但是也不完整的对象。所以,在多线程下上述的代码会返回一个不完整的对象。这时需要加入一个volatile关键字
来禁止指令重排序。
(2.4)多线程环境下的懒汉式单例模式(DCL,double-checked locking双检锁/双重校验锁+volatile实现)
采用双锁机制,安全且在多线程情况下能保持高性能。
实现代码如下:
class Single{
private static volatile Single s = null; //禁止重排序
private Single(){}
public static Single getInstance(){
if(null==s){
synchronized(Single.class){
if(null==s)
s = new Single();
}
}
return s;
}
}
(3)工厂方法模式
参考:
工厂模式|菜鸟教程
工厂方法模式是一种常见的设计模式。工厂方法模式定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法模式使一个类的实例化延迟到其子类。
工厂方法模式的类图:
其中,Product和ConcreteProduct分别表示抽象产品类和具体产品类。Creator和ConcreteCreator则分别表示抽象创建类和具体创建类。抽象创建类Creator中定义了创建产品的方法createProduct。
一个简单的工厂方法模式的Demo:
public class FactoryMethodTest {
public static void main(String[] args) {
// 创建具体的创建类对象
Creator creator = new ConcreteCreator();
// 通过传入指定的产品类对象,来创建对应的产品
Product product = creator.createProduct(ConcreteProduct1.class);
// 创建对象之后,可以进行业务逻辑处理
product.method1();
product.method2();
}
}
// 定义抽象产品类
abstract class Product {
// 产品类的公共方法
public void method1(){
// 公共的业务逻辑
}
// 抽象方法
public abstract void method2();
}
// 定义具体产品类
class ConcreteProduct1 extends Product {
public void method2() {
// 具体产品类1的业务逻辑处理
}
}
class ConcreteProduct2 extends Product {
public void method2() {
// 具体产品类2的业务逻辑处理
}
}
// 定义抽象创建类
abstract class Creator {
// 创建对象的抽象方法
public abstract <T extends Product> T createProduct(Class<T> c);
}
// 定义具体的创建类,真正来创建所需的对象
class ConcreteCreator extends Creator {
public <T extends Product> T createProduct(Class<T> c){
Product product=null;
try {
// 通过反射技术来创建对象
product = (Product)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
//异常处理
}
return (T)product;
}
}
工厂方法模式的优点:
- 工厂方法模式具有很好的封装性。客户端不需要知道创建对象的过程,只需要知道要创建的是哪个具体的产品即可。
- 工厂方法模式对扩展开放。当新增一个产品种类的时候,只需要传入新增产品类对象给具体工厂,即可返回新增的产品对象。
工厂方法模式的使用场景:
- 工厂方法模式的作用就是创建指定的对象,可以作为new一个对象的替代方式。但是需要考虑是否有必要使用工厂方法模式来创建对象。
- 当需要灵活,可扩展的创建多个对象的场景时,可以使用工厂方法模式。
工厂方法模式总结:
工厂方法模式的本质就是指定一个要创建对象的类,然后传给具体的工厂类,由具体工厂类通过反射技术来创建并且返回一个对象。工厂方法模式可以扩展成为简单工厂模式和多工厂模式等。
(4)模板方法模式
模板方法模式也是一个常见的模式。模板方法模式定义了一个框架,将一些步骤延迟到其子类中实现,子类可以在不改变框架的前提下重新定义某些特定的执行步骤。
模板方法模式的类图:
AbstractClass是一个抽象模板,它的方法分为模板方法和基本方法。
- 基本方法:是抽象方法,由子类实现,并且在模板方法中被调用。
- 模板方法:可以有一个或者几个,一般是具体的方法,实现对基本方法的调度,完成确定的业务逻辑,确定一个框架。
ConcreteClass1和ConcreteClass2属于具体模板类,实现抽象模板所定义的抽象方法,并且拥有父类模板中的模板方法。
模板方法模式的Demo:
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.start();
Cat cat = new Cat();
cat.start();
}
}
// 抽象的父类
abstract class Animal{
// 定义抽象方法
abstract void eat();
abstract void run();
// 定义具体的模板方法
public void start(){
// 定义一个框架,确定方法的执行步骤
eat();
run();
}
}
// 定义子类
class Dog extends Animal{
@Override
void eat() {
System.out.println("我是小狗,我在吃东西...");
}
@Override
void run() {
System.out.println("我是小狗,我在跑步...");
}
}
class Cat extends Animal{
@Override
void eat() {
washHand();
System.out.println("我是小猫,我在吃东西...");
}
@Override
void run() {
System.out.println("我是小猫,我在跑步...");
}
// 其余业务逻辑
private void washHand(){
System.out.println("我是小猫,我在洗手...");
}
}
由案例可以看出,在不改变eat和run方法的执行顺序的前提下,Cat类通过在实现eat方法的加入其他的业务逻辑,来改变了eat这个特定的步骤。
模板方法模式的优点:
- 封装不变部分,扩展可变部分
- 提取公共部分代码,便于维护
- 行为由父类控制,子类实现
模板模式总结:
模板方法模式就是在模板方法中调用基本方法来确定整个算法的执行框架。
(5)抽象工厂模式
抽象工厂模式为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。
抽象工厂模式和工厂方法模式的区别:
如果产品单一,适合使用工厂模式。但是如果有多个业务品种、业务分类时,需要使用抽象工厂模式。也就是说,工厂模式针对的是一个产品等级结构 ,抽象工厂模式针对的是面向多个产品等级结构的。
(6)代理模式
代理模式也是常用的一种设计模式,代理模式为其他对象提供一种代理以控制对这个对象的访问。将原类进行封装,客户端不能直接找到原类,必须通过代理角色。即代理是原类的一个替身,客户端要找原类,必须找代理才可以搞定。明星和经纪人的关系就是一种代理模式。
代理模式又分为静态代理和动态代理。动态代理在实现阶段不用关心代理谁,而在运行阶段才指定代理哪一个对象。相对来说,自己写代理类的方式就是静态代理。
静态代理:
- 字节码一上来就创建好,并完成加载
- 装饰者模式就是静态代理的一种体现
动态代理:
- 字节码随用随创建,随用随加载
- 不修改源码的基础上对方法增强