第四章:设计模式
设计模式是前人(一般都是大师)对程序设计经验的总结,学习并应用设计模式可以使我们开发的项目更加规范、便于扩展和维护。
1)面向对象四大特征
封装:即将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变。
张三这个人,他的姓名等属性,要有自己提供的获取或改变的方法来操作。
private name setName getName
抽象:就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处,并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。 就是把现实生活中的对象,抽象为类。
继承:在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。遗产继承
多态:是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
Object obj = new xxx();
UserDao userDao = new UserDaoJdbcImpl();
UserDao userDao = new UserDaoHibernateImpl();
靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象(里氏替换原则),而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
扩展:一个程序就是一个世界,世界由不同的种类组成,每个类别下的个体就是一个个对象。封装是对对象属性的私有化,然后通过公开的方法进行访问。继承是is-a的关系,子类会继承父类的非私有属性与方法。多态分静态的多态(构造函数的重载)与动态的多态(编译时多态,里氏替换原则),表面意义是一种类型在不同情况下的不同形态【如类的多态,行为(接口)的多态】。
2)五大设计原则
单一职责原则(即一个类只负责一项职责)
单一职责原则(Single Responsibility Principle,SRP):就一个类而言,应该仅有一个引起它变化的原因。即一个类应该只负责一个功能领域中的相应职责。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
开闭原则(一个类或方法应该对扩展开放,对修改关闭)
开闭原则(Open-Closed Principle,OCP): 是指软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。即软件实体应该尽量在不修改原有代码的情况下进行扩展。
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。
里氏替换原则(子类可以扩展父类的功能,但不能改变父类原有的功能,子类对象实例化父类对象)
里氏替换原则(Liskov Substitution Principle,LSP):所有引用父类的地方必须能够透明的使用子类的对象。即子类型必须能够替换掉它们的父类型。
里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。同时,里氏代换原则是实现开闭原则的重要方式之一。
它包含以下含义:
1) 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2) 子类的方法重载父类的方法时,方法的入参要比父类方法的入参更宽松。
3) 子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。
依赖倒置原则(上层模块不应该依赖低层模块的实现,而应该依赖其抽象)
依赖倒置原则(Dependency Inversion Principle,DIP):抽象不应该依赖细节,细节应该依赖于抽象。即应该针对接口编程,而不是针对实现编程。
在大多数情况下,我们会同时使用开闭原则、里氏代换原则和依赖倒转原则,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段。
依赖倒置的优点:减少类之间的耦合,提高系统的稳定性,减少并行开发引起的风险。
接口隔离原则(一个类对另一个类的依赖应该建立在最小的接口上)
接口隔离原则(Interface Segregation Principle,ISP):使用专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
3)设计模式基础
一般地设计模式分为三类共23种:
3.1. 创建型模式
单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
3.2. 结构型模式
适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
3.3. 行为型模式
模版模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式、访问者模式。
这些设计模式特别关注对象之间的通信。
4)单例模式
确保一个类最多只有一个实例,并提供一个全局访问点。
有些对象我们只需要一个:线程池,缓存,硬件设备,如果多个实例会造成冲突,结果不一致等问题,可以用静态变量来实现,或者程序员之间协商一个全局变量。
具体实现原理:可以将一个类的构造函数私有化,然后在该类内部创建自己的对象。
饱汉模式:不管是否用到,都先创建好
/**
* @author: 肖德子裕
* @date: 2019/02/28 20:50
* @description: 饱汉模式
*/
public class Singleton2 {
private static Singleton2 uniqeInstance=new Singleton2();
private Singleton2() {
}
public static Singleton2 getInstance(){
return uniqeInstance;
}
}
饥汉模式:需要时才创建
/**
* @author: 肖德子裕
* @date: 2019/02/28 20:50
* @description: 饥汉模式
*/
public class Singleton {
private static Singleton uniqeInstance=null;
private Singleton() {
}
public static Singleton getInstance(){
if (uniqeInstance==null){
uniqeInstance=new Singleton();
}
return uniqeInstance;
}
}
在多线程的情况下,可能2个线程同时访问该方法,这样的话就会创建2个实例对象,所以使用双重检查加锁解决该问题
public static ChocolateFactory getInstance() {
if (uniqueInstance == null) {
synchronized (ChocolateFactory.class) {
if (uniqueInstance == null) {
uniqueInstance = new ChocolateFactory();
}
}
}
return uniqueInstance;
}
扩展知识:
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
-
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
-
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
-
修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
-
修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
修饰一个代码块:一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
创建了两个SyncThread的对象syncThread1和syncThread2,线程thread1执行的是syncThread1对象中的synchronized代码(run),而线程thread2执行的是syncThread2对象中的synchronized代码(run);我们知道synchronized锁定的是对象,这时会有两把锁分别锁定syncThread1对象和syncThread2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。
当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
在用synchronized修饰方法时要注意以下几点:
-
synchronized关键字不能继承。
-
在定义接口方法时不能使用synchronized关键字。
-
构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
5)工厂模式
遵循依赖抽象原则:
变量不要持有具体类的引用;不要让类继承自具体类,要继承自抽象类或接口;不要覆盖基类中已经实现的方法。
5.1 简单工厂模式:
定义了一个创建对象的类,由这个类来封装实例化对象的行为
问题:如一家披萨店,在下订单时会根据不同的披萨实例化不同的对象,这样以后每次提出新品种或者下架某品种,就要去修改订单里面的实例化操作。
如下可以定义一个简单的工厂类来封装实例化过程:
/**
* 简单工厂模式
*/
public class SimplePizzaFactory {
/**
* 传入披萨的类型
*
* @param ordertype
* @return
*/
public Pizza CreatePizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new CheesePizza();
} else if (ordertype.equals("greek")) {
pizza = new GreekPizza();
} else if (ordertype.equals("pepper")) {
pizza = new PepperPizza();
}
return pizza;
}
}
/**
* 披萨订单模块
*/
public class OrderPizza {
SimplePizzaFactory mSimplePizzaFactory;
public OrderPizza(SimplePizzaFactory mSimplePizzaFactory) {
setFactory(mSimplePizzaFactory);
}
public void setFactory(SimplePizzaFactory mSimplePizzaFactory) {
Pizza pizza = null;
String ordertype;
this.mSimplePizzaFactory = mSimplePizzaFactory;
//工厂生产披萨并进行包装发货
do {
ordertype = gettype();
pizza = mSimplePizzaFactory.CreatePizza(ordertype);
if (pizza != null) {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
} while (true);
}
private String gettype() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(
System.in));
System.out.println("input pizza type:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
/**
* 披萨店中购买披萨
*/
public class PizzaStroe {
public static void main(String[] args) {
SimplePizzaFactory mSimplePizzaFactory;
OrderPizza mOrderPizza;
mOrderPizza=new OrderPizza(new SimplePizzaFactory());
}
}
5.2 方法工厂模式:
定义了一个创建对象的抽象方法,由子类决定要实例化的类。
问题:如果一家披萨店要在全国开连锁,这样地方的配料肯定不一样,所以单纯的将工厂复制过去是不行的。
如下可以将创建披萨的方法抽象化,让子类继承后去实例化具体的对象:
/**
* 抽象化下订单的模块
*/
public abstract class OrderPizza {
public OrderPizza() {
Pizza pizza = null;
String ordertype;
do {
ordertype = gettype();
pizza = createPizza(ordertype);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} while (true);
}
abstract Pizza createPizza(String ordertype);
private String gettype() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(
System.in));
System.out.println("input pizza type:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
/**
* 伦敦的披萨店
*/
public class LDOrderPizza extends OrderPizza {
@Override
Pizza createPizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new LDCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new LDPepperPizza();
}
return pizza;
}
}
/**
* 纽约的披萨店
*/
public class NYOrderPizza extends OrderPizza {
@Override
Pizza createPizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new NYCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new NYPepperPizza();
}
return pizza;
}
}
/**
* 披萨店中购买披萨
*/
public class PizzaStroe {
public static void main(String[] args) {
OrderPizza mOrderPizza;
mOrderPizza=new NYOrderPizza();
}
}
5.3 抽象工厂模式
定义了一个接口用于创建相关或有依赖关系的对象族,而无需明确指定具体类。
/**
* 抽象工厂模式
*/
public interface AbsFactory {
public Pizza CreatePizza(String ordertype) ;
}
/**
* 伦敦工厂
*/
public class LDFactory implements AbsFactory {
@Override
public Pizza CreatePizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new LDCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new LDPepperPizza();
}
return pizza;
}
}
/**
* 纽约工厂
*/
public class NYFactory implements AbsFactory {
@Override
public Pizza CreatePizza(String ordertype) {
Pizza pizza = null;
if (ordertype.equals("cheese")) {
pizza = new NYCheesePizza();
} else if (ordertype.equals("pepper")) {
pizza = new NYPepperPizza();
}
return pizza;
}
}
/**
* 下订单的模块
*/
public class OrderPizza {
AbsFactory mFactory;
public OrderPizza(AbsFactory mFactory) {
setFactory(mFactory);
}
public void setFactory(AbsFactory mFactory) {
Pizza pizza = null;
String ordertype;
this.mFactory = mFactory;
do {
ordertype = gettype();
pizza = mFactory.CreatePizza(ordertype);
if (pizza != null) {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
} while (true);
}
private String gettype() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(
System.in));
System.out.println("input pizza type:");
String str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
/**
* 披萨店中购买披萨
*/
public class PizzaStroe {
public static void main(String[] args) {
OrderPizza mOrderPizza;
mOrderPizza=new OrderPizza(new LDFactory());
}
}
6)策略模式
分别封装行为接口,实现算法族,超类里放行为接口对象,在子类里具体设置行为对象。
策略模式注重功能实现。
原则:分离变化部分,封装接口,基于接口编程各种功能,此模式让行为算法的变化独立于算法的使用者。
将行为定义为接口:
/**
* 飞行
*/
public interface FlyBehavior {
void fly();
}
具体的实现类:
/**
* 会飞
*/
public class GoodFlyBehavior implements FlyBehavior {
@Override
public void fly() {
System.out.println("--GoodFly--");
}
}
/**
* 不会飞
*/
public class NoFlyBehavior implements FlyBehavior {
@Override
public void fly() {
System.out.println("--NoFly--");
}
}
如果还有其他行为也可以向上面一样,先定义接口,在实现,比如叫声可以分呱呱叫还有其他叫。
定义一个鸭子类:
/**
* 鸭子
*/
public abstract class Duck {
/**
* 飞行
*/
FlyBehavior mFlyBehavior;
/**
* 叫声
*/
QuackBehavior mQuackBehavior;
public abstract void display();
public Duck() {
}
public void Fly() {
mFlyBehavior.fly();
}
public void Quack() {
mQuackBehavior.quack();
}
public void SetQuackBehavoir(QuackBehavior qb) {
mQuackBehavior = qb;
}
public void SetFlyBehavoir(FlyBehavior fb) {
mFlyBehavior = fb;
}
public void swim() {
System.out.println("~~im swim~~");
}
}
鸭子下的子类:
/**
* 绿头鸭
*/
public class GreenHeadDuck extends Duck {
public GreenHeadDuck() {
//实例化具体的飞行方式
mFlyBehavior = new GoodFlyBehavior();
//实例化具体的叫声
mQuackBehavior = new GaGaQuackBehavior();
}
@Override
public void display() {
System.out.println("**GreenHead**");
}
}
这样就能实现按不同需求实现:
public class StimulateDuck {
public static void main(String[] args) {
GreenHeadDuck mGreenHeadDuck = new GreenHeadDuck();
RedHeadDuck mRedHeadDuck = new RedHeadDuck();
mGreenHeadDuck.display();
mGreenHeadDuck.Fly();
mGreenHeadDuck.Quack();
mGreenHeadDuck.swim();
mRedHeadDuck.display();
mRedHeadDuck.Quack();
mRedHeadDuck.swim();
mRedHeadDuck.Fly();
}
}
7)模板模式
封装了一个算法步骤,并允许子类为一个或多个步骤方法提供实现;模板模式可以使子类在不改变算法结构的情况下,重新定义算法中的某些步骤。
模板模式注重步骤实现。
/**
* @author: 肖德子裕
* @date: 2019/03/03 19:31
* @description: 模板模式
*/
public abstract class HotDrinkTemplate {
/**
* 这是一个模板方法
*/
public final void prepareRecipe() {
//烧水,固定
boilWater();
//喝什么,由子类实现
brew();
//加水,固定
pourInCup();
//通过钩子方法,让子类确定是否配料
if (wantCondimentsHook()) {
//加配料
addCodiments();
} else {
System.out.println("不加配料");
}
}
/**
* 钩子方法
*
* @return
*/
public boolean wantCondimentsHook() {
return true;
}
public final void boilWater() {
System.out.println("Boiling water");
}
public abstract void brew();
public final void pourInCup() {
System.out.println("put in cup");
}
public abstract void addCodiments();
}
/**
* @author: 肖德子裕
* @date: 2019/03/03 19:52
* @description: 泡茶
*/
public class TeaWithHook extends HotDrinkTemplate{
@Override
public boolean wantCondimentsHook() {
System.out.println("y/n:");
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
String result="";
try {
result=in.readLine();
}catch (IOException e){
e.printStackTrace();
}
if (result.equals("n")){
return false;
}
return true;
}
@Override
public void brew() {
System.out.println("泡茶");
}
@Override
public void addCodiments() {
System.out.println("加柠檬");
}
}
/**
* @author: 肖德子裕
* @date: 2019/03/03 19:58
* @description: 测试
*/
public class Main {
public static void main(String[] args) {
TeaWithHook tea=new TeaWithHook();
tea.prepareRecipe();
}
}
模板方法模式的优点
-
封装不变的内容,扩展可变部分,如果我们要添加一个H3悍马模型,只需要继承父类就可以了。
-
提取公共部分代码,便于维护
-
行为由父类控制,子类实现。基本方法是由子类实现的,因此子类可以通过拓展的方法增加相应的功能,符合开闭原则。
模板方法模式的使用场景
-
多个子类有公有的方法,并且逻辑基本相同时
-
重复、复杂的算法,可以把核心算法设计为模板方法,周边的细节则有各个子类实现
-
代码重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后用钩子方法约束其行为。
钩子方法源于设计模式中模板方法模式,模板方法模式中分为两大类:模版方法和基本方法,而基本方法又分为:抽象方法,具体方法,钩子方法。
好莱坞原则:明星无需联系经纪人问名具体流程,只需做好自己的事即可,经纪人会通知明星去哪里做什么。高层无需知道调用底层的细节,利于解耦。好比模板模式的子类无需知道模板的步骤,只需做好自己该实现的方法。