概述
软件模式是软件设计中常见问题的典型解决方案,是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件生存期的每一个阶段都存在着一些被认同的模式。
六大设计原则
无论何种设计模式,都是基于六大设计原则:
- 开闭原则:一个软件实体如类、模块和函数应该对修改封闭,对扩展开放。
- 里氏替换原则:子类应该可以完全替换父类。也就是说在使用继承时,只扩展新功能,而不要破坏父类原有的功能。
- 依赖倒置原则:细节应该依赖于抽象,抽象不应依赖于细节。把抽象层放在程序设计的高层,并保持稳定,程序的细节变化由低层的实现层来完成。
- 单一职责原则:一个类只做一件事,一个类应该只有一个引起它修改的原因。
- 迪米特法则:又名“最少知道原则”,一个类不应知道自己操作的类的细节,换言之,只和朋友谈话,不和朋友的朋友谈话。
- 接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
总结:六大原则中,开闭原则、里氏替换原则、依赖倒置原则 联系比较紧密,后两者是实现开闭原则重要前提,使用中通过抽象化设计具有很好的可拓展性和可维护性。
知道最少原则 可以降低耦合,减少不必要的交互,主张设计接口和类要简单易使用,将复杂的逻辑封装并提供简单易用的接口。
单一职责原则 使项目中的类和方法根据职责细分,避免单个类负担过重。职责越多,被复用的可能性就越小或使用起来越麻烦。
接口分离原则 将功能复杂的接口细分成多个特定功能的接口,只做该做的事情,降低耦合,但是细化粒度不能太细,容易导致接口过多。单一职责原则强调单个类内部根据职责细分的设计,接口分离原则强调类之间的耦合,尽量建立最小的依赖关系。
三种主要的模式类别
- 创建型模式提供创建对象的机制, 增加已有代码的灵活性和可复用性。
- 结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
- 行为模式负责对象间的高效沟通和职责委派。
创建型模式
1. 工厂方法模式
工厂方法模式也称为虚拟构造方法,其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。
问题场景举例: 假设一套物流管理应用中可以有轮船、卡车、飞机等运输工具进行运输、交付等物流功能。若一开始我们直接在轮船类中实现以上功能,程序中也已经多处调用该类的方法,很明显,程序中代码已经与该类有了很强的耦合关系,此时向程序中新增类就显得没那么方便了,体现出来的可能就是编写繁复的代码,if判断哪种运输工具后,new对应的类,再调用具体类的方法。这样一来,后续的代码可读性就会很差,耦合性也很强,与面向对象的高内聚低耦合背道而驰。
问题解决方案: 使用特殊的工厂方法代替对于对象构造函数的直接调用(即使用 new运算符)。不用担心,对象仍将通过 new运算符创建,只是该运算符改在工厂方法中调用罢了。工厂方法返回的对象通常被称作 “产品”。乍看之下,这种更改可能毫无意义:我们只是改变了程序中调用构造函数的位置而已。但是,仔细想一下,现在你可以在子类中重写工厂方法,从而改变产品类型。仅当这些产品具有共同的基类或者接口时,子类才能返回不同类型的产品,同时基类中的工厂方法还应将其返回类型声明为这一共有接口。
代码示例:
以上面说的物流应用为例,工厂类为Logistics,海上物流子类为SeaLogistics,陆地物流子类为RoadLogistics,各物流应用的功能方法为运输transport和交付delivery。
public class Logistics {
public void transport(){
// do something for transport
}
public void delivery(){
// do something for delivery
}
}
public class RoadLogistics extends Logistics{
public void transport(){
System.out.println("road transport");
}
public void delivery(){
System.out.println("road delivery");
}
}
public class SeaLogistics extends Logistics{
public void transport(){
System.out.println("sea transport");
}
public void delivery(){
System.out.println("sea delivery");
}
}
public class Main {
public static void main(String[] args){
RoadLogistics roadLogistics = new RoadLogistics();
roadLogistics.transport();
SeaLogistics seaLogistics = new SeaLogistics();
seaLogistics.transport();
}
}
优点:
-
用户只需要关心其所需产品对应的具体工厂是哪一个即可,不需要关心产品的创建细节,也不需要知道具体产品类的类名。
-
当系统中加入新产品时,不需要修改抽象工厂和抽象产品提供的接口,也无须修改客户端和其他的具体工厂和具体产品,而只要添加一个具体工厂和与其对应的具体产品就可以了,符合了开闭原则。 缺点:
-
当系统中加入新产品时,除了需要提供新的产品类之外,还要提供与其对应的具体工厂类。因此系统中类的个数将成对增加,增加了系统的复杂度。
适用场景:
- 当你在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。工厂方法将创建产品的代码与实际使用产品的代码分离,从而能在不影响其他代码的情况下扩展产品创建部分代码。例如,如果需要向应用中添加一种新产品,你只需要开发新的创建者子类,然后重写其工厂方法即可。
- 如果你希望用户能扩展你软件库或框架的内部组件,可使用工厂方法。继承可能是扩展软件库或框架默认行为的最简单方法。但是当你使用子类替代标准组件时,框架如何辨识出该子类?解决方案是将各框架中构造组件的代码集中到单个工厂方法中,并在继承该组件之外允许任何人对该方法进行重写。
- 如果你希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法。在处理大型资源密集型对象(比如数据库连接、文件系统和网络资源)时,你会经常碰到这种资源需求。
2. 抽象工厂模式
抽象工厂模式,意在创建一系列相关或者相互依赖的对象,而无需指定其具体类。其与工厂方法模式的区别在于前者作为一个方法直接挂载在抽象的creator类中的(例如:抽象披萨商店类),除了工厂方法外其他的方法都已经具体实现。而后者作为抽象方法延后到子类(即具体的披萨商店类)实现。
问题场景举例: 以家具(Furniture)商店应用为例,商店中有一系列的产品,比如桌子 Desk、沙发 Sofa,而桌子跟沙发都有不同的风格 Style,如中国风 ChineseStyle 跟北欧风 NordicStyle,客户在购买过程中,希望整套家具风格一致,此时如果以工厂方法模式进行设计,应该是 Furniture 为父类, Desk 以及 Sofa 为派生类,这样一来,客户需要各样不同风格的家具,对应的派生类方法都得做调整,很明显,这并不是我们想要的。
问题解决方案: 使用抽象工厂模式,声明抽象工厂类 Furniture,包含系列中所有产品构造方法的接口,比如createDesk、createSofa。同时定义 Desk 跟 Sofa 接口,再由实体类实现具体的风格。
代码示例:
//抽象工厂类
public interface Furniture {
Desk createDesk();
Sofa createSofa();
}
//实际工厂类:创建北欧风格家具
public class NordicFurniture implements Furniture{
@Override
public Desk createDesk() {
return new NordicDesk();
}
@Override
public Sofa createSofa() {
return new NordicSofa();
}
}
//实体工厂类:创建中国风格家具
public class ChineseFurniture implements Furniture{
@Override
public Desk createDesk() {
return new ChineseDesk();
}
@Override
public Sofa createSofa() {
return new ChineseSofa();
}
}
//抽象产品:桌子
public interface Desk {
void style();
}
//实体产品:北欧风格桌子
public class NordicDesk implements Desk{
@Override
public void style() {
System.out.println("this is Nordic style desk");
}
}
//实体产品:中国风桌子
public class ChineseDesk implements Desk{
@Override
public void style() {
System.out.println("this is Chinese style desk");
}
}
//抽象产品:沙发
public interface Sofa {
void style();
}
//实体产品:北欧风格沙发
public class NordicSofa implements Sofa{
@Override
public void style() {
System.out.println("this is Nordic style sofa");
}
}
//实体产品:中国风沙发
public class ChineseSofa implements Sofa{
@Override
public void style() {
System.out.println("this is Chinese style sofa");
}
}
//外部创建对象入口
public class Application {
private Desk desk;
private Sofa sofa;
//构造方法,接收实体风格
public Application(Furniture furniture){
desk = furniture.createDesk();
sofa = furniture.createSofa();
}
public void run(){
desk.style();
sofa.style();
}
}
优点: 具体产品在应用层代码隔离,不需要关心产品细节。只需要知道自己需要的产品是属于哪个工厂的即可 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
缺点: 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口。
适用场景: 如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望代码基于产品的具体类进行构建,在这种情况下,你可以使用抽象工厂。抽象工厂为你提供了一个接口,可用于创建每个系列产品的对象。只要代码通过该接口创建对象,那么你就不会生成与应用程序已生成的产品类型不一致的产品。
3. 单例模式
单例模式 意在保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
问题场景举例: 在日常开发数据库操作中,我们需要创建实例,然后进行频繁的数据库操作,这样一来会造成系统资源的浪费。
问题解决方案: 利用单例模式实现静态对象公用,减少实例对象的频繁创建,以达到节省CPU、节省内存的效果。
代码示例:
/**
* 双重锁校验(线程安全)
* 懒加载,只有程序中用到这样的类才会进行创建,减少获取实例的耗时
**/
public final class DataBaseLink {
private static volatile DataBaseLink dbLink;
private DataBaseLink() {
}
public static DataBaseLink getInstance(){
if(null != dbLink) return dbLink;
synchronized (DataBaseLink.class){
if (null == dbLink){
dbLink = new DataBaseLink();
}
}
return dbLink;
}
}
public class Application(){
private DataBaseLink dbLink;
public void getDbLink(){
this.dbLink.getInstance();
}
}
优点: 可以保证一个类只有一个实例,统一节点全局访问该实例的全局访问节点,仅在首次请求单例对象时对其进行初始化,节约系统资源。
缺点: 由于单例模式中没有抽象层,因此单例类很难进行扩展。对于有垃圾回收系统的语言 Java,C# 来说,如果对象长时间不被利用,则可能会被回收。那么如果这个单例持有一些数据的话,在回收后重新实例化时就不复存在了。
适用场景: 如果程序中的某个类对于所有客户端只有一个可用的实例,或者需要更加严格地控制全局变量,可以使用单例模式,因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
4. 生成器模式(建造者模式)
生成器模式 意在将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。其所完成的内容就是通过将多个简单对象通过一步步的组装构建出一个复杂对象的过程。
问题场景举例: 这里我们模拟装修公司对于设计出一些套餐装修服务的场景。 很多装修公司都会给出自家的套餐服务,一般有;欧式豪华、轻奢田园、现代简约等等,而这些套餐的后面是不同的商品的组合。例如;一级&二级吊顶、多乐士涂料、圣象地板、马可波罗地砖等等,按照不同的套餐的价格选取不同的品牌组合,最终再按照装修面积给出一个整体的报价。普通思路就是搞个构造器,接收各种各样的参数,在构造函数中if else判断处理;又或者直接成程序代码中if else判断,返回各自对应的装修报价。这样一来,程序代码非常的臃肿,后续若有新增的装修风格追加也会非常麻烦,总而言之就是代码可读性差且不好维护。
问题解决方案: 使用建造者模式,将各个商品组合拆分,各个功能抽象成一个方法,每个方法返回当前对象,后续不断的追加其他功能,一步步的组装构建出对应组合的装修报价。
建造者模式主要解决的问题是在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的过程构成;由于需求的变化,这个复杂对象的各个部分经常面临着重大的变化,但是将它们组合在一起的过程却相对稳定。这里我们会把构建的过程交给创建者类,而创建者通过使用我们的构建工具包,去构建出不同的装修套餐。
/**
* 装修物料
*/
public interface Matter {
/**
* 场景;地板、地砖、涂料、吊顶
*/
String scene();
/**
* 品牌
*/
String brand();
/**
* 型号
*/
String model();
/**
* 平米报价
*/
BigDecimal price();
/**
* 描述
*/
String desc();
}
/**
* 具体的class implements interface
**/
class ****
/**
* 菜单接口
**/
public interface IMenu {
/**
* 吊顶
*/
IMenu appendCeiling(Matter matter);
/**
* 涂料
*/
IMenu appendCoat(Matter matter);
/**
* 地板
*/
IMenu appendFloor(Matter matter);
/**
* 地砖
*/
IMenu appendTile(Matter matter);
/**
* 明细
*/
String getDetail();
}
/**
* 装修包
*/
public class DecorationPackageMenu implements IMenu {
private List<Matter> list = new ArrayList<Matter>(); // 装修清单
private BigDecimal price = BigDecimal.ZERO; // 装修价格
private BigDecimal area; // 面积
private String grade; // 装修等级;豪华欧式、轻奢田园、现代简约
private DecorationPackageMenu() {
}
public DecorationPackageMenu(Double area, String grade) {
this.area = new BigDecimal(area);
this.grade = grade;
}
public IMenu appendCeiling(Matter matter) {
list.add(matter);
price = price.add(area.multiply(new BigDecimal("0.2")).multiply(matter.price()));
return this;
}
public IMenu appendCoat(Matter matter) {
list.add(matter);
price = price.add(area.multiply(new BigDecimal("1.4")).multiply(matter.price()));
return this;
}
public IMenu appendFloor(Matter matter) {
list.add(matter);
price = price.add(area.multiply(matter.price()));
return this;
}
public IMenu appendTile(Matter matter) {
list.add(matter);
price = price.add(area.multiply(matter.price()));
return this;
}
public String getDetail() {
StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n" +
"装修清单" + "\r\n" +
"套餐等级:" + grade + "\r\n" +
"套餐价格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元\r\n" +
"房屋面积:" + area.doubleValue() + " 平米\r\n" +
"材料清单:\r\n");
for (Matter matter: list) {
detail.append(matter.scene()).append(":").append(matter.brand()).append("、").append(matter.model()).append("、平米价格:").append(matter.price()).append(" 元。\n");
}
return detail.toString();
}
}
public class Builder {
public IMenu levelOne(Double area) {
return new DecorationPackageMenu(area, "豪华欧式")
.appendCeiling(new LevelTwoCeiling()) // 吊顶,二级顶
.appendCoat(new DuluxCoat()) // 涂料,多乐士
.appendFloor(new ShengXiangFloor()); // 地板,圣象
}
public IMenu levelTwo(Double area){
return new DecorationPackageMenu(area, "轻奢田园")
.appendCeiling(new LevelTwoCeiling()) // 吊顶,二级顶
.appendCoat(new LiBangCoat()) // 涂料,立邦
.appendTile(new MarcoPoloTile()); // 地砖,马可波罗
}
public IMenu levelThree(Double area){
return new DecorationPackageMenu(area, "现代简约")
.appendCeiling(new LevelOneCeiling()) // 吊顶,一级顶
.appendCoat(new LiBangCoat()) // 涂料,立邦
.appendTile(new DongPengTile()); // 地砖,东鹏
}
}