设计模式
一,设计模式概述
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Gang of Four)",而这本书也就被称为"四人组(或 GoF)"书。
在《设计模式》这本书的最大部分是一个目录,该目录列举并描述了 23 种设计模式。
java的设计模式大体上分为三大类:
创建型模式(5种):工厂方法模式,抽象工厂模式,单例模式,建造者模式,原型模式。
结构型模式(7种):适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式。
行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
学习设计模式的好处:
- 使程序设计更加标准化,提高软件开发效率。
- 提升程序员的思维能力,编程能力,设计能力。
- 增强代码的可重用性,可读性,灵活性,可靠性,可维护性等。
总原则:开闭原则
开闭原则:就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。
这样的设计,能够面对需求改变却可以保持相对稳定,从而使系统在第一个版本以后不断推出新的版本;面对需求,对程序的改动是通过增加新的代码进行的,而不是更改现有的代码; 所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等。
1、单一职责原则 定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。 问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。 解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
2、里氏替换原则 里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
3、依赖倒置原则 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。 解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
4、接口隔离原则 这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度
5、迪米特法则(最少知道原则) 高内聚低耦合 就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。 最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。
6、合成复用原则 尽量使用合成/聚合.避免继承.在新类中应该尽量使用关联关系采用现有的对象,使之成为新对象的一部分.达到现有功能复用的目的. 通过合成聚合的原则可以降低类于类之间的依赖关系.被依赖的类的修改对其他类的影响相对小一些. 合成/聚合原则是动态的.可以自由选择使用现有类的那些方法.而继承是静态的,失去了灵活性.如果父类改变有可能会影响子类的修改,同时破坏了父类的封装性,父类将会暴露不相关的方法给子类.
二,单例模式(Singleton Pattern)
类加载顺序:类加载(classLoader)机制一般遵从下面的加载顺序
如果类还没有被加载:
-
先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。
-
执行子类的静态代码块和静态变量初始化。
-
执行父类的实例变量初始化
-
执行父类的构造函数
-
执行子类的实例变量初始化
-
执行子类的构造函数
同时,加载类的过程是线程私有的,别的线程无法进入。
如果类已经被加载:
静态代码块和静态变量不在重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。
**概述:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。
**何时使用:**当系统需要某个类只有一个实例的时候。
2.1 饿汉模式创建单例
**概述:**在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。
public class Singleton{
private static Singleton s = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return s;
}
}
**缺点:**类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,内存就浪费了。
2.2 懒汉模式创建单例
**概述:**在类加载时不初始化,等到第一次被使用时才初始化。
2.2.1 普通懒汉式**(线程不安全,不可用)**
public class Singleton{
private static Singleton singleton;
private Singleton(){};
public static Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
2.2.2 同步方法懒汉式
public class Singleton{
private static Singleton singleton;
private Singleton(){};
//同步方法
public static synchronized Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
这种写法是对getInstance()加了锁的处理,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,为了改进这种写法,就有了下面的双重检查懒汉式
2.2.3 双重检查锁懒汉式
public class Singleton{
private static volatile Singleton singleton;
private Singleton(){};
public static Singleton getInstance(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。
2.4 静态内部类创建单例
public class Singleton{
private Singleton(){}
private static class SingletonHolder{
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。
同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。
2.5 单例模式优点和缺点
**优点:**单例类只有一个实例,节省了内存资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;单例模式可以在系统设置全局的访问点,优化和共享数据,例如前面说的Web应用的页面计数器就可以用单例模式实现计数值的保存。
**缺点:**单例模式一般没有接口,扩展的话除了修改代码基本上没有其他途径。
三,工厂模式
**概述:**工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
需求:生产汽车。
3.1 简单工厂模式
先看一下简单工厂模式,简单工厂模式也是一种工厂方法模式。简单工厂模式又称静态工厂方法模式。从命名上就可以看出这个模式一定很简单。它存在的目的很简单:定义一个用于创建对象的接口。如果一个一些对象(产品),已经确定了并不易改变和添加新的产品,那么久可以使用简单工厂模式。
Car接口:
/**
* 所有车的父类
*/
public interface Car {
void name();
}
宝马:
public class BaoMa implements Car{
@Override
public void name() {
System.out.println("宝马车");
}
}
奔驰:
public class BenChi implements Car{
@Override
public void name() {
System.out.println("奔驰车");
}
}
五菱之光:
public class WuLing implements Car{
@Override
public void name() {
System.out.println("五菱之光");
}
}
CarFactory工厂:
/**
* 简单工厂模式
*/
public class CarFactory {
public static Car getCar(String name){
if(name.equals("宝马")){
return new BaoMa();
}else if(name.equals("奔驰")){
return new BenChi();
}else if(name.equals("五菱")){
return new WuLing();
}
return null;
}
}
测试类:
public class CarTest {
public static void main(String[] args) {
Car baoma = CarFactory.getCar("宝马");
baoma.name();
Car benchi = CarFactory.getCar("奔驰");
benchi.name();
Car wuling = CarFactory.getCar("五菱");
wuling.name();
}
}
3.2 工厂方法模式
工厂方法模式去掉了简单工厂模式中工厂方法的静态属性,使得它可以被子类继承。这样在简单工厂模式里集中在工厂方法上的压力可以由工厂方法模式里不同的工厂子类来分担。
CarFactory接口:
public interface CarFactory {
Car pro();//生产方法
}
每一种车都有属于自己独立的 工厂:要去实现 CarFactory接口:
BaoMaFactory:
/**
* 有一个单独的宝马工厂 专门用来生产宝马车
*/
public class BaoMaFactory implements CarFactory{
@Override
public Car pro() {
return new BaoMa();
}
}
BenChiFactory:
public class BenChiFactory implements CarFactory{
@Override
public Car pro() {
return new BenChi();
}
}
如果想扩展新的车,我们不用改动老代码,直接添加我们需要的工厂以及工厂要生产的类 就可以了。
四,适配器模式
概述:适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s19dLTqE-1616160047630)(assets/UpBUKAzMCccf8miR.png!thumbnail)]
举个栗子1:当我们给手机充电时,由于手机充电口是5V,而插座提供的是220V交流电,因此我们通常需要使用充电器将220V交流电转换成可供手机充电用的5V直流电,这个充电器就是一个适配器。
举个栗子2:我们现在的轻薄本电脑没有提供网线接口,不能直接插网线,需要通过一个网线转接口来连接网线。完成上网的功能。
电脑,网线,网线转接口(适配器)
**作用:**适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。
三种适配器:类的适配器模式、对象的适配器模式、接口的适配器模式。
4.1 类适配器
类适配:创建新类,继承源类,并实现新接口。
接口:AdapterInterface
public interface AdapterInterface {
void charuwangxian();//无线网卡
}
网线 源类:
public class WangXian {
public void conn(){
System.out.println("连接网线上网");
}
}
适配器类:
/**
* 实例化一个适配器对象
* 实现适配器接口
*/
public class AdapterInterfaceImpl extends WangXian implements AdapterInterface{
@Override
public void charuwangxian() {
//继承网线类,所以可以直接使用父类的方法
super.conn();
}
}
客户端 电脑类:
public class Computer {
public static void main(String[] args) {
/*WangXian wangXian = new WangXian();
wangXian.conn();*/
AdapterInterfaceImpl adapterInterface = new AdapterInterfaceImpl();
adapterInterface.charuwangxian();
}
}