软件设计原则
开闭原则
定义:软件实体(类,模块,函数等)应对扩展开放,对修改关闭。这意味着当需要添加新功能时,应该扩展现有代码来实现,而不是修改已有的稳定的代码。
里氏代换原则
定义:子类必须能够替换掉他们的父类。即如果S是T的子类型,那么在任何需要T类型对象的地方都可以用S类型来代替,并且程序的行为应该是合理的。
依赖倒转原则
定义:高层模块不应该依赖于低层模块。抽象不应该依赖于细节,细节应该依赖于抽象(接口)。
接口隔离原则
定义:客户端不应该被迫依赖于它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。将功能过多的接口拆散。
合成复用原则
定义:尽量使用组合(或者聚合)关系来实现复用,而不是继承关系。
迪米特法则
定义:一个对象应该对其他对象有最少的了解,只和直接的朋友通信。“朋友”包括对象本身,通过方法参数传进来的对象,积极创建或者实例化的对象以及对象的成员变量所引用的对象。
常用设计模式
简单工厂方法
概念
简单工厂模式是一种创建对象的设计模式。它的主要作用是将对象的创建和使用分离。就好像有一个工厂,这个工厂负责生产产品(对象),而客户(使用对象的代码)只需要告诉工厂要生产什么样的产品,不需要关心产品是怎么生产出来的。
图1.简单工厂模式类图示例
// 产品抽象类
interface Product {
void use();
}
// 具体产品类A
class ConcreteProductA implements Product {
@Override
public void use() {
System.out.println("使用产品A");
}
}
// 具体产品类B
class ConcreteProductB implements Product {
@Override
public void use() {
System.out.println("使用产品B");
}
}
// 简单工厂类
class SimpleFactory {
public Product createProduct(String type) {
if ("A".equals(type)) {
return new ConcreteProductA();
} else if ("B".equals(type)) {
return new ConcreteProductB();
}
return null;
}
}
public class Main {
public static void main(String[] args) {
SimpleFactory factory = new SimpleFactory();
Product productA = factory.createProduct("A");
if (productA!= null) {
productA.use();
}
Product productB = factory.createProduct("B");
if (productB!= null) {
productB.use();
}
}
}
优点
实现了对象创建和使用的分离
客户端无需知道所创建产品类具体的类名,只需要知道对应产品所对应的参数即可
缺点
工厂类集中了所有产品的创建逻辑,指责过重
系统扩展困难,一旦增加新产品就不得不修改工厂逻辑
使用场景
工厂类负责创建的对象比较少
客户端只知道传入工厂的参数,对于如何创建对象不关心
工厂方法模式
概念
工厂方法模式是一种创建对象的设计模式。它将对象的创建推迟到子类中进行。简单来说,在简单工厂模式里,是由一个工厂类来负责创建所有类型的产品对象;而在工厂方法模式中,有一个抽象的工厂类,这个抽象工厂类定义了一个创建产品的抽象方法,具体由它的子类来负责创建特定类型的产品。
// 产品抽象类
interface Product {
void use();
}
// 具体产品类A
class ConcreteProductA implements Product {
@Override
public void use() {
System.out.println("使用产品A");
}
}
// 具体产品类B
class ConcreteProductB implements Product {
@Override
public void use() {
System.out.println("使用产品B");
}
}
// 抽象工厂类
interface Factory {
Product createProduct();
}
// 具体工厂类A,用于生产产品A
class ConcreteFactoryA implements Factory {
@Override
public Product createProduct() {
return new ConcreteProductA();
}
}
// 具体工厂类B,用于生产产品B
class ConcreteFactoryB implements Factory {
@Override
public Product createProduct() {
return new ConcreteProductB();
}
}
public class Main {
public static void main(String[] args) {
Factory factoryA = new ConcreteFactoryA();
Product productA = factoryA.createProduct();
productA.use();
Factory factoryB = new ConcreteFactoryB();
Product productB = factoryB.createProduct();
productB.use();
}
}
优点
在系统加入新产品时符合开闭原则
能够让工厂自主确定创建何种产品对象
向客户隐藏了哪种具体产品类被实例化
缺点
类的个数增加,增加了系统复杂度
增加了系统的抽象性和理解难度
使用场景
客户不知道她所需要的对象的类
抽象工厂类通过其子类来指定创建哪个对象
抽象工厂模式
概念
抽象工厂模式是一种创建对象的设计模式,它提供了一种创建一系列相关对象的接口,而无需指定它们具体的类。它比工厂方法模式更抽象、更具一般性。
// 建筑抽象类
interface Building {
void build();
}
// 单位抽象类
interface Unit {
void train();
}
// 人类城堡
class HumanCastle implements Building {
@Override
public void build() {
System.out.println("建造人类城堡");
}
}
// 人类士兵
class HumanSoldier implements Unit {
@Override
public void train() {
System.out.println("训练人类士兵");
}
}
// 精灵城堡
class ElfCastle implements Building {
@Override
public void build() {
System.out.println("建造精灵城堡");
}
}
// 精灵魔法师
class ElfMage implements Unit {
@Override
public void train() {
System.out.println("训练精灵魔法师");
}
}
// 抽象工厂类
interface AbstractFactory {
Building createBuilding();
Unit createUnit();
}
// 人类工厂
class HumanFactory implements AbstractFactory {
@Override
public Building createBuilding() {
return new HumanCastle();
}
@Override
public Unit createUnit() {
return new HumanSoldier();
}
}
// 精灵工厂
class ElfFactory implements AbstractFactory {
@Override
public Building createBuilding() {
return new ElfCastle();
}
@Override
public Unit createUnit() {
return new ElfMage();
}
}
public class Main {
public static void main(String[] args) {
AbstractFactory humanFactory = new HumanFactory();
Building humanBuilding = humanFactory.createBuilding();
Unit humanUnit = humanFactory.createUnit();
humanBuilding.build();
humanUnit.train();
AbstractFactory elfFactory = new ElfFactory();
Building elfBuilding = elfFactory.createBuilding();
Unit elfUnit = elfFactory.createUnit();
elfBuilding.build();
elfUnit.train();
}
}
优点
隔离了具体类的生成
增加新的产品族很方便,符合开闭原则
缺点
增加新的产品等级结构麻烦,违背了开闭原则
使用场景
系统中有多于一个的产品族,但每次只是用其中一个产品族
产品等级结构稳定
解决跨平台带来的兼容性问题
原型模式
原型模式是一种创建型设计模式。它的核心是通过复制一个已经存在的对象(原型)来创建新的对象,而不是通过传统的使用构造函数来创建。就好像是克隆一样,以一个现有的对象为模板,快速地生成与之相似的新对象。
// 原型接口
interface Prototype {
Prototype clone();
}
// 具体原型类
class Person implements Prototype {
private String name;
private int age;
private Address address;
public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
// 实现克隆方法
@Override
public Prototype clone() {
try {
// 使用Java的Object类的clone方法进行浅克隆
Person cloned = (Person) super.clone();
// 如果需要深克隆,可以在这里对address进行克隆操作
// cloned.address = this.address.clone();
return cloned;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Address getAddress() {
return address;
}
}
class Address {
private String city;
public Address(String city) {
this.city = city;
}
public String getCity() {
return city;
}
}
public class Main {
public static void main(String[] args) {
Address address = new Address("New York");
Person person1 = new Person("Alice", 30, address);
// 克隆一个新的Person对象
Person person2 = (Person) person1.clone();
System.out.println("person1 name: " + person1.getName() + ", age: " + person1.getAge() + ", city: " + person1.getAddress().getCity());
System.out.println("person2 name: " + person2.getName() + ", age: " + person2.getAge() + ", city: " + person2.getAddress().getCity());
}
}
优点
简化对象的创建过程
简化创建结构
可以用深克隆保存对象的状态
扩展性比较好
缺点
需要为每一个类配备一个克隆方法,而且该克隆方法位于类的内部,违反开闭原则
实现深度克隆时候代码复杂
使用场景
创建新对象成本较大
系统要保存对象的状态,而对象的状态变化很小
需要避免使用分层次的工厂类创建分层次的对象
单例模式
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。就好像在一个软件系统中,某些资源(如配置文件管理器、数据库连接池)只需要一个实例来管理所有相关的操作,避免创建多个实例导致资源浪费或数据不一致等问题。
//饿汉式
class Singleton {
// 私有静态变量,在类加载时就创建实例
private static Singleton instance = new Singleton();
// 私有构造函数,防止外部通过构造函数创建实例
private Singleton() {}
// 公有静态方法,用于获取单例对象
public static Singleton getInstance() {
return instance;
}
}
//懒汉式
class Singleton {
// 私有静态变量,初始值为null
private static Singleton instance;
// 私有构造函数,防止外部通过构造函数创建实例
private Singleton() {}
// 公有静态方法,用于获取单例对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//双重检查锁定(DCL)懒汉式单例(适用于多线程环境)
class Singleton {
// 私有静态变量,初始值为null
private static volatile Singleton instance;
// 私有构造函数,防止外部通过构造函数创建实例
private Singleton() {}
// 公有静态方法,用于获取单例对象
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
//静态内部类单例:
class Singleton {
// 私有构造函数,防止外部通过构造函数创建实例
private Singleton() {}
// 静态内部类,在内部类中创建单例对象
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
// 公有静态方法,用于获取单例对象
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
优点
提供了对唯一实例的受控访问
节约系统资源,提高系统的性能
适配器模式
适配器模式是一种结构型设计模式,它的主要作用是将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的类可以协同工作。
// 目标接口,代表客户端期望的接口
interface Target {
void request();
}
// 被适配者接口,与目标接口不兼容
interface Adaptee {
void specificRequest();
}
// 被适配者接口的具体实现
class ConcreteAdaptee implements Adaptee {
@Override
public void specificRequest() {
System.out.println("执行被适配者的特定请求");
}
}
// 适配器类,实现目标接口,并且内部包含被适配者的引用
class Adapter implements Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
// 调用被适配者的方法来实现目标接口的方法
adaptee.specificRequest();
}
}
public class Main {
public static void main(String[] args) {
Adaptee adaptee = new ConcreteAdaptee();
Target target = new Adapter(adaptee);
target.request();
}
}
优点
将目标类和适配器类解耦
增加了类的透明性和复用
缺点
类适配器模式:
(1) 一次最多只能适配一个适配者类,不能同时适配多个适配者
(2) 适配者类不能为最终类
(3) 目标抽象类只能为接口,不能为类
对象适配器模式:在适配器中置换适配者类的某些方法比较麻烦
使用场景
系统需要使用一些现有的类,而这些类的接口不符合系统的需要,甚至没有这些类的源代码
创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类,包括一些可能在将来引进的类一起工作
组合模式
组合模式是一种结构型设计模式,它将对象组合成树形结构来表示 “部分 - 整体” 的层次结构。使得用户对单个对象和组合对象的使用具有一致性。
// 抽象组件接口
interface Component {
void operation();
void add(Component component);
void remove(Component component);
Component getChild(int index);
}
// 叶子节点类
class Leaf implements Component {
@Override
public void operation() {
System.out.println("叶子节点执行操作");
}
@Override
public void add(Component component) {
System.out.println("叶子节点不支持添加子组件");
}
@Override
public void remove(Component component) {
System.out.println("叶子节点不支持删除子组件");
}
@Override
public Component getChild(int index) {
System.out.println("叶子节点没有子组件");
return null;
}
}
// 树枝节点类
class Composite implements Component {
private java.util.ArrayList<Component> children = new java.util.ArrayList<>();
@Override
public void operation() {
System.out.println("树枝节点执行操作,同时调用子组件操作");
for (Component child : children) {
child.operation();
}
}
@Override
public void add(Component component) {
children.add(component);
}
@Override
public void remove(Component component) {
children.remove(component);
}
@Override
public Component getChild(int index) {
return children.get(index);
}
}
public class Main {
public static void main(String[] args) {
// 创建一个树枝节点
Composite composite = new Composite();
// 创建叶子节点并添加到树枝节点
Leaf leaf1 = new Leaf();
Leaf leaf2 = new Leaf();
composite.add(leaf1);
composite.add(leaf2);
// 执行树枝节点操作,会递归调用叶子节点操作
composite.operation();
}
}
优点
可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,让客户端忽略了层次的差异,方便对整个层次结构进行控制
客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
增加新的容器构件和叶子构件都很方便,符合开闭原则
为树形结构的面向对象实现提供了一种灵活的解决方案
缺点
在增加新构件时很难对容器中的构件类型进行限制
使用场景
在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们
在一个使用面向对象语言开发的系统中需要处理一个树形结构
在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型
装饰模式
装饰模式是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构。就好像给一个原本简单的物品(如杯子)添加各种装饰(如贴上漂亮的贴纸、加上杯套),而不改变杯子本身的基本结构,且可以动态地添加或组合这些装饰。
// 抽象组件接口,定义饮品的基本操作
interface Beverage {
double cost();
String getDescription();
}
// 具体组件,黑咖啡
class BlackCoffee implements Beverage {
@Override
public double cost() {
return 3.0;
}
@Override
public String getDescription() {
return "黑咖啡";
}
}
// 抽象装饰类,用于装饰饮品
abstract class CondimentDecorator implements Beverage {
protected Beverage beverage;
public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}
@Override
public double cost() {
return beverage.cost();
}
@Override
public String getDescription() {
return beverage.getDescription();
}
}
// 具体装饰类,牛奶装饰
class MilkDecorator extends CondimentDecorator {
public MilkDecorator(Beverage beverage) {
super(beverage);
}
@Override
public double cost() {
return beverage.cost() + 0.5;
}
@Override
public String getDescription() {
return beverage.getDescription() + ",加了牛奶";
}
}
public class Main {
public static void main(String[] args) {
Beverage blackCoffee = new BlackCoffee();
System.out.println("描述:" + blackCoffee.getDescription() + ",价格:" + blackCoffee.cost());
Beverage milkCoffee = new MilkDecorator(blackCoffee);
System.out.println("描述:" + milkCoffee.getDescription() + ",价格:" + milkCoffee.cost());
}
}
优点
对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加
可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为
可以对一个对象进行多次装饰具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,且原有类库代码无须改变,符合开闭原则
缺点
使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,在一定程度上影响程序的性能
比继承更加易于出错,排错也更困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐
使用场景
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式
观察者模式
观察者模式是一种行为设计模式。它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象的状态发生变化时,会通知所有依赖它的观察者对象,使它们能够自动更新自己的状态。
// 抽象观察者接口,定义更新方法
interface Observer {
void update(String news);
}
// 抽象主题接口,定义添加、删除观察者和通知观察者的方法
interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers(String news);
}
// 具体观察者类,实现抽象观察者接口
class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(String news) {
System.out.println(name + "收到新闻:" + news);
}
}
// 具体主题类,实现抽象主题接口,维护观察者列表并实现通知逻辑
class ConcreteSubject implements Subject {
private java.util.ArrayList<Observer> observers = new java.util.ArrayList<>();
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String news) {
for (Observer observer : observers) {
observer.update(news);
}
}
}
public class Main {
public static void main(String[] args) {
ConcreteSubject newsWebsite = new ConcreteSubject();
Observer user1 = new ConcreteObserver("用户1");
Observer user2 = new ConcreteObserver("用户2");
newsWebsite.attach(user1);
newsWebsite.attach(user2);
newsWebsite.notifyObservers("有新的科技新闻发布!");
}
}
优点
可以实现表示层和数据逻辑层的分离
在观察目标和观察者之间建立一个抽象的耦合
支持广播通信,简化了一对多系统设计的难度
符合开闭原则,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便
缺点
将所有的观察者都通知到会花费很多时间
如果存在循环依赖时可能导致系统崩溃
没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而只是知道观察目标发生了变化
使用场景
当⼀个对象状态的改变需要改变其他对象时。
⼀个对象发⽣改变时只想要发送通知,⽽不需要知道接收者是谁。⽐如,订阅
微信公众号的⽂章,发送者通过公众号发送,发送者并不知道哪些⽤户订阅了
公众号。
需要创建⼀种链式触发机制时。⽐如,在系统中创建⼀个触发链,A 对象的⾏
为将影响 B 对象,B 对象的⾏为将影响 C 对象……这样通过观察者模式能够很好地实现。
状态模式
状态模式是一种行为设计模式。它允许对象在其内部状态改变时改变它的行为。就好像一个自动售卖机,它有不同的状态(如空闲、投币后、出货后等),在不同状态下,用户进行相同操作(如按按钮)会引发不同的行为(如空闲时按按钮提示投币,投币后按按钮出货)。
// 抽象状态类,定义文档在不同状态下保存操作的接口
interface DocumentState {
void save(Document document);
}
// 新建状态类,具体实现文档在新建状态下的保存行为
class NewDocumentState implements DocumentState {
@Override
public void save(Document document) {
System.out.println("正在保存新建文档,分配文档编号...");
document.setState(new EditingDocumentState());
}
}
// 编辑中状态类,具体实现文档在编辑中状态下的保存行为
class EditingDocumentState implements DocumentState {
@Override
public void save(Document document) {
System.out.println("正在保存编辑中的文档,更新文件内容...");
document.setState(new SavedDocumentState());
}
}
// 已保存状态类,具体实现文档在已保存状态下的保存行为
class SavedDocumentState implements DocumentState {
@Override
public void save(Document document) {
System.out.println("文档已保存,无需重复保存。");
}
}
// 文档类,作为环境类,维护当前状态并处理保存请求
class Document {
private DocumentState state;
public Document() {
this.state = new NewDocumentState();
}
public void setState(DocumentState state) {
this.state = state;
}
public void save() {
state.save(this);
}
}
public class Main {
public static void main(String[] args) {
Document document = new Document();
document.save();
document.save();
document.save();
}
}
优点
封装了状态的转换规则,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中
将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为
允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,可以避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起
可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数
缺点
封装了状态的转换规则,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中
将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为
允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,可以避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起
可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数
使用场景
对象的行为依赖于它的状态(例如某些属性值),状态的改变将导致行为的变化
在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强
策略模式
策略模式是一种行为设计模式,它定义了一系列算法,将每个算法都封装起来,并且使它们可以相互替换。就好比在一个电商系统中,计算商品总价的方式可能有多种策略,如按照原价计算、按照折扣价计算、按照满减后的价格计算等,这些不同的计算方式就是不同的策略。
// 抽象策略类,定义计算总价的接口
interface PricingStrategy {
double calculateTotalPrice(double[] prices);
}
// 原价计算策略类,具体实现按照原价计算总价的算法
class OriginalPriceStrategy implements PricingStrategy {
@Override
public double calculateTotalPrice(double[] prices) {
double total = 0;
for (double price : prices) {
total += price;
}
return total;
}
}
// 折扣计算策略类,具体实现按照折扣计算总价的算法
class DiscountPriceStrategy implements PricingStrategy {
private double discountRate;
public DiscountPriceStrategy(double discountRate) {
this.discountRate = discountRate;
}
@Override
public double calculateTotalPrice(double[] prices) {
double total = 0;
for (double price : prices) {
total += price * discountRate;
}
return total;
}
}
// 上下文类,维护一个策略对象引用并执行计算总价的请求
class ShoppingCart {
private PricingStrategy strategy;
public ShoppingCart(PricingStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(PricingStrategy strategy) {
this.strategy = strategy;
}
public double calculateTotal(double[] prices) {
return strategy.calculateTotalPrice(prices);
}
}
public class Main {
public static void main(String[] args) {
double[] prices = {10.0, 20.0, 30.0};
// 使用原价计算策略
ShoppingCart cart1 = new ShoppingCart(new OriginalPriceStrategy());
double total1 = cart1.calculateTotal(prices);
System.out.println("原价计算总价:" + total1);
// 使用折扣计算策略(例如8折)
ShoppingCart cart2 = new ShoppingCart(new DiscountPriceStrategy(0.8));
double total2 = cart2.calculateTotal(prices);
System.out.println("折扣计算总价:" + total2);
}
}
优点
提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为
提供了管理相关的算法族的办法
提供了一种可以替换继承关系的办法
可以避免多重条件选择语句
提供了一种算法的复用机制,不同的环境类可以方便地复用策略类
缺点
客户端必须知道所有的策略类,并自行决定使用哪一个策略类
将造成系统产生很多具体策略类
无法同时在客户端使用多个策略类
使用场景
一个系统需要动态地在几种算法中选择一种
避免使用难以维护的多重条件选择语句
不希望客户端知道复杂的、与算法相关的数据结构,提高算法的保密性与安全性