Android开发学习笔记之设计模式——组合模式&享元模式&外观模式
组合模式
概述
组合模式是一种结构型模式,相对而言比较简单,通常用于设计一些呈树状结构的对象模型,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性。
组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点,总而言之我们就可以将其看作是一颗树,其每个节点都可以使用统一接口定义,看作是相同对象,如此一来,我们就无须关注每个节点是什么对象,而是统一处理。
比如,在实际开发中我们需要开发一个商城应用,商城中对应了各种商品,而且还可以购买礼包,礼包中又包含了各种商品,此时礼包实际上也是作为一种商品存在的,我们可以与正常商品继承自统一接口,这样我们在处理购买时就无需区分是正常商品还是礼包了。
应用场景
- 当我们需要使用树状对象结构时,可以使用组合模式,它提供了两种共享公共接口的基本元素类型: 简单叶节点和复杂容器。 容器中可以包含叶节点和其他容器。 这使得我们可以构建树状嵌套递归对象结构。
- 当我们希望隐藏简单对象和复杂组合对象之间的不同,使用相同的处理方式处理两类对象时,我们可以使用组合模式,提供统一的接口。
优缺点
优点:
- 可以利用多态和递归机制更方便地使用复杂树结构。
- 无需更改现有代码, 就可以在应用中添加新元素, 使其成为对象树的一部分。符合“开闭原则"。
缺点:
- 增加了系统结构设计的复杂度,对于功能差异较大的类, 提供公共接口或许会有困难而且会造成影响理解。
实现
组合模式的结构非常简单,基本可以分为包括:抽象组件,叶节点组件以及容器组件,具体如下:
- 抽象组件:它的主要作用是为叶节点组件和容器组件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由容器组件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
- 叶节点组件:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。
- 容器(树枝节点)组件:组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
其结构图如下:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b6713d2c1c25bb42ad52cd862ec3f0e8.png)
组合模式其实现可以分为透明式和安全式。两种模式其实差别不大,其区别只是对于抽象部件中接口的声明,其中透明模式在抽象组件将叶节点和容器中的接口都声明了,此时叶节点和容器对于客户端是完全透明的,没有任何区别,但是存在着安全隐患,因为实际上两类组件的接口并不通用;而安全模式只在抽象组件中声明了通用接口,此时客户端是需要区分两类组件的,因此不能做到完全透明,但是更为安全也更加符合逻辑上的设计。因此,我们通常都使用安全模式。
如上述开发场景,我们可以创建商品接口Goods,商品可能存在包含书籍、日用品等各类商品,我们可以将其作为不同子类,也可以作为统一子类ActualGoods,礼包也作为一类商品GiftPack实现Goods接口,其中可能存在多个子节点。
首先,我们创建抽象组件Goods,如下所示:
/**
* 抽象组件
*/
interface Goods {
val price: Int
val type: String
/**
* 公共接口
*/
fun discount() : Int
}
然后,创建叶节点组件ActualGoods,其中包含商品的基本信息和特定接口,如下所示:
/**
* 叶节点组件
*/
class ActualGoods(override val price: Int, override val type: String) : Goods{
/**
* 公共接口实际实现
*/
override fun discount() : Int{
return (price*0.8).toInt()
}
}
最后,我们创建容器组件GiftPack,其中除了包含作为商品的基本信息外,还存在一个商品列表,存储礼包中包含的商品,而且其中还可能存在对于子节点的操作,我们可以通过递归的方式来遍历树结构的所有叶子节点,如下:
/**
* 容器组件
*/
class GiftPack(override val price: Int, override val type: String) : Goods{
/**
* 容器中包含的子节点,注意此时应该使用基类Goods,而不是ActualGoods,因为礼包中也可能包含礼包
*/
var goodsList = mutableListOf<Goods>()
private set
/**
* 容器组件的公共接口实现
*/
override fun discount(): Int {
return (price*0.9).toInt()
}
/**
* 添加子节点
*/
fun addGoods(goods: Goods){
goodsList.add(goods)
}
/**
* 移除子节点
*/
fun removeGoods(goods: Goods){
goodsList.remove(goods)
}
/**
* 获取所有商品,可以通过递归获取树结构的所有叶子节点
*/
fun getAllGoods() : MutableList<Goods>{
val list = mutableListOf<Goods>()
goodsList.forEach {
when(it){
is ActualGoods -> list.add(it)
is GiftPack -> list.addAll(it.getAllGoods())
else -> {}
}
}
return list
}
}
组合模式非常简单,其实际上就是一个树状结构,需要注意的是,在容器组件和客户端中,我们应该尽量使用基类来操作树中的节点。
享元模式
概述
根据面向对象的设计思想,万事万物皆对象,在我们设计开发中,随着系统的日益复杂,对象数量也越来越多,而每个对象都是会消耗内存的,经过存在GC机制,但是总是可能存在对象数量过多而导致内存消耗过多,内存不足,导致OOM等问题。因此,在我们设计开发过程中,我们应该尽可能地减少一次性创建过多对象,比如使用缓存等机制。在实际开发中,还可能存在一种情况,那就是某一类对象中存在大量地重复属性,从而需要创建大量地重复对象,此时,我们就能够使用享元模式来共享相同地对象,实现对象地复用,从而减少内存地消耗。
享元模式即运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。其本质在于缓存共享对象,降低内存消耗。
要了解享元模式,首先,我们需要了解内部状态和外部状态,具体如下:
- 内部状态:对象共享出来的信息,存储在享元信息内部,并且不会随环境的改变而改变;
- 外部状态:对象得以依赖的一个标记,随环境的改变而改变,不可共享。
享元模式的定义提出了两个要求:细粒度和共享对象,我们知道享元模式其本质就是共享对象,那么应该共享哪些对象呢?应该怎么划分呢?享元模式要求,将对象中不可改变的内部状态单独抽出来作为享元对象,共享同一个对象,而对于会随着外部环境改变的外部状态,享元模式规定必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。
享元模式建议不在对象中存储外在状态, 而是将其传递给依赖于它的一个特殊方法。 程序只在对象中保存内在状态, 以方便在不同情景下重用。 这些对象的区别仅在于其内在状态 (与外在状态相比, 内在状态的变体要少很多), 因此你所需的对象数量会大大削减。
比如,我们需要做一个围棋的游戏,其中每个棋子的颜色等外观状态都是确定后就不会改变的属于内部状态,而其位置属于外部状态,此时我们应该共享其外观信息作为享元对象。
应用场景
- 当系统中存在大量的对象,而且这些对象都存在很多重复属性可以按照内部状态分组并消耗内存较多时,我们可以使用享元模式。
优缺点
优点:
- 可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
- 外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
缺点:
- 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化;
- 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
实现
对于享元模式的实现,从上述的描述中,我们知道其根本在于分离内部状态和外部状态并将内部状态作为享元对象共享,而外部状态通常则通常被封装到一个情景类中。而为了实现对象的共享,我们通常会结合工厂方法模式,创建一个享元工厂,缓存享元对象。根据结构来划分,大致可以划分为以下几个部分:
- 享元:包含原始对象中部分能在多个对象中共享的状态。
- 享元工厂:对已有享元的缓存池进行管理,实现享元对象的复用。
- 情景类:包含原始对象中各不相同的外在状态。 情景与享元对象组合在一起就能表示原始对象的全部状态。
具体如下图:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5f9bdf2a2926d08a1d4a05621476a75d.png)
分析之前提出的围棋实例,在围棋中,每个棋子的外观是固定的属于内部状态,即为我们的享元对象,而棋子的位置不固定,属于外部状态。
首先,我们可以确定棋子外观为其享元对象,创建棋子装饰类PieceDecoration作为享元类,具体如下:
/**
* 享元,棋子装饰类
*/
data class PieceDecoration(
private val color: String,
private val size: Int
)
其次,我们知道棋子的位置是不固定的,属于外部状态,我们可以创建一个Point类来描述其位置,而结合棋子的享元我们就可以得知棋子的所有状态Piece,此为情景类,具体如下:
/**
* 外部状态,棋子坐标
*/
data class Point(
val x: Int,
val y: Int
)
/**
* 情景类,棋子
*/
class Piece(val decoration: PieceDecoration, val point: Point) {
fun downPiece(){
//TODO 下子,根据棋子的外观和位置在棋盘上画出棋子
}
}
最后,我们创建享元工厂PieceDecorationFactory类,如下:
/**
* 享元工厂,实现享元对象的复用
*/
class PieceDecorationFactory {
companion object {
const val TYPE_WHITE = "type_white"//白棋
const val TYPE_BLACK = "type_black"//黑棋
//缓存享元对象
private val pieceDecorationCache = mutableMapOf<String, PieceDecoration>()
/**
* 获取享元对象
*/
fun getPieceDecoration(type : String) : PieceDecoration?{
pieceDecorationCache[type]?.apply { return this }//复用享元对象
return when(type) {
TYPE_BLACK -> {
val decoration = PieceDecoration("#000000", 10)
pieceDecorationCache[type] = decoration
decoration
}
TYPE_WHITE -> {
val decoration = PieceDecoration("#ffffff", 10)
pieceDecorationCache[type] = decoration
decoration
}
else -> null
}
}
}
}
享元模式的核心就在于享元工厂对于享元对象的复用,以及对于内部状态和外部状态的区分,从而设计享元对象。注意,由于享元对象可在不同的情景中使用, 你必须确保其状态不能被修改。 享元类的状态只能由构造函数的参数进行一次性初始化, 它不能对其他对象公开其设置器或公有成员变量。
外观模式
概述
外观模式是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观模式是一种非常简单的模式,是迪米特法则的典型应用,其本质就是对于复杂系统功能进行封装,提供一个接口供客户端调用,从而隐藏系统内部实现。在我们的日常开发中,当我们在高层模块调用底层系统时,通常会对其进行一个封装,比如对于网络接口调用的封装,这实际上就是外观模式的应用,还用我们在开发中创建的大量工具类,这实际上也可以称之为外观模式。
应用场景
- 当我们需要使用一个复杂子系统时,我们可以使用外观模式提供一个直接调用接口,将对于子系统的调用等功能实现逻辑封装到里面;
- 当客户端与多个子系统之间存在很大的联系时,我们可以使用外观模式将其分离, 我们可以要求子系统仅使用外观来进行交互, 以减少子系统之间的耦合。
优缺点
优点:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
- 外观类会与各个子系统耦合比较严重。
实现
其实,从上文中的描述中,我们就可以知道外观模式的结构和实现都非常简单,我们只需要创建一个外观类,然后将实现子系统相关功能并暴露对应的接口即可。比如,当我们进行app开发时,如果需要进行视频格式转化功能时,我们可能会使用对应的格式转换库,而这个子系统的实现可能非常复杂,我们只需要创建一个外观类,将视频格式转换的功能全部封装进去,并暴露出对应的接口,使用时我们只需要调用该接口即可,而无需知道其内部功能实现。其基本结构,如下图所示:
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/58aad904158c745518c755929c6cb468.png)
根据“单一职责原则”,在开发中,我们应该将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口,同时也降低原有系统的复杂度。
为了减少资源的浪费,通常,我们可以将外观类设置为单例模式。而为了体现体现单一职责原则
,同时使代码结构更加清晰,对于不同子系统以及不同功能,我们可以创建多个不同的外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。