学习设计模式之前我们需要具备一些基础知识,首先需要熟悉面向对象软件开发经历的三个阶段,即OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。
UML2.0包括以下14类图,本篇只是提出但不详细学习这些图,需要各自掌握类、接口、类图,并能运用自如画图,后续我们有时间再专门针对UML详细展开。
在软件设计中类与类之间关系包括泛化、 实现、组合 、 聚合 、 关联 、 依赖 。
软件设计有七大设计原则,分别是开闭原则、单一职责原则、接口隔离原则、里氏替换原则、依赖倒置原则、合成复用原则、迪米特法则,设计原则作为设计模式的指导思想,而设计模式则是基于设计原则实践发展沉淀的经验,目的是为了提高程序可靠性、可维护性、可复用性、可扩展性,实现面向对象编程的解耦,类与类做到低耦合高内聚;软件编程过程需综合考虑并尽量遵守这些设计原则。引用一句口诀:访问加限制、函数要节俭、依赖不允许、动态加接口、父类要抽象、扩展不更改。
开闭原则(Open-Closed Principle,OCP)是规范我们程序员编程最基本、最重要的原则,强调对扩展开放、对修改关闭,即软件实体应尽量在不修改原有代码的情况下进行扩展。开闭原则是其他设计原则的总纲,是面向对象的可复用设计的首要基石,其他设计原则本质是围绕开闭原则在不同角度去展开。
下面举一个违反开闭原则的例子:支持显示各种类型的图表,如饼状图和柱状图等,类图设计如下:
在ChartDisplay类的display()方法中有如下if else if的处理逻辑代码
if (type.equals("pie")) {
PieChart chart = new PieChart();
chart.display();
}
else if (type.equals("bar")) {
BarChart chart = new BarChart();
chart.display();
在上面ChartDisplay类代码中如果需要增加一个新的图表类如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,这样就违反了开闭原则;比如可采用策略模式的设计模式来解决上面这种违反开闭原则会变化if else的问题。
吃类Eat.java,如果后续增加吃的方式那么就需要在Eat类修改,这样就有可能影响了原有功能,违反开闭原则。
package cn.itxs.principle.srp;
public class Eat {
public void doEat(String animal){
if ("狮子".equals(animal)){
System.out.println(animal+"在大口吃肉!");
}else if ("黄牛".equals(animal)) {
System.out.println(animal+"在细嚼慢咽吃草!");
}
}
}
测试类SrpMain.java
package cn.itxs.principle.srp;
public class SrpMain {
public static void main(String[] args) {
Eat animal = new Eat();
animal.doEat("狮子");
animal.doEat("黄牛");
}
}
吃肉类EatMeet.java
package cn.itxs.principle.srp;
public class EatMeet {
public void doEat(String animal){
System.out.println(animal+"在大口吃肉!");
}
}
吃草类EatGrass.java
package cn.itxs.principle.srp;
public class EatGrass {
public void doEat(String animal){
System.out.println(animal+"在细嚼慢咽吃草!");
}
}
测试类SrpMain.java
package cn.itxs.principle.srp;
public class SrpMain {
public static void main(String[] args) {
EatMeet animalMeet = new EatMeet();
animalMeet.doEat("狮子");
EatGrass animalGrass = new EatGrass();
animalGrass.doEat("黄牛");
}
}
吃类Eat.java
package cn.itxs.principle.srp;
public class Eat {
public void doEatMeet(String animal){
System.out.println(animal+"在大口吃肉!");
}
public void doEatGrass(String animal){
System.out.println(animal+"在细嚼慢咽吃草!");
}
}
测试类SrpMain.java
package cn.itxs.principle.srp;
public class SrpMain {
public static void main(String[] args) {
Eat animal = new Eat();
animal.doEatMeet("狮子");
animal.doEatGrass("黄牛");
}
}
Behavior行为接口
package cn.itxs.principle.isp;
public interface Behavior {
void work();
void eat();
}
Man类实现行为接口
package cn.itxs.principle.isp;
public class Man implements Behavior{
@Override
public void work() {
System.out.println("man do work!");
}
@Override
public void eat() {
System.out.println("man eat something!");
}
}
Robot类实现行为接口
package cn.itxs.principle.isp;
public class Robot implements Behavior{
@Override
public void work() {
System.out.println("robot do work!");
}
//机器人不需要吃饭,空实现
@Override
public void eat() {}
}
WorkBehavior接口
package cn.itxs.principle.isp;
public interface WorkBehavior {
void work();
}
EatBehavior接口
package cn.itxs.principle.isp;
public interface EatBehavior {
void eat();
}
Man类实现Work和Eat行为接口
package cn.itxs.principle.isp;
public class Man implements WorkBehavior,EatBehavior{
@Override
public void work() {
System.out.println("man do work!");
}
@Override
public void eat() {
System.out.println("man eat something!");
}
}
Robot类只实现WorkBehavior接口
package cn.itxs.principle.isp;
public class Robot implements WorkBehavior{
@Override
public void work() {
System.out.println("robot do work!");
}
}
计算器类Caculator.java
package cn.itxs.principle.lsp;
public class Caculator {
public int add(int a,int b){
return a+b;
}
}
特殊计算器类SpecialCaculator.java
package cn.itxs.principle.lsp;
public class SpecialCaculator extends Caculator{
@Override
public int add(int a, int b) {
return a-b;
}
}
测试类ISPMain.java
package cn.itxs.principle.lsp;
public class ISPMain {
public static void main(String[] args) {
Caculator caculator = new SpecialCaculator();
System.out.println("100+20="+caculator.add(100, 20));
}
}
**输出结果为100+20=80,我们发现原来运行正常的加法功能发生了错误,原因子类SpecialCaculator可能无意重写Caculator的add方法,造成原本运行加法功能的代码调用了类SpecialCaculator的重写后的方法而导致出现了错误。 **
特殊计算器类SpecialCaculator.java
package cn.itxs.principle.lsp;
public class SpecialCaculator extends Caculator{
public int sub(int a, int b) {
return a-b;
}
}
测试类ISPMain.java
package cn.itxs.principle.lsp;
public class ISPMain {
public static void main(String[] args) {
SpecialCaculator specialCaculator = new SpecialCaculator();
System.out.println("100+20="+specialCaculator.add(100, 20));
System.out.println("80-30="+specialCaculator.sub(80, 30));
}
}
Doc文档读取类ReadDoc.java
package cn.itxs.principle.dip;
public class ReadDoc {
public void read(){
System.out.println("read doc!");
}
}
Excel文件读取类ReadExcel.java
package cn.itxs.principle.dip;
public class ReadExcel {
public void read(){
System.out.println("read excel!");
}
}
ReadXml文档读取类ReadXml.java
package cn.itxs.principle.dip;
public class ReadXml {
public void read(){
System.out.println("read xml!");
}
}
读取工具类ReadTool.java
package cn.itxs.principle.dip;
public class ReadTool {
public void read(ReadXml r){
r.read();
}
public void read(ReadDoc r){
r.read();
}
public void read(ReadExcel r){
r.read();
}
}
上面的设计如果需要扩展读取其他文件内容,需要在ReadTool类中增加相应类型的read方法,这就违反了开闭原则。
先设计一个读接口
package cn.itxs.principle.dip;
public interface Readable {
void read();
}
然后Excel、Doc、Xml实现类都实现Readable读接口
public class ReadExcel implements Readable{
@Override
public void read(){
System.out.println("read excel!");
}
}
package cn.itxs.principle.dip;
public class ReadXml implements Readable{
@Override
public void read(){
System.out.println("read xml!");
}
}
package cn.itxs.principle.dip;
public class ReadDoc implements Readable{
@Override
public void read(){
System.out.println("read doc!");
}
}
下面我们选择set方法设值注入的方式来演示
package cn.itxs.principle.dip;
public class ReadTool {
private Readable readable;
public void setreadable(Readable readable) {
this.readable = readable;
}
public void read(){
readable.read();
}
}
读取工具类ReadTool.java
package cn.itxs.principle.dip;
public class DIPMain {
public static void main(String[] args) {
ReadTool readTool = new ReadTool();
readTool.setreadable(new ReadXml());
readTool.read();
readTool.setreadable(new ReadExcel());
readTool.read();
readTool.setreadable(new ReadDoc());
readTool.read();
}
}
这样我们依赖的是抽象,增加读取类型的时候只需要增加实现类即可,ReadTool就无需再进行修改了。
汉堡抽象类Hamburger.java
package cn.itxs.principle.crp;
public abstract class Hamburger {
abstract void meat();
}
鸡肉汉堡类ChickenHamburger.java
package cn.itxs.principle.crp;
public class ChickenHamburger extends Hamburger{
@Override
void meat() {
System.out.println("鸡肉汉堡");
}
}
牛肉汉堡类BeefHamburger.java
package cn.itxs.principle.crp;
public class BeefHamburger extends Hamburger{
@Override
void meat() {
System.out.println("牛肉汉堡");
}
}
芝士鸡肉汉堡类CheeseChickenHamburger.java
package cn.itxs.principle.crp;
public class CheeseChickenHamburger extends ChickenHamburger{
public void burden(){
System.out.println("芝士");
super.meat();
}
}
黄油鸡肉汉堡类ButterChickenHamburger.java
package cn.itxs.principle.crp;
public class ButterChickenHamburger extends ChickenHamburger{
public void burden(){
System.out.println("黄油");
super.meat();
}
}
芝士牛肉汉堡类CheeseBeefHamburger.java
package cn.itxs.principle.crp;
public class CheeseBeefHamburger extends BeefHamburger{
public void burden(){
System.out.println("芝士");
super.meat();
}
}
黄油牛肉汉堡类ButterBeefHamburger.java
package cn.itxs.principle.crp;
public class ButterBeefHamburger extends BeefHamburger{
public void burden(){
System.out.println("黄油");
super.meat();
}
}
测试类,输出芝士牛肉汉堡
package cn.itxs.principle.crp;
public class CRPMain {
public static void main(String[] args) {
CheeseBeefHamburger hamburger = new CheeseBeefHamburger();
hamburger.burden();
}
}
上图可以看出继承关系实现产生了大量的子类,而且增加了新的肉类或是颜色都要修改源码,这违背了开闭原则,显然不可取,但如果改用组合关系实现就很好的解决了以上问题,图例
采用组合或聚合复用方式,第一步先将将配料Burden抽象为接口,并实现黄油,芝士两个配料类,第二步将Burden对象组合在汉堡类Hamburger中,最终我们用更少的类就可以实现上面的功能(ButterChickenHamburger、ButterBeefHamburger、CheeseChickenHamburger、CheeseBeefHamburger);而且以后当增加猪肉、虾肉汉堡或者新增加奶酪配料都不需要修改原来的代码,只要增加对应的实现类即可,符合开闭原则。
Burden接口Burden.java
package cn.itxs.principle.crp;
public interface Burden {
void burdenKind();
}
芝士类Cheese.java
package cn.itxs.principle.crp;
public class Cheese implements Burden{
@Override
public void burdenKind() {
System.out.println("芝士");
}
}
黄油类Cheese.java
package cn.itxs.principle.crp;
public class Butter implements Burden{
@Override
public void burdenKind() {
System.out.println("黄油");
}
}
汉堡抽象类Hamburger.java
package cn.itxs.principle.crp;
public abstract class Hamburger {
abstract void meat();
private Burden burden;
public Burden getBurden() {
return burden;
}
public void setBurden(Burden burden) {
this.burden = burden;
}
}
测试类同样输出芝士牛肉汉堡
package cn.itxs.principle.crp;
public class CRPMain {
public static void main(String[] args) {
BeefHamburger beefHamburger = new BeefHamburger();
Burden cheese = new Cheese();
beefHamburger.setBurden(cheese);
beefHamburger.getBurden().burdenKind();
beefHamburger.meat();
}
}
短视频类Video.java
package cn.itxs.principle.lod;
public class Video {
private String title;
public Video(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
抖音app类App.java
package cn.itxs.principle.lod;
public class App {
public void play(Video video){
System.out.println("播放短视频,标题为"+video.getTitle());
}
}
手机类Phone.java
package cn.itxs.principle.lod;
public class Phone {
private App app = new App();
private Video video = new Video("大话设计模式");
public void playVideo(){
app.play(video);
}
}
测试类,输出“播放短视频,标题为大话设计模式”
package cn.itxs.principle.lod;
public class LODMain {
public static void main(String[] args) {
Phone phone = new Phone();
phone.playVideo();
}
}
短视频和抖音app对象都在手机上,手机和短视频是没有太大联系,短视频对象不应该在手机类里面。应该修改为手机里面有抖音APP,抖音APP里面有短视频,这才符合迪米特法则;手机和抖音APP是朋友,抖音APP和短视频是朋友,在软件设计里朋友的朋友不是朋友,也就是手机和短视频不是朋友,所以它们不应该有直接交集。
其他不变,只修改抖音App和Phone类,抖音App类App.java
package cn.itxs.principle.lod;
public class App {
private Video video = new Video("大话设计模式");
public void play(){
System.out.println("播放短视频,标题为"+video.getTitle());
}
}
手机类Phone.java
package cn.itxs.principle.lod;
public class Phone {
private App app = new App();
public void playVideo(){
app.play();
}
}
设计模式是经过大量实践和归纳总结的范式,是可以直接运用于软件开发中,设计模式是一种风格,从某种意义上不止上面23种,比如像MVC也可算是一个设计模式。
package cn.itxs.pattern.singleton;
public class HungerSingleton {
private static final HungerSingleton instance = new HungerSingleton();
private HungerSingleton(){
}
public HungerSingleton getInstance(){
return instance;
}
}
package cn.itxs.pattern.singleton;
public class DoubleCheckSingleton {
//volatile有两个作用,其一保证instance的可见性,其二禁止jvm指令重排序优化
private static volatile DoubleCheckSingleton instance = null;
private DoubleCheckSingleton(){
}
public static DoubleCheckSingleton getInstance(){
//第一个if主要用于非第一次创建单例后可以直接返回的性能优化
if (instance == null){
//采用同步代码块保证线程安全
synchronized (DoubleCheckSingleton.class){
if (instance == null){
//这一行jvm内部执行多步,1先申请堆内存,2对象初始化,3对象指向内存地址;2和3由于jvm有指令重排序优化所以存在3先执行可能会导致instance还没有初始化完成,其他线程就得到了这个instance不完整单例对象的引用值而报错。
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
** 静态内部类的方式满足懒加载,没有线程安全问题,同时也十分高效,代码也容易让人理解**
package cn.itxs.pattern.singleton;
public class InnerSingleton {
private InnerSingleton(){
}
private static class SingletonHolder{
private static final InnerSingleton instance = new InnerSingleton();
}
public static InnerSingleton getInstance(){
return SingletonHolder.instance;
}
}
不管是上面的饿汉式、懒汉式的双层检测锁、静态内部类也不能阻止反射攻击导致单例被破坏,虽然我们一开始都对构造器进行了私有化处理,但Java本身的反射机制却还是可以将private访问权限改为可访问,依旧可以创建出新的实例对象而破坏单例。测试类如下
package cn.itxs.pattern.singleton;
import java.lang.reflect.Constructor;
public class SingletonMain {
public static void main(String[] args) {
//反射攻击
reflectAttack();
}
public static void reflectAttack() {
System.out.println("正常单例获取对象");
InnerSingleton instanceA = InnerSingleton.getInstance();
System.out.println(instanceA);
InnerSingleton instanceB = InnerSingleton.getInstance();
System.out.println(instanceB);
Constructor constructor = null;
try {
constructor = InnerSingleton.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
InnerSingleton instanceC = constructor.newInstance();
System.out.println("反射攻击后单例获取对象");
System.out.println(instanceC);
} catch (Exception e) {
e.printStackTrace();
}
}
}
饿汉式、懒汉式的双层检测锁、静态内部类如果都实现序列化接口,也会被序列化和反序列化破坏单例。
在DoubleCheckSingleton.java中增加实现Serializable接口
测试类代码如下:
package cn.itxs.pattern.singleton;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SingletonMain {
public static void main(String[] args) {
//序列化和反序列化破坏
serializeDestroy();
}
public static void serializeDestroy() {
System.out.println("正常单例获取对象");
DoubleCheckSingleton instanceA = DoubleCheckSingleton.getInstance();
System.out.println(instanceA);
DoubleCheckSingleton instanceB = DoubleCheckSingleton.getInstance();
System.out.println(instanceB);
try {
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(instanceB);
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
DoubleCheckSingleton instanceC = (DoubleCheckSingleton) ois.readObject();
System.out.println("序列化破坏后单例获取对象");
System.out.println(instanceC);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里如何应对序列化和反序列化破坏,只需要在DoubleCheckSingleton.java进行小小的修改 ,添加一个方法readResolve()
再次运行测试类输出后三个单例获取对象都是相同了
**枚举是最简洁、线程安全、不会被反射创建实例的单例实现,《Effective Java》中也表明了这种写法是最佳的单例实现模式。从Constructor构造器中反射实例化对象方法newInstance的源码可知:反射禁止了枚举对象的实例化,也就防止了反射攻击,不用自己在构造器实现复杂的重复实例化逻辑了。 **
EnumSingleton.java
package cn.itxs.pattern.singleton;
public enum EnumSingleton {
INSTANCE;
}
测试类
package cn.itxs.pattern.singleton;
import java.lang.reflect.Constructor;
public class SingletonMain {
public static void main(String[] args) {
//反射攻击
reflectAttack();
}
public static void reflectAttack() {
System.out.println("正常单例获取对象");
EnumSingleton instanceA = EnumSingleton.INSTANCE;
System.out.println(instanceA);
EnumSingleton instanceB = EnumSingleton.INSTANCE;
System.out.println(instanceB);
Constructor constructor = null;
try {
constructor = EnumSingleton.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
EnumSingleton instanceC = constructor.newInstance();
System.out.println("反射攻击后单例获取对象");
System.out.println(instanceC);
} catch (Exception e) {
e.printStackTrace();
}
}
}
游戏抽象类Game.java
package cn.itxs.pattern.template;
public abstract class Game {
public final void play(){
init();
startPlay();
endPlay();
}
//初始化游戏
abstract void init();
//开始游戏
abstract void startPlay();
//结束游戏
abstract void endPlay();
}
英雄联盟游戏类LoL.java
package cn.itxs.pattern.template;
public class LoL extends Game{
@Override
void init() {
System.out.println("英雄联盟手游初始化");
}
@Override
void startPlay() {
System.out.println("英雄联盟手游激烈进行中");
}
@Override
void endPlay() {
System.out.println("英雄联盟手游游戏结束,恭喜三连胜");
}
}
和平精英游戏类Game.java
package cn.itxs.pattern.template;
public class Peace extends Game{
@Override
void init() {
System.out.println("和平精英手游初始化");
}
@Override
void startPlay() {
System.out.println("和平精英手游激烈进行中");
}
@Override
void endPlay() {
System.out.println("和平精英手游游戏结束,大吉大利今晚吃鸡");
}
}
测试类
package cn.itxs.pattern.template;
public class TemplateMain {
public static void main(String[] args) {
//玩英雄联盟游戏
Game lol = new LoL();
lol.play();
//玩和平精英游戏
Game peace = new Peace();
peace.play();
}
}
留言与评论(共有 0 条评论) “” |