需求背景
在学习算法的过程中,一个问题的解决思路往往会有多种,例如排序算法的实现就包含V1.0(冒泡排序),V1.1(选择排序),V1.2(快速排序)等多个版本。在具体使用过程中,可能会因为不同的需求而变更算法版本。为了方便管理和使用不同版本的算法,本文采用工厂方法的设计模式。理解工厂模式的可以直接看具体实现。
工厂模式简介
这里采用黑马程序员的例子来说一下简单工厂模式和工厂方法模式的区别。需求:设计一个咖啡店点餐系统。不采用任何设计模式的话,那我们可以设计一个咖啡类(Coffee),并定义其两个子类,美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】;再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。具体类图的设计如下:
可以看到不采用任何设计模式时,违反了开闭原则,程序耦合严重的扩展性很差,每当增加一个咖啡时,就要重新修改代码。
简单工厂模式
简单工厂包含如下角色:
抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
具体产品 :实现或者继承抽象产品的子类
具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。
工厂类代码如下:
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}
可以看到,使用简单工厂模式并不能消除耦合,他只是针对coffeStore再封装了一层,相当于是把耦合转移了。那这么做有什么好处呢?如果coffeStore要集成其他系统,比如接入美团,饿了么等,那只需要专注于在coffeStore上修改接入的代码。所以说简答工厂模式并不像一种设计模式,而是一种编程习惯,一个方法只做一件事情。
工厂方法模式
但是使用工厂方法模式就可以做到开闭原则,解除部分耦合(耦合只能尽可能减少,不能完全解除)。工厂方法模式简单来说就是:定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
工厂方法模式的主要角色:
抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂
方法来创建产品。
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
代码如下
抽象工厂:
public interface CoffeeFactory {
Coffee createCoffee();
}
具体工厂:
public class LatteCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
}
public class AmericanCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
咖啡店类:
public class CoffeeStore {
private CoffeeFactory factory;
public CoffeeStore(CoffeeFactory factory) {
this.factory = factory;
}
public Coffee orderCoffee(String type) {
Coffee coffee = factory.createCoffee();
coffee.addMilk();
coffee.addsugar();
return coffee;
}
}
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,
这样就解决了简单工厂模式的缺点。工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。但是缺点也很明显,每创建一个具体产品就要创建一个具体工厂,容易产生“类爆炸”情况。
具体实现
为了解决不同版本的算法迭代,同时为了避免“类爆炸”,本项目吸取了工厂方法模式的优缺点,具体类图结构如下。
在项目中主要分为以下几个角色,拿最短路算法作为具体例子来说:
抽象工厂(AlgorithmFactory):抽象算法工厂里定义了一个createAlgorithm方法,并定义泛型,在具体工厂实现AlgorithmFactory时要指定泛型类型。代码如下
public interface AlgorithmFactory<T> {
T createAlgorithm();
}
具体工厂(ShortestPathFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建,并指定泛型为ShortestPath。代码如下
//在实现抽象工厂时通过泛型机制指定具体产品
public class ShortestPathFactory implements AlgorithmFactory<ShortestPath> {
//枚举值,表示算法名称
private final ShortPathAlgorithmName algorithmName;
public ShortestPathFactory(ShortPathAlgorithmName algorithmName) {
this.algorithmName = algorithmName;
}
@Override
public ShortestPath createAlgorithm(){
ShortestPath shortestPath;
try {
String packageName = "hnfnu.zmq.graphTheory.shortestPath.impl";
//这是我自己写的工具类,通过反射创建对象
shortestPath = ConstructUtil.Construct(packageName, this.algorithmName.getName(), ShortestPath.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
return shortestPath;
}
}
抽象产品(Algorithm):定义了产品的规范,描述了产品的主要特性和功能。在本项目中并没有用到抽象产品,因为每一个算法的参数,返回值基本上都是不一样的,难以定义统一的参数和返回值。
具体产品(ShortestPath):由具体工厂来创建,它同具体工厂之间一一对应。在本项目中,具体产品大多是一个抽象类,目的是为了后续拓展不同的版本。例如ShortestPath下的Dijkstra算法,Bellman Ford算法和SPFA算法。具体实现如下
//具体产品
public abstract class ShortestPath {
protected static final Integer MAX_WEIGHT=Integer.MAX_VALUE/2;
protected int[] dist;
protected int[] initGraphDist(Graph g){
int[] dist=new int[g.getNodeCount()+1];
Arrays.fill(dist,MAX_WEIGHT);
dist[g.getStart()]=0;
return dist;
}
public abstract void calculatedShortestPath(Graph g);
}
//具体产品下的子产品
public class SPFA extends ShortestPath {
@Override
public void calculatedShortestPath(Graph g){
if(g==null){
System.out.println("SPFA...null");
return;
}
int[] dist = initGraphDist(g);
int[][] graph = g.getGraph();
//用于存放被更新过的节点
Queue<Integer> queue=new LinkedList<>();
Boolean[] exist=new Boolean[g.getNodeCount()+1];
queue.offer(g.getStart());
exist[g.getStart()]=true;
while (!queue.isEmpty()){
int node = queue.poll();
exist[node]=false;
for (int i=1;i<=g.getNodeCount();i++) {
if (graph[node][i]!=MAX_WEIGHT && dist[i] > dist[node] + graph[node][i]) {
dist[i] = dist[node] + graph[node][i];
queue.offer(i);
exist[i]=true;
}
}
}
this.dist=dist;
}
}
//具体产品下的子产品
public class Dijkstra extends ShortestPath {
@Override
public void calculatedShortestPath(Graph g){
if(g==null){
System.out.println("Dijkstra...null");
return;
}
//初始化距离数组
int[] dist=initGraphDist(g);;
int[][] graph = g.getGraph();
//创建一个小根堆
PriorityQueue<Integer> confirmNodes = new PriorityQueue<>((i, j)->(dist[i]-dist[j]));
confirmNodes.offer(g.getStart());
//堆不为空表示可以继续更新其他节点
while (!confirmNodes.isEmpty()) {
//每次取离起点距离最近的点,用这个点去更新其他的点,在把更新后的点加入到队列中
int node = confirmNodes.poll();
for (int i=1;i<=g.getNodeCount();i++) {
if (graph[node][i]!=MAX_WEIGHT && dist[i] > dist[node] + graph[node][i]) {
dist[i] = dist[node] + graph[node][i];
confirmNodes.offer(i);
}
}
}
this.dist=dist;
}
}
//具体产品下的子产品
public class BellmanFord extends ShortestPath {
@Override
public void calculatedShortestPath(Graph graph){
if(graph==null){
System.out.println("BellmanFord...null");
return;
}
int[] dist = initGraphDist(graph);
int[] last = new int[graph.getNodeCount()+1];
List<Graph.Edge> edges = graph.buildEdges();
for (int i = 0; i < edges.size(); i++) {
System.arraycopy(dist,0,last,0,dist.length);
for (Graph.Edge edge : edges) {
dist[edge.end] = Math.min(dist[edge.end], last[edge.begin] + edge.weight);
}
}
this.dist=dist;
}
}
具体使用如下:
public static void main(String[] args) {
AlgorithmStore algorithmStore = new AlgorithmStore();
ShortPath shortPath = algorithmStore.getAlgorithm(new ShortPathFactory(ShortPathAlgorithmName.SPFA));
shortPath.calculatedShortestPath(null);
}
本项目的设计与咖啡店案例最大的不同在于抽象产品和具体产品上的差异,咖啡店案例中具体产品是一个具体的类,而本项目中是一个抽象类,咖啡店案例有抽象产品,而本项目中不存在抽象产品。然而尽管有所差异,但是也不违反开闭原则,后续的拓展也是非常方便。在后续迭代过程中也不会出现“类爆炸的情况”
对于拓展一个新算法,需要做四步
- 1.新建具体算法工厂类实现抽象工厂算法接口,并指定算法泛型。
- 2.新建算法的枚举类,表示该算法的具体实现种类或者是版本
- 3.新建具体算法类,该类是一个抽象类,定义抽象方法,目的是方便后续对该算法的迭代升级
- 4.新建impl包在impl包下新建具体算法类的子类,继承抽象类,并实现抽象方法
对于拓展一个算法的新版本,只需要做两步。
- 1:在该算法的枚举类下新增一个枚举值。
- 2:新建一个类继承具体算法类。
需要源码的小伙伴评论一下就好,欢迎大家一起讨论学习