【通用开发】设计模式

本文介绍了Java项目开发中最常见的几种设计模式
高内聚&低耦合
这是最常见的一个概念,也是各个设计模式的最基本的原则。
高内聚
模块内部的各个类,要实现紧密连接,功能上高度的相互配合。按照内聚程度从低到高主要有以下几种:
- 偶然内聚:一个模块内的各处理元素之间没有任何联系,只是偶然地被凑到一起。这种模块也称为巧合内聚,内聚程度最低。
- 逻辑内聚:这种模块把几种相关的功能组合在一起, 每次被调用时,由传送给模块参数来确定该模块应完成哪一种功能 。
- 时间内聚:把需要同时执行的动作组合在一起形成的模块称为时间内聚模块。
- 过程内聚:构件或者操作的组合方式是,允许在调用前面的构件或操作之后,马上调用后面的构件或操作,即使两者之间没有数据进行传递。简单的说就是如果一个模块内的处理元素是相关的,而且必须以特定次序执行则称为过程内聚。
- 例如某要完成登录的功能,前一个功能判断网络状态,后一个执行登录操作,显然是按照特 定次序执行的。
- 通信内聚:指模块内所有处理元素都在同一个数据结构上操作或所有处理功能都通过公用数据而发生关联(有时称之为信息内聚)。即指模块内各个组成部分都使用相同的数据结构或产生相同的数据结构。
- 顺序内聚:一个模块中各个处理元素和同一个功能密切相关,而且这些处理必须顺序执行,通常前一个处理元素的输出时后一个处理元素的输入。
- 例如某要完成获取订单信息的功能,前一个功能获取用户信息,后一个执行计算均价操作,显然该模块内两部分紧密关联。
- 顺序内聚的内聚度比较高,但缺点是不如功能内聚易于维护。
- 功能内聚:模块内所有元素的各个组成部分全部都为完成同一个功能而存在,共同完成一个单一的功能,模块已不可再分。即模块仅包括为完成某个功能所必须的所有成分,这些成分紧密联系、缺一不可。
低耦合
耦合,即模块间的关联程度,模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差。
按照耦合程度从低到高排布如下:
- 非直接耦合:两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。耦合度最弱,模块独立性最强。
- 数据耦合:调用模块和被调用模块之间只传递简单的数据项参数。相当于高级语言中的值传递。
- 标记耦合:调用模块和被调用模块之间传递数据结构而不是简单数据,同时也称作特征耦合。表就和的模块间传递的不是简单变量,而是像高级语言中的数据名、记录名和文件名等数据结果,这些名字即为标记,其实传递的是地址。
- 控制耦合:模块之间传递的不是数据信息,而是控制信息例如标志、开关量等,一个模块控制了另一个模块的功能。
- 外部耦合:一组模块都访问同一全局简单变量,而且不通过参数表传递该全局变量的信息,则称之为外部耦合。
- 公共耦合:一组模块都访问同一个全局数据结构,则称之为公共耦合。公共数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。如果模块只是向公共数据环境输入数据,或是只从公共数据环境取出数据,这属于比较松散的公共耦合;如果模块既向公共数据环境输入数据又从公共数据环境取出数据,这属于较紧密的公共耦合。公共耦合会引起以下问题:
无法控制各个模块对公共数据的存取,严重影响了软件模块的可靠性和适应性。 使软件的可维护性变差。若一个模块修改了公共数据,则会影响相关模块。 降低了软件的可理解性。不容易清楚知道哪些数据被哪些模块所共享,排错困难。 一般地,仅当模块间共享的数据很多且通过参数传递很不方便时,才使用公共耦合。
- 内容耦合:一个模块直接访问另一模块的内容,则称这两个模块为内容耦合。若在程序中出现下列情况之一,则说明两个模块之间发生了内容耦合:
一个模块直接访问另一个模块的内部数据。 一个模块不通过正常入口而直接转入到另一个模块的内部。 两个模块有一部分代码重叠(该部分代码具有一定的独立功能)。 一个模块有多个入口。 内容耦合可能在汇编语言中出现。大多数高级语言都已设计成不允许出现内容耦合。这种耦合的耦合性最强,模块独立性最弱。
六大设计原则
- 单一职责原则 定义:就一个类而言,应该仅有一个引起它变化的原因。
- 开放封闭原则 定义:类、模块、函数等应该是可以拓展的,但是不可修改。
- 里氏替换原则 定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。
- 依赖倒置原则 定义:高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。模块间的依赖通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合。
- 迪米特原则 定义:一个软件实体应当尽可能少地与其他实体发生相互作用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
- 接口隔离原则 定义:一个类对另一个类的依赖应该建立在最小的接口上。建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少。
创建型设计模式
单例模式
一、饿汉单例模式,实例随类初始化一起加载,无线程安全问题。特点是类加载慢,访问速度快。如果未使用,就会有内存浪费。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
二、懒汉线程安全模式,保证线程安全,需要时才进行对象实例化,但是每次获取实例都需要进行同步,也会增大开销。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getinstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
三、静态内部单例模式,只有第一次调用getinstance方法时才会加载holder类,初始化instance。
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
四、枚举单例,默认线程安全。
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
杜绝反序列化生成另一个实例,可以将readResolve()返回值设为singleton对象。
使用场景
- 工具类
- 项目共享全局资源
- 实现I/O或者数据库等资源的操作
工厂模式
简单工厂模式,用来说明工厂方法模式。
- Factory:工厂类,这是简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
- IProduct:抽象产品类,这是简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
- Product:具体产品类,这是简单工厂模式的创建目标。 由工厂类,根据使用者的参数来决定创建哪一个产品实现类:
public class ComputerFactory {
public static Computer createComputer(String type) {
Computer mComputer = null;
switch (type) {
case "lenovo":
mComputer = new LenovoComputer();
break;
case "hp":
mComputer = new HpComputer();
break;
case "asus":
mComputer = new AsusComputer();
break;
}
return mComputer;
}
}
缺点,每次需要新增一个产品实现类时,都需要去修改工厂类里的方法。
工厂方法模式
ConcreateFactory类里通过反射来创建对应的产品类,具体创建哪一个由调用方去传入Class来确定。
public class GDComputerFactor extends ComputerFactory {
@Override
public <T extends Computer> T createComputer(Class<T> clz) {
Computer computer = null;
String classname = clz.getName();
try {
//通过反射来生产不同厂家的计算机
computer = (Computer) Class.forName(classname).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return (T) computer;
}
}
建造者模式
使用场景:
- 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
- 相同的方法,不同的执行顺序,产生不同的事件结果时。
- 多个部件或零件都可以被装配到一个对象中,但是产生的运行结果又不相同时。
- 产品类非常复杂,或者产品类中的调用顺序不同而产生了不同的效能。
- 在创建一些复杂的对象时,这些对象的内部组成构件间的建造顺序是稳定的,但是对象的内部组成构件面临着复杂的变化。
核心为Builder实现类,里面由Director导演类来调用来确定建造类的参数,最后调用create()创建一个对象。这个过程对外部不可见。
public class MoonComputerBuilder extends Builder {
private Computer mComputer = new Computer();
@Override
public void buildCpu(String cpu) {
mComputer.setmCpu(cpu);
}
@Override
public void buildMainboard(String mainboard) {
mComputer.setmMainboard(mainboard);
}
@Override
public void buildRam(String ram) {
mComputer.setmRam(ram);
}
@Override
public Computer create() {
return mComputer;
}
}
优点:
使用建造者模式可以使客户端不必知道产品内部组成的细节。具体的建造者类之间是相互独立的,容易扩展。由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。
缺点:
产生多余的Build对象以及导演类。
结构型设计模式
代理模式
在代理模式中,存在三个角色:
- 抽象主题(Subject):定义了真实主题和代理主题的公共接口,这样在任何使用真实主题的地方都可以使用代理主题。
- 真实主题(RealSubject):是实际需要被代理的对象,它定义了具体的业务逻辑。
- 代理主题(Proxy):持有对真实主题的引用,并可以在调用真实主题的方法前后添加额外的逻辑。 示例代码:
interface Service {
void doService();
}
class RealService implements Service {
@Override
public void doService() {
System.out.println("执行真实服务");
}
}
class StaticProxy implements Service {
private Service realService;
public StaticProxy(Service realService) {
this.realService = realService;
}
@Override
public void doService() {
System.out.println("在执行服务前的额外操作");
realService.doService();
System.out.println("在执行服务后的额外操作");
}
}
public class StaticProxyDemo {
public static void main(String[] args) {
Service realService = new RealService();
Service proxy = new StaticProxy(realService);
proxy.doService();
}
}
在上述代码中:
- 定义了一个Service接口,RealService类实现了该接口。
- StaticProxy类也实现了Service接口,作为静态代理类。它持有一个Service类型的对象realService,在doService方法中可以在调用真实对象的方法前后添加额外的操作。
- 在main方法中创建了真实服务对象,并将其传递给代理对象,然后调用代理对象的方法。
动态代理
上面是静态代理,即在编译的时候,代理类的class文件就确定了。动态代理则是在运行时通过反射来动态生成代理类对象。Java 提供了动态的代理接口 InvocationHandler,实现该接口需要重写 invoke() 方法。
object DynamixProxyDemo {
interface IShop {
fun buy(p:Int)
}
class RealShop : IShop {
override fun buy(number:Int) {
Log.i(GLOBAL_TAG, "DynamicProxy RealShop: BUY $number HAT!")
}
}
class DynamicShopProxy(private val shop: IShop) : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?) =
method?.invoke(shop, args?.get(0) as Int)
}
fun entrance() {
val realShop = RealShop()
val shopProxy: IShop = Proxy.newProxyInstance(
realShop.javaClass.classLoader,
Array(1) { IShop::class.java },
DynamicShopProxy(realShop)
) as IShop
shopProxy.buy(5)
}
}
通过Proxy.newProxyInstance来生成动态代理类,强转调用disomething方法,会调用到DynamicProxyHandler的invoke方法。需要注意参数传递,有参无参,类型转换。
使用场景和优点
- 远程代理:为一个对象在不同的地址空间提供局部代表,这样系统可以将 Server 部分的实现隐藏。
- 虚拟代理:使用一个代理对象表示一个十分耗费资源的对象并在真正需要时才创建。
- 安全代理:用来控制真实对象访问时的权限。一般用于真实对象有不同的访问权限时。
- 智能指引:当调用真实的对象时,代理处理另外一些事,比如计算真实对象的引用计数,当该对象没有引用时,可以自动释放它;或者访问一个实际对象时,检查是否已经能够锁定它,以确保其他对象不能改变它。
代理模式的优点
- 真实主题类就是实现实际的业务逻辑,不用关心其他非本职的工作。
- 真实主题类随时都会发生变化;但是因为它实现了公共的接口,所以代理类可以不做任何修改就能够使用。
装饰模式
大体和代理模式类似,但是装饰者在调用实现类的时候,可以插入自己实现的其他方法。
- Component:抽象组件,可以是接口或是抽象类,被装饰的最原始的对象。
- ConcreteComponent:组件具体实现类。Component的具体实现类,被装饰的具体对象。
- Decorator:抽象装饰者,从外类来拓展Component类的功能,但对于Component来说无须知道Decorator的存在。在它的属性中必然有一个private变量指向Component抽象组件。
- ConcreteDecorator:装饰者的具体实现类。
抽象装饰者:
public abstract class Master extends Swordsman {
private Swordsman mSwordsman;
public Master(Swordsman mSwordsman) {
this.mSwordsman = mSwordsman;
}
@override
public void attackMagic() {
mSwordsman.attackMagic();
}
}
想扩展组件实现类的装饰者:
public class HongQiGong extends Master {
public HongQiGong(Swordsman mSwordsman) {
super(mSwordsman);
}
public void teachAttackMagic() {
System.out.println("洪七公教授打狗棒法");
System.out.println("杨过使用打狗棒法");
}
@override
public void attackMagic() {
super.attackMagic();
teachAttackMagic();
}
}
优点:
- 通过组合而非继承的方式,动态地扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。
- 有效避免了使用继承的方式扩展对象功能而带来的灵活性差、子类无限制扩张的问题。
- 具体组件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体组件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开放封闭原则”。
缺点:
- 因为所有对象均继承于Component,所以如果Component内部结构发生改变,则不可避免地影响所有子类(装饰者和被装饰者)。如果基类改变,则势必影响对象的内部。
- 比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。所以,只在必要的时候使用装饰模式。
- 装饰层数不能过多,否则会影响效率。
Kotlin的扩展函数是一种更为方便的实现方式。从语言层面,生成后缀加Kt的Java类,里面生成对应的static函数。
外观模式
- Facade:外观类,知道哪些子系统类负责处理请求,将客户端的请求代理给适当的子系统对象。
- Subsystem:子系统类,可以有一个或者多个子系统。实现子系统的功能,处理外观类指派的任务,注意子系统类不含有外观类的引用。
实际上就是对其他类的封装,多次调用综合成一个方法。
/**
* 外观类张无忌
*/
public class ZhangWuji {
private JingMai jingMai;
private ZhaoShi zhaoShi;
private NeiGong neiGong;
public ZhangWuJi() {
jingMai = new JingMai();
zhaoShi = new ZhaoShi();
neiGong = new NeiGong();
}
// 使用乾坤大挪移
public void Qiankun() {
jingMai.jingmai();//开启经脉
neiGong.Qiankun();//使用内功乾坤大挪移
}
//使用七伤拳
public void QiShang() {
jingMai.jingmai();//开启经脉
neiGong.JiuYang();//使用内功九阳神功
zhaoShi.QiShangQuan();//使用招式七伤拳
}
}
张无忌使用的一些技能,综合了其他子系统类的若干个其他方法。
优点:
- 减少系统的相互依赖,所有的依赖都是对外观类的依赖,与子系统无关。
- 对用户隐藏了子系统的具体实现,减少用户对子系统的耦合。这样即使具体的子系统发生了变化,用户也不会感知到。
- 加强了安全性,子系统中的方法如果不在外观类中开通,就无法访问到子系统中的方法。
缺点:
- 不符合开放封闭原则。如果业务出现变更,则可能要直接修改外观类。
享元模式
使用共享对象有效地支持大量细粒度的对象。要求细粒度对象,那么不可避免地使得对象数量多且性质相近。
这些对象分为两个部分:内部状态和外部状态。
内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变;而外部状态是对象依赖的一个标记,它是随环境改变而改变的并且不可共享的状态。
例如商城系统,商品对象维护一份即可,不随每个客户端请求查看而重新创建一个。
public class Goods implements IGoods {
private String name;//名称
private String version;//版本
Goods(String name) {
this.name = name;
}
@Override
public void showGoodsPrice(String version) {
if (version.equals("32G")) {
System.out.println("价格为5199元");
} else if (version.equals("128G")) {
System.out.println("价格为5999元");
}
}
}
name为内部状态,用来标记自身,不变化。version为外部状态,由外部传入,实时变化。
工厂:
public class GoodsFactory {
private static Map<String, Goods> pool = new HashMap<String, Goods>();
public static Goods getGoods(String name) {
if (pool.containsKey(name)) {
System.out.println("使用缓存,key为:" + name);
return pool.get(name);
} else {
Goods goods = new Goods(name);
pool.put(name, goods);
System.out.println("创建商品,key为:" + name);
return goods;
}
}
}
pool里符合要求的对象已存在,直接复用,没有才创建。
使用场景:
- 系统中存在大量的相似对象。
- 需要缓冲池的场景。
行为型设计模式
策略模式
当我们写代码时总会遇到一种情况,就是我们会有很多的选择,由此衍生出很多的if…else,或者case。
如果每个条件语句中包含了一个简单的逻辑,那还比较容易处理;但如果在一个条件语句中又包含了多个条件语句,就会使得代码变得臃肿,维护的成本也会加大,这显然违背了开放封闭原则。
策略模式是一种行为设计模式,它定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
demo:
public class ZhangWuji {
public static void main(String[] args) {
Context context;
//张无忌遇到对手宋青书,采用对较弱对手的策略
context = new Context(new WeakRivalStrategy());
context.fighting();
//张无忌遇到对手灭绝师太,采用对普通对手的策略
context = new Context(new CommonRivalsticategy());
context.fighting();
//张无忌遇到对手成昆,采用对强大对手的策略
context = new Context(new StrongRivalStrcategy());
context.fighting();
}
}
策略模式的使用场景和优缺点
- 使用场景:
对客户隐藏具体策略(算法)的实现细节,彼此完全独立。 针对同一类型问题的多种处理方式,仅仅是具体行为有差别时。 在一个类中定义了很多行为,而且这些行为在这个类里的操作以多个条件语句的形式出现。策略模式将相关的条件分支移入它们各自的 Strategy 类中,以代替这些条件语句。
- 优点:
使用策略模式可以避免使用多重条件语句。多重条件语句不易维护,而且易出错。 易于拓展。当需要添加一个策略时,只需要实现接口就可以了。
- 缺点:
每一个策略都是一个类,复用性小。如果策略过多,类的数量会增多。上层模块必须知道有哪些策略,才能够使用这些策略,这与迪米特原则相违背。
模板方法模式
在软件开发中,有时会遇到类似的情况:某个方法的实现需要多个步骤,其中有些步骤是固定的;而有些步骤并不固定,存在可变性。为了提高代码的复用性和系统的灵活性,可以使用模板方法模式来应对这类情况。
public abstract class AbstractSwordsman {
//该方法为final,防止算法框架被覆写
public final void fighting() {
//运行内功,抽象方法
neigong();
//调整经脉,具体方法
meridian();
//如果有武器,则准备武器
if (hasWeapons()) {//2
weapons();
}
//使用招式
moves();
//钩子方法
hook();//1
}
//空实现方法
protected void hook() {
}
protected abstract void neigong();
protected abstract void weapons();
protected abstract void moves();
protected void meridian() {
System.out.println("开启正经与奇经");
}
/**
* 是否有武器,默认是有武器的,钩子方法
*
* @return
*/
protected boolean hasWeapons() {
return true;
}
}
定义:定义一个操作中的算法框架,而将一些步骤延迟到子类中,使得子类不改变一个算法的结构即可重定义算法的某些特定步骤。
这个抽象类包含了3种类型的方法,分别是抽象方法、具体方法和钩子方法。抽象方法是交由子类去实现的,具体方法则是父类实现了子类公共的方法。在上面的例子中就是武侠开启经脉的方式都一样,所以就在具体方法中实现。
钩子方法则分为两类:第一类在上面代码注释1 处,它有一个空实现的方法,子类可以视情况来决定是否要覆盖它;第二类在注释 2 处,这类钩子方法的返回类型通常是 boolean 类型的,其一般用于对某个条件进行判断,如果条件满足则执行某一步骤,否则将不执行。
观察者模式
观察者模式又被称为发布-订阅模式,属于行为型设计模式的一种,是一个在项目中经常使用的模式。
它的定义如下。
定义:定义对象间一种一对多的依赖关系,每当一个对象改变状态时,则所有依赖于它的对象都会得到通知并被自动更新。
- Subject:抽象主题(抽象被观察者)。抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject:具体主题(具体被观察者)。该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer:抽象观察者,是观察者的抽象类。它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
public class Client {
public static void main(String[] args) {
SubscriptionSubject mSubscriptionSubject = new SubscriptionSubject();
//创建微信用户
Weixinuser userl = new WeixinUser("杨影枫");
WeixinUser user2 = new WeixinUser("月眉儿");
WeixinUser user3 = new WeixinUser("紫轩");
//订阅公众号
mSubscriptionSubject.attach(userl);
mSubscriptionSubject.attach(user2);
mSubscriptionSubject.attach(user3);
//公众号更新发出消息给订阅的微信用户
mSubscriptionSubject.notify("刘望舒的专栏更新了");
}
}
被观察者维护一个列表,用于添加删除观察者,在变化时给所有的观察者发送通知。
观察者模式的使用场景和优缺点
- 使用场景:
关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。 事件多级触发场景。跨系统的消息交换场景,如消息队列、事件总线的处理机制。
- 优点:
观察者和被观察者之间是抽象耦合,容易扩展。方便建立一套触发机制。
- 缺点:
在应用观察者模式时需要考虑一下开发效率和运行效率的问题。程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在 Java 中消息的通知一般是顺序执行的,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步方式。
小结
需要注意的是学习设计模式最忌讳生搬硬套,为了设计模式而设计。设计模式主要解决的问题就是设计模式的六大原则,只要我们设计的代码遵循这六大原则,那么就是优秀的代码。