设计模式的6个基本原则:
就是一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。
比如当要做一个图片加载器的时候,不应该把所有的东西都写在一个类中,应该各个功能独立出来,可以分成图片加载功能和缓存功能等模块,这样类中的代码逻辑清晰可读性、可扩展性和可维护性会大大提高。
扩展原有功能是开发的,修改原有功能是关闭的。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
简单说就是只要父类出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,或者可能根本就不需要知道父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就能适应。
高层模块不应该依赖底层模块,两者都应该依赖其抽象。
类间的依赖关系应该建立在最小的接口上,接口隔离原则把臃肿的接口拆分成更小更具体的接口。
但是接口不能拆分的过于细致,以免接口泛滥。
也称为最小知识原则。一个对象应该对其他对象有最少的了解。通俗的讲,一个类应该对自己需要耦合或调用的类知道得最小,类的内部如何实现与调用者或者依赖者没关系,调用者或者依赖者只需要知道它需要的方法即可,其他的可一概不管。类与类之间关系越密切,耦合越大,当一个类方法改变时,对另一个类的影响也越大。
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
// 创建所有汽车的接口抽象类
public interface Car {
void drive();
}
// 创建具体的奥迪汽车
public class AudiCar implements Car {
public void drive() {
System.out.println("drive audi...");
}
}
// 创建具体的奔驰汽车
public class BenzCar implements Car {
public void drive() {
System.out.println("drive benz...");
}
}
// 创建工厂类,通过工程类获取相关的汽车对象
/**
* 汽车工厂类
*/
public class CarFactory {
public static Car getCar(String carInfo) {
if("audi".equals(carInfo)){
return new AudiCar();
} else {
return new BenzCar();
}
}
}
//客户端使用
public class Client {
public static void main(String[] args) {
Car car = CarFactory.getCar("benz");
car.drive();
}
}
好处:客户端(使用者)免去了直接创建产品的责任,也不需要关心这个对象是怎么生成的,它只需要拿到这个对象进行消费即可。
缺点:假如我们现在需要再创建一个宝马汽车,那么只需要实现Car类即可,不需要改动原本的产品部分,符合开闭原则。但是新增了这个宝马汽车,在CarFactory里必须再增加一行判断来生成宝马汽车对象,那么这部分是不符合开闭原则的。即新增一个产品,CarFactory类必须得改。
应用场景:简单工厂一般用于业务较为简单的项目里,并且产品很少会扩展的情况下,类似于上面例子里,业务需求里基本确定只有Audi车和Benz车,很难再会有别的车的对象衍生了,那么使用简单工厂即可。
// Car,AudiCar,BenzCar类代码和简单工厂里的一样
/**
* 所有汽车生成工厂的抽象类
*/
public abstract class AbsFactory {
protected abstract Car getCar();
}
/**
* 奥迪车的生成工厂
*/
public class AudiFactory extends AbsFactory {
public Car getCar() {
return new AudiCar();
}
}
/**
* 奔驰车的生成工厂
*/
public class BenzFactory extends AbsFactory {
public Car getCar() {
return new BenzCar();
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
AbsFactory factory = new AudiFactory();
Car car = factory.getCar();
car.drive();
}
}
好处:工厂方法是在简单工厂的基础上进行改进,把简单工厂里的工厂进行了抽象,解决了简单工厂里的缺点问题。假如我们现在需要个宝马汽车,那么我们只需要建一个宝马汽车BMWCar类实现于Car,及宝马汽车工厂BMWFactory类继承于AbsFactory即可,符合开闭原则。
缺点:当大量的产品出现时,就会出现很多Factory类。比如有10个不同类型的汽车,那么就有10个对应的工厂类来生产不同的类型的汽车。
应用场景:稍微复杂点的业务,并且产品的扩展是存在不确定性的情况下,可以考虑使用工厂方法来实现。
// 创建所有汽车的接口抽象类
public interface Car {
void drive();
}
// 创建奥迪和奔驰汽车的抽象类
public abstract class AudiCar implements Car {
}
public abstract class BenzCar implements Car{
}
// 创建奥迪的跑车类和商务车类
public class AudiSportCart extends AudiCar {
public void drive() {
System.out.println("奥迪跑车");
}
}
public class AudiBusinessCart extends AudiCar {
public void drive() {
System.out.println("奥迪商务车");
}
}
// 创建奔驰的跑车类和商务车类
public class BenzSportCart extends BenzCar {
public void drive() {
System.out.println("奔驰跑车");
}
}
public class BenzBusinessCart extends BenzCar {
public void drive() {
System.out.println("奔驰商务车");
}
}
// 创建抽象工厂
/**
* 所有汽车生成工厂的抽象类
*/
public abstract class AbsFactory {
protected abstract Car getBusinessCar();
protected abstract Car getSportCar();
}
// 奥迪车和奔驰车的工厂类
public class AudiFactory extends AbsFactory {
/**
* 生成奥迪商务车
*/
protected Car getBusinessCar() {
return new AudiBusinessCart();
}
/**
* 生成奥迪跑车
*/
protected Car getSportCar() {
return new AudiSportCart();
}
}
public class BenzFactory extends AbsFactory {
/**
* 生成奔驰商务车
*/
protected Car getBusinessCar() {
return new BenzBusinessCart();
}
/**
* 生成奔驰跑车
*/
protected Car getSportCar() {
return new BenzSportCart();
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
AbsFactory factory = new AudiFactory();
Car businessCar = factory.getBusinessCar();
Car sportCar = factory.getSportCar();
businessCar.drive();
sportCar.drive();
}
}
好处:和工厂方法不同的地方在于,每个工厂能生产不同的产品族,即车有不同牌子,但是扩展开来,每个牌子里又有跑车类型、商务车类型等等。每个牌子的工厂,能生产出不同类型的车,比如奥迪的工厂里,能生产出奥迪的跑车、奥迪的商务车等等。如果增加一种牌子的车,比如宝马,是符合开闭原则的,不需要改动原代码。
缺点:如果增加一种类型的车,比如奥迪里增加家庭类型车,那么就不符合开闭原则,此外也是和工厂方法一样,类较多。
应用场景:任何复制的场景,都可以使用抽象工厂。但是要考虑的是哪个维度最有可能不变,假如像上面的例子里,汽车的牌子是可能经常要变的,而汽车的类型几乎是不变的话,那么就可以像上例子那样实现。假如调转过来,汽车的牌子估计是不变的,汽车的类型经常会拓展,那么工厂就需要调整成SportFactory、BusinessFactory,每个工厂里能生产出不同牌子的汽车。比如SportFactory里能生产出audi、benz、bmw的跑车。
单例模式的核心,就是全局只有一个实例。下面就每一种创建方式分析其优缺点。
/**
* 饿汉式
*/
public class PersonHungry {
private static PersonHungry INSTANCE = new PersonHungry();
private PersonHungry() {}
public static PersonHungry getInstance() {
return INSTANCE;
}
}
饿汉式优点:饿汉式在类加载时,就已经创建了实例对象,故不存在线程安不安全的问题。
饿汉式缺点:类加载时就创建了实例对象,比如只是调用里面某个static方法,这个时候就会创建了该实例对象了,有可能一直都不需要getInstance,因此这样会造成资源浪费。
public class Person {
private static Person INSTANCE = null;
static {
INSTANCE = new Person();
}
private Person() {}
public static Person getInstance() {
return INSTANCE;
}
}
静态代码块其实相当于另外一种饿汉式,类加载的时候,对象就会创建,虽说线程安全,但是资源浪费。
public class PersonLazy1 {
private static PersonLazy1 mInstance = null;
private PersonLazy1() {}
/**
* 该方式在多线程运行下,实例不唯一
*/
public static PersonLazy1 getInstance() {
if (mInstance == null) {
mInstance = new PersonLazy1();
}
return mInstance;
}
}
public class PersonLazy2 {
private static PersonLazy2 mInstance = null;
private PersonLazy2() {}
/**
* 耗性能
*/
public synchronized static PersonLazy2 getInstance() {
if (mInstance == null) {
mInstance = new PersonLazy2();
}
return mInstance;
}
}
懒汉式1,在多线程的情况下,没法保证只创建一个实例对象。所以演变出懒汉式2,这种方式从效果的角度是可以实现单例的,通过synchronized保证了多线程的安全性。但是,懒汉式2最大的缺点就是耗性能,不管是否已经创建了实例,每次调用getInstance都需要锁。
public class PersonLazy3 {
private static PersonLazy3 mInstance = null;
private PersonLazy3() {}
/**
* 改进性能,双重判断
*/
public static PersonLazy3 getInstance() {
if (mInstance == null) {
synchronized (PersonLazy3.class) {
if (mInstance == null) {
mInstance = new PersonLazy3();
}
}
}
return mInstance;
}
}
/**
* 推荐使用的方式
*/
public class PersonLazy4 {
// 加上volatile关键字,最值得推荐的方式
private static volatile PersonLazy4 mInstance = null;
private PersonLazy4() {}
/**
* 改进性能,双重判断
*/
public static PersonLazy4 getInstance() {
if (mInstance == null) {
synchronized (PersonLazy4.class) {
if (mInstance == null) {
mInstance = new PersonLazy4();
}
}
}
return mInstance;
}
}
懒汉式3和4都是使用双重判断再加锁的形式,即DCL。相比于懒汉式2,改进了性能,只有在第一次getInstance时会比较耗性能。而4比3多了一个关键字叫volatile。那么volatile关键字有什么作用呢?
1、防止重排序。**什么是重排序呢?比如Apple a = new Apple();,创建Apple对象的步骤如下:
(1)在堆中划出一片内存区域,用于存放Apple对象
(2)Apple对象初始化
(3)把堆中的地址赋值给栈中的a
但是,java多线程中,步骤(2)和(3)是有可能调换顺序的。假如先把地址赋值给了a,但是Apple对象还没有初始化。这个时候去判断null就已经不是空了,然后,就把这个对象返回去用,由于其还没有初始化,就有可能各种脏数据,出问题了。
2、线程可见性。**什么是可见性呢?某一个线程改变了公用对象或者变量,在短时间内,另外一个线程有可能是不可见的,因为每个线程都有自己的缓存区域。
综上所述,懒汉式4,即DCL + volatile的方式,是最比较值得推荐的实现方式。
public class PersonStatic {
private PersonStatic() {}
public static PersonStatic getInstance() {
return PersonStaticHolder.INSTANCE;
}
/**
* 静态内部类
*/
private static class PersonStaticHolder {
private static PersonStatic INSTANCE = new PersonStatic();
}
}
其实现原理和饿汉式差不多,解决了饿汉式加载类就加载的问题,因为外部类PersonStatic加载时,其内部类PersonStaticHolder是不会随着加载的。当第一次执行getInstance时,执行到PersonStaticHolder.Instance时,PersonStaticHolder类才开始被加载。其实不管多少个线程去调用getInstance方法,返回的都是第一次调用时创建的对象。这种方法不仅能保证了线程的安全性,也保证单例的唯一性,同时也延迟了单例的实例化。
缺点:假如PersonStatic创建时需要传入一个参数,比如Context,那么这种方式实现的单例是不支持。
1、定义一个需要单例的Person类
2、定义枚举类,用于保存Person类对象
public enum PersonEnum {
INSTANCE;
private Person mPerson;
PersonEnum() {
mPerson = new Person();
System.out.println("PersonEnum run");
}
public Person getPerson() {
return mPerson;
}
}
3、调用
public class Test {
public static void main(String[] args) {
Person p1 = PersonEnum.INSTANCE.getPerson();
Person p2 = PersonEnum.INSTANCE.getPerson();
System.out.println(p1 == p2); // true
}
}
首先需要明确两点,第一,枚举类的构造方法本身就只支持私有的,即外部是不能直接去new的。第二,JAVA里枚举类的创建本身就是在线程安全的情况下。基于以上两点,第一次调用PersonEnum.Instance实际上就会调用枚举类其自己构造方法,类似于我们创建对象一样,当执行构造方法后,就会创建一个Person对象存储在mPerson变量中。当第二次再调用PersonEnum.Instance时,Instance属性因为已经存在了,所以不会再执行枚举里的构造方法了,从而保证了单例的唯一性。
缺点:Android里并不推荐使用枚举类,有两个原因,第一,较多的枚举类会增加DEX文件大小。第二,枚举比普通常量占用内存大很多。
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象。
和工厂模式的区别是:
工厂模式一般都是创建一个产品,注重的是把这个产品创建出来就行,只要创建出来,不关心这个产品的组成部分。从代码上看,工厂模式就是一个方法,用这个方法就能生产出产品。
建造者模式也是创建一个产品,但是不仅要把这个产品创建出来,还要关心这个产品的组成细节、组成过程。从代码上看,建造者模式在建造产品时,这个产品有很多方法,建造者模式会根据这些相同方法但是不同执行顺序建造出不同组成细节的产品。
/**
* 产品-电脑
*/
public class Computer {
private String cpu;
private String broad;
private String memory;
public void setCpu(String cpu) {
this.cpu = cpu;
}
public void setBroad(String broad) {
this.broad = broad;
}
public void setMemory(String memory) {
this.memory = memory;
}
@Override
public String toString() {
return "Computer [cpu=" + cpu + ", broad=" + broad + ", memory="
+ memory + "]";
}
}
/**
* 定义电脑制造者的标准接口
*/
public interface Builder {
// 组装CPU
Builder buildCpu(String cpu);
// 组装主板
Builder buildBroad(String broad);
// 组装内存
Builder buildMemory(String memory);
// 生产出产品-电脑
Computer build();
}
/**
* 电脑的制造者
*/
public class ComputerBuilder implements Builder {
private String mCpu, mBroad, mMemory;
public Builder buildCpu(String cpu) {
this.mCpu = cpu;
return this;
}
public Builder buildBroad(String broad) {
this.mBroad = broad;
return this;
}
public Builder buildMemory(String memory) {
this.mMemory = memory;
return this;
}
// 生产出电脑
public Computer build() {
Computer computer = new Computer();
computer.setCpu(mCpu);
computer.setBroad(mBroad);
computer.setMemory(mMemory);
return computer;
}
}
// 客户需要获取产品-电脑
public static void main(String[] args) {
// 需要一个电脑制造者
ComputerBuilder builder = new ComputerBuilder();
// 制造出电脑,关注生产细节,主板、CPU、内存等
Computer computer = builder.buildCpu("CPU")
.buildBroad("主板")
.buildMemory("DDR3内存")
.build();
System.out.println(computer.toString());
}
// 新建一个Sport类
public class Sport {
private String mTypeName;
public Sport(String typeName) {
this.mTypeName = typeName;
}
public String getTypeName() {
return mTypeName;
}
}
// 新建一个Person类,要克隆复制的是这个类,克隆的类需要实现Cloneable接口并实现clone方法
public class Person implements Cloneable {
public String name;
public int age;
public Sport sport;
protected Object clone() {
try {
return super.clone();
} catch(Exception e) {
return null;
}
}
}
// 客户端应用类
public class Client {
public static void main(String[] args) {
Person xiaoming = new Person();
xiaoming.name = "小明";
xiaoming.age = 20;
xiaoming.sport = new Sport("打篮球");
Person lili = (Person) xiaoming.clone();
System.out.println(xiaoming == lili); // false
System.out.println(xiaoming.sport == lili.sport); // true
}
}
所谓的克隆就是把对象复制一份新的,而浅克隆只能复制了该类里的八大基本属性,引用属性只是赋值了,并没有复制。例如上面例子中,lili对象是从xiaoming里复制出来的,其里面的值也是和被克隆者一样的,从结果xiaoming != lili可以看出,他们是不同的对象。但是xiaoming和lili两个不同对象里的sport变量,指向的却是同一个sport对象,即创建xiaoming对象时new的那个sport对象。
// Sport类增加实现Cloneable接口,实现clone方法
public class Sport implements Cloneable {
private String mTypeName;
public Sport(String typeName) {
this.mTypeName = typeName;
}
public String getTypeName() {
return mTypeName;
}
protected Object clone() {
try {
return super.clone();
} catch(Exception e) {
return null;
}
}
}
// Person类在浅克隆基础上进行修改
public class Person implements Cloneable {
public String name;
public int age;
public Sport sport;
protected Object clone() {
try {
Person p = (Person) super.clone();
// sport类也进行克隆
p.sport = (Sport) sport.clone();
return p;
} catch(Exception e) {
return null;
}
}
}
// 客户端应用类
public class Client {
public static void main(String[] args) {
Person xiaoming = new Person();
xiaoming.name = "小明";
xiaoming.age = 20;
xiaoming.sport = new Sport("打篮球");
Person lili = (Person) xiaoming.clone();
System.out.println(xiaoming == lili); // false
System.out.println(xiaoming.sport == lili.sport); // false
}
}
所谓的深度克制,即把被克隆类里的非基本数据类型也进行了复制操作,比如上述例子中的Sport类。
总结:原型模式适合用于创建对象时较为复杂,内存开销或者耗时比较大的情况下,又或者希望对目标对象的修改不影响原对象。其优点是,简化了对象的创建过程,创建对象性能更好尤其在复杂对象时,修改目标对象不影响原对象。其缺点一是,当对已有的类进行克隆改造时,需要修改每个类,重写clone方法,违反了开闭原则。其缺点二是,如果是深度克隆,并且对象有多重嵌套时,每一层对象都要去做深度克隆,相对麻烦。