设计模式的目的
在进行软件开发时,为了让程序的实现**高内聚,低耦合,以及提高程序的维护性、扩展性、重用性、灵活性等**。
设计模式的本质就是**提高软件的维护性、通用性和扩展性,并降低软件的复杂度**。
核心思想:
- 找出应用中可能需要变化之处,把他们独立出来,不要和那些不会变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 为了交互对象之间的松耦合设计而努力
设计模式七大原则
单一职责原则
单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因。
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的功能。这种耦合关系会让系统变得很脆弱,当系统发生变化时,设计可能会遭到意想不到的破坏。
软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。
如果你能够想到多以一个的动机去改变一个类,那么这个类就具有多于一个的职责。
对类来说的,即一个类应该只负责一项职责。
- 降低类的复杂度,一个类只负责一项职责
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 通常情况下,我们应该遵守单一职责原则,只有逻辑足够简单,才可以在代码级别违反单一职责原则;只有类中方法数量足够少,可以==在方法级别==保持单一职责原则。
接口隔离原则
客户端不应该依赖它不需要得接口,即一个类对另一个类的依赖应该建立在最小的接口上。
需求:
A通过Interface1依赖类B,但是只需要使用到1,2,3方法
C通过Interface1依赖类D,但是只需要使用到1,4,5方法
将Interface1进行==拆分成几个独立的接口==,A和C分别于与他们需要的接口建立关系。
依赖倒转原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象
抽象不应该依赖细节,细节应该依赖抽象
依赖倒转的中心思想是面向接口编程,不要面对实现编程
相对于细节的多变性,抽象的东西要更加稳定。以抽象为基础的架构要比以细节为基础的架构更加稳定。在Java中,抽象指的是接口和抽象类,细节就是具体的实现类。
使用接口和抽象类的目的就是制定好规范,而且不涉及任何具体的操作,把展现细节的任务交给他们的实现类。
代码实现:
interface IReceiver {
String getInfo();
}
class Email implements IReceiver {
public String getInfo() {
return "Email";
}
}
class Wechat implements IReceiver {
public String getInfo() {
return "Wechat";
}
}
public class Person {
public void receiver(IReceiver receiver) {
System.out.println(receiver.getInfo());
}
}
依赖关系传递的三种方式:
-
接口传递
interface IOpenAndClose { void open(ITV tv); } interface ITV { void play(); } class ChangHong implements ITV { void play() { System.out.println("ChangHong"); } } public class OpenAndClose implements IOpenAndClose { public void open(ITV tv) { tv.play(); } }
-
构造方法传递
interface IOpenAndClose { void open(); } interface ITV { void play(); } class ChangHong implements ITV { void play() { System.out.println("ChangHong"); } } public class OpenAndClose implements IOpenAndClose { public ITV tv; public OpenAndClose(ITV tv) { this.tv = tv; } // 这种方式在open方法中不需要传入tv,tv设置成了类的属性 public void open() { this.tv.play(); } }
-
setter方法传递
interface IOpenAndClose { void open(); // 接口中多了一个setter方法 void setter(ITV tv); } interface ITV { void play(); } class ChangHong implements ITV { void play() { System.out.println("ChangHong"); } } public class OpenAndClose implements IOpenAndClose { public ITV tv; public void setter(ITV tv) { this.tv = tv; } public void open() { this.tv.play(); } }
注意事项:
- 底层模块尽量要有接口或者抽象类,或者两者都有,程序稳定性更好
- 变量的声明类型尽量要是接口或者抽象类,这样我们在变量引用和实际对象间,就会存在一个缓冲层,有利于程序的扩展和优化。
- 继承时应该遵守里氏替换原则。
里氏替换原则
继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承时会给程序带来入侵性,程序的可移植性降低了,增加了对象之间的耦合性。在实际编程中,我们常常会通过重写父类的方法来实现新的功能,这样虽然写起来简单,但是会使得整个继承体系的复用性非常差,特别时是在运行多态比较频繁的时候。为了避免这些情况,在使用继承时,应该尽量遵守**里氏替换原则**。
里氏替换原则:
- 如果对于每个类型为T1的对象o1,都有类型为T2的对象o2,使得T1定义的所有程序P,在所有对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2就是类型T1的子类。换句话说,所有引用基类的地方必须能透明的使用其子类的对象。
- 在使用继承时,遵循里氏替换原则,在**子类中尽量不要重写父类的方法。**
- 继承实际上让两个类的耦合增强了,如果子类需要修改父类的方法,在适当情况下,可以通过聚合、组合、依赖来解决问题。
解决方法:
将原来的父类和子类继承一个更加基础的基类,原有的基础关系去掉,采用依赖、聚合、组合等关系来代替。
如果B类需要用到A类中的方法,不必继承A类,只需要通过组合关系,在B类中将A类作为一个属性,然后调用A类的方法。
开闭原则
- 开闭原则是编程中**最基础、最重要**的设计原则。
- 模块和函数应该**对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节**。
- 当软件需求发生变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
- 编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则。
以下代码在新增时,会需要修改GraphicEditor中的drawShape方法,并且需要在GraphicEditor添加新的方法。
public class GraphicEditor {
public void drawShape(Shape s) {
if (s.m_type == 1) {
drawRectangle(s);
} else if (s.m_type == 2) {
drawCircle(s);
}
}
public void drawRectangle(Shape s) {
System.out.printle("Rectangle");
}
public void drawCircle(Shape s) {
System.out.printle("Circle");
}
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
下面的代码在需要新增功能时,只需要继承相关的抽象类,实现(注意是实现,不是重写)相关的方法即可,不需要修改抽象类。能够实现使用方不需要修改原本的代码,提供方实现扩展即可。
public class GraphicEditor {
public void drawShape(Shape s) {
s.draw();
}
}
abstract class Shape {
int m_type;
// 绘图抽象方法
public abstract void draw();
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
@Override
public void draw() {
System.out.println("Rectangle");
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
@Override
public void draw() {
System.out.printle("Circle");
}
}
迪米特法则
迪米特法则的核心是**降低类之间的耦合**。
由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类之间(对象之间)的耦合关系,并不是完全没有依赖。
基本介绍:
- 一个对象应该对其他对象保持**最少的了解**。
- 迪米特法则又叫**最少知道原则**,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管有多复杂,都尽量将逻辑封装在类的内部。对外除了提供一个public方法,不再透露任何信息。
- 迪米特法则还有一个更简单的定义:只与直接朋友进行通信。
- 直接朋友:每个类都会与其他对象有耦合关系,只要两个对象之间有耦合关系,就被称为朋友关系。耦合的方式有很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值的类为直接朋友,而出现局部变量的类不是直接朋友。也就是说,陌生类最好不要以局部变量的形式出现在类的内部。
class Emp {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeEmp {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeManager {
public List<CollegeEmp> getAllEmp() {
List<CollegeEmp> list = new List<CollegeEmp>();
for (int i = 0; i < 10; i++) {
CollegeEmp emp = new CollegeEmp();
emp.setId(i);
list.add(emp);
}
return list;
}
}
// Emp和CollegeManager是直接朋友
// CollegeEmp不是直接朋友,违反了迪米特法则
class SchoolManager {
public List<Emp> getAllEmp() {
List<Emp> list = new List<Emp>();
for (int i = 0; i < 10; i++) {
Emp emp = new Emp();
emp.setId(i);
list.add(emp);
}
return list;
}
public void printAllEmp(CollegeManager sub) {
List<CollegeEmp> list1 = sub.getAllEmp();
System.out.println("-----分公司员工-----");
for (CollegeEmp e : list1) {
System.out.println(e.getId());
}
List<Emp> list2 = this.getAllEmp();
System.out.println("-----分公司员工-----");
for (Emp e : list2) {
System.out.println(e.getId());
}
}
}
public static vooid main(String[] args) {
SchoolManager schoolManager = new SchoolManager();
schoolManager.printAllEmp(new CollegeManager());
}
上述代码中,SchoolManager类中只有Emp和CollegeManager是直接朋友,CollegeEmp不是直接朋友,所以违反了迪米特法则,不应该在SchoolManager类进行过多的操作,需要将对CollegeEmp类进行操作的代码直接写在CollegeEmp类中。
class Emp {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeEmp {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
class CollegeManager {
public List<CollegeEmp> getAllEmp() {
List<CollegeEmp> list = new List<CollegeEmp>();
for (int i = 0; i < 10; i++) {
CollegeEmp emp = new CollegeEmp();
emp.setId(i);
list.add(emp);
}
return list;
}
public void printAllEmp() {
List<CollegeEmp> list1 = getAllEmp();
System.out.println("-----分公司员工-----");
for (CollegeEmp e : list1) {
System.out.println(e.getId());
}
}
}
// Emp和CollegeManager是直接朋友
// CollegeEmp不是直接朋友,违反了迪米特法则
class SchoolManager {
public List<Emp> getAllEmp() {
List<Emp> list = new List<Emp>();
for (int i = 0; i < 10; i++) {
Emp emp = new Emp();
emp.setId(i);
list.add(emp);
}
return list;
}
public void printAllEmp(CollegeManager sub) {
// List<CollegeEmp> list1 = sub.getAllEmp();
// System.out.println("-----分公司员工-----");
// for (CollegeEmp e : list1) {
// System.out.println(e.getId());
// }
sub.printAllEmp();
List<Emp> list2 = this.getAllEmp();
System.out.println("-----分公司员工-----");
for (Emp e : list2) {
System.out.println(e.getId());
}
}
}
public static vooid main(String[] args) {
SchoolManager schoolManager = new SchoolManager();
schoolManager.printAllEmp(new CollegeManager());
}
合成复用原则
尽量使用**合成/聚合**的方式,而不使用继承。
B类只需要调用A类的方法,如果使用继承,很增大A和B之间的耦合性。
解决办法:
方法一:
使用依赖:B类中传入A类的对象实例,B依赖A
方法二:
使用聚合:在B类中添加A类的属性,把A聚合到B中
方法三:
使用组合:在B中创建A的对象,把A组合到B中
23种设计模式
设计模式分为**三种类型,共23种**
- 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
- 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式
单例模式
采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个过去类对象实例的方法
比如Hibernate的SessionFactory,它充当数据存储元的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般来说,整个程序只需要一个SessionFactory就够了,所以使用单例模式。
单例模式创建的八种方式:
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(同步代码块)
- 双重检查
- 静态内部类
- 枚举
饿汉式(静态常量)
- 构造器私有化(防止外界new)
- 类的内部创建对象
- 向外暴露一个静态的公共方法获取实例(getInstance)
public class Singleton {
private final static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
优缺点:
- 优点:这种方法书写比较简单,在类装载时完成实例的创建,避免了线程同步问题。
- 缺点:在类转载时完成实例化,没有达到Lazy Loading的效果。如果从始至终都没有使用到该类,会在**造成内存的浪费**。
- 这种基于classloader的机制避免了多线程同步的问题(因为在**类加载时便创建好一个静态的对象,之后便不会再改变)。但是导致类加载的原因有很多种,因此不能确定其他方式(或者其他静态方法)导致类加载,这就导致了初始化instance的时候达不到懒加载的效果**。
- 结论:可能造成资源浪费。
饿汉式(静态代码块)
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Sinleton() {}
public static Singleton getInstance() {
return instance;
}
}
这种方式的优缺点和上面的类似。
结论:可能造成资源浪费。
懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return singleton;
}
}
优缺点:
- 优点:起到了Lazy Loading的效果,但是只能在单线程中使用。
- 缺点:如果在多线程中,当一个线程进入if判断语句,但是还没有将对象实例化;同时另一个线程也进入了if判断语句,这个时候便会破坏单例模式。所以在多线程中不能使用这种方式。
- 结论:在真实生产中不可以使用这种方法,只能用于单线程,因为存在线程安全问题。
懒汉式(线程安全,同步方法)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优缺点:
- 优点:解决了线程安全问题。
- 缺点:效率太低。每个线程想要获取了类实例的时候,都需要进行同步。然后这个方法只要执行一次就行了,之后可以直接返回instance实例。方法进行同步效率太低了。
- 结论:效率过低,不推荐使用。
懒汉式(同步代码块)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
和第三种方法错误相同,解决不了任何问题。
结论:无法使用。
双重检查
public class Singleton {
// 此处的volatile是为了防止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
singleton = new Singleton();
}
}
}
return instance;
}
}
这里必须加volalite的原因:
new 对象的过程分为三步:
- 分配空间
- 初始化对象
- 指向对象内存地址。
2和3可能被编译器自动重排,导致判断非空但是实际拿的对象还未完成初始化。
优缺点:
- 优点:Double-check是在多线程开发中经常使用的,进行了两次if (instance == null) 的判断,第一次是提高效率,第二次是保证线程安全。
- 结论:保证了线程安全,实现了延迟加载,效率较高;推荐使用。
静态内部类
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
静态内部类方式在外部类Singleton被装载时并不会被初始化,只有当调用getInstance时,才会装载内部类SingletonInstance,从而完成Singleton的实例。
类的静态属性只会在第一次加载时初始化,所以,JVM帮我们保证了线程的安全,在类进行初始化时,其他线程无法进入。
优缺点:
- 优点:避免了线程不安全,利用了类加载机制的特点实现了延迟加载,效率高。
- 结论:推荐使用。
枚举
public enum Singleton {
INSTANCE;
public void method() {
}
}
优缺点:
- 优点:借助JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步的问题,而且**还能防止反序列化重新创建新的对象**。
- 结论:推荐使用,而且是Effective Java作者推荐使用。
单例模式总结
Java中使用到的单例模式:
java.lang.Runtime使用饿汉式单例模式
java.util.logging中多次使用了单例模式(饿汉式、双重检测模式)
注意事项和细节说明:
- 单例模式保证了系统内存中该对象实例只存在一份,节省了系统资源,对于一些频繁需要创建销毁的,使用单例模式可以提高系统的性能。
- 当想使用单例模式时,记住要使用相应获取对象的方法,不要使用new。
单例模式使用场景:
- 需要频繁创建销毁的对象
- 创建对象时耗时过多或者使用资源较多(即重量级对象),但是又经常用到
- 工具类对象
- 频繁访问数据库或者文件的对象(比如数据源、session工厂等)
简单工厂模式
public class OperationFactory {
public static Operation createOperate(String operate) {
Operation oper = null;
switch (operation) {
case "+":
oper = new OperationAdd();
break;
case "-":
oper = new OperationSub();
break;
case "*":
oper = new OperationMul();
break;
case "/":
oper = new OperationDiv();
break;
default:
break;
}
return oper;
}
}
工厂方法模式
工厂方法模式是简单工厂的进一步抽象和推广。
优点:使用了多态性,保持了简单工厂模式的优点,而且克服了它的缺点。
缺点:但是会增加额外的开发量。
public interface IFactory {
LeiFeng createLeiFeng();
}
class UndergraduateFactory implements IFactory {
public Leifeng createLeifeng() {
return new Undergraduate();
}
}
class VolunteerFactory implements IFactory {
public Leifeng createLeifeng() {
return new Volunteer();
}
}
/**
* 当需要换成“志愿者”时,只需要将UndergraduateFactory换成VolunteerFactory,
* 其他的不需要改变;需要新增其他相关的也不需要修改服务层代码,只需要新增一个实现类即可;
* 保证了对修改关闭,对扩展开放
*/
IFactory factory = new UndergraduateFactory();
LeiFeng leiFeng = factory.createLeiFeng();