最近我在项目中做了一个地图API相关的功能开发,需求就是,国内调百度地图API,海外就调谷歌地图API。功能实现很简单,但是这个功能开发完后,让我对策略模式有了更深的理解和印象。在这里我把当时的想法和遇到的问题完整记录下来。
首先,定义一个地图接口。
/**
* 地图service
*/
public interface IMapService {
/**
* 兴趣点查询
*
* @param poi 兴趣点
* @return
*/
void queryPlaceOfInterest(PoiInfo poi);
}
然后,用百度和谷歌地图API分别实现这个接口。
/**
* 百度地图service
*/
public class BaiduMapServiceImpl implements IMapService {
@Override
public void queryPlaceOfInterest(PoiInfo poi) {
System.out.println("baidu地图兴趣点查询!");
}
}
/**
* 谷歌地图service
*/
public class GoogleMapServiceImpl implements IMapService {
@Override
public void queryPlaceOfInterest(PoiInfo poi) {
System.out.println("google地图兴趣点查询!");
}
}
就是这样,这个功能就基本完成了。客户端使用如下:
public class Client {
public static void main(String[] args) {
/**
* 客户端持有百度地图实现类和谷歌地图实现类
*/
IMapService baiduMap = new BaiduMapServiceImpl ();
IMapService goolgeMap = new GoogleMapServiceImpl();
// 待查询的兴趣点
PoiInfo poi = new PoiInfo();
baiduMap.queryPlaceOfInterest(poi);
goolgeMap.queryPlaceOfInterest(poi);
}
}
这是我一开始的思路,我底层提供不同地图的实现类,客户端想用什么地图,由他自己调用即可。其实,这不符合软件设计的原则——高内聚低耦合和开闭原则。
我这里就是客户端直接对实现类的引用。造成客户端代码和地图实现类高度耦合了。这种代码的问题就是,扩展性差。
以后有其他地图要接入,如:高德。我会提供一个高德地图实现类,然后客户端代码也要改动,添加一个对高德地图实现的引用。这样子其实很不友好,对客户端代码侵入性较高,不利于扩展。
其实,我们可以给客户端提供一个适配器,客户端只持有适配器,具体的服务由适配器去调用,最后达到解耦的目的。
那如何去操作呢?策略模式就完全解决了我的问题!
策略模式
策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法。
组成
- 抽象策略角色: 策略类,通常由一个接口或者抽象类实现。
- 具体策略角色:包装了相关的算法和行为。
- 环境角色:持有一个策略类的引用,最终给客户端调用。
UML图
策略模式的具体实现
接下来,我们只要按照策略模式的角色组成完善代码即可。
- 抽象策略角色,就是我代码中的IMapService接口。
- 具体策略角色,就是我要实现的具体地图实现类,如百度,高德,谷歌等。
- 环境角色,这个角色很重要,大家可以发现,我之前的代码设计其实就是缺了这个角色,这也正是我要的“适配器”。它的作用将策略类从客户端解耦出来。所以我要做的工作就是,设计这个环境角色类。
在我的代码中,环境角色要干的事情是,它先选择正确的具体地图实现类,然后执行对应的方法,最终把结果返回给客户端即可。
为了让环境角色类可以判断具体的实现类,我在每个实现类中增加地图类型的标识。代码如下:
/**
* 地图类型枚举类
* @author chunxingzhang
*
*/
public enum MapTypeEnum {
BAIDU,GOOGLE,GAODE
}
/**
* 地图service
*/
public interface IMapService {
/**
* 是否支持poi的地图类型
* @param poi
* @return
*/
boolean supportMapType(PoiInfo poi);
/**
* 兴趣点查询
*
* @param poi 兴趣点
* @return
*/
void queryPlaceOfInterest(PoiInfo poi);
}
public class BaiduMapServiceImpl implements IMapService {
@Override
public void queryPlaceOfInterest(PoiInfo poi) {
System.out.println("baidu地图兴趣点查询!");
}
/**
* 支持百度
*/
@Override
public boolean supportMapType(PoiInfo poi) {
return MapTypeEnum.BAIDU.ordinal() == poi.mapType;
}
}
public class GoogleMapServiceImpl implements IMapService {
@Override
public void queryPlaceOfInterest(PoiInfo poi) {
// TODO Auto-generated method stub
System.out.println("google地图兴趣点查询!");
}
/**
* 支持谷歌
*/
@Override
public boolean supportMapType(PoiInfo poi) {
return MapTypeEnum.GOOGLE.ordinal() == poi.mapType;
}
}
新增MapTypeEnum枚举类来维护地图类型,在接口中新加supportMapType(PoiInfo poi)用来标识实现类是否支持当前地图。
接下来可以去实现环境角色类Context。根据定义,环境角色类持有策略类的引用来设计。
/**
* 环境角色类,持有对具体策略类实现的集合引用
* @author chunxingzhang
*
*/
public class MapServiceContext implements IMapService {
/**
* 使用spring管理bean,可以这样用,
* 自动装配所有实现类到集合中
*/
//@Autowired
//private List<IMapService> mapServiceList = new ArrayList<>();
private List<IMapService> mapServiceList = new ArrayList<>();
/**
* 初始化的时候将所有已有实现类装配到集合中。
*/
{
mapServiceList.add(new BaiduMapServiceImpl());
mapServiceList.add(new GoogleMapServiceImpl());
}
/**
* 遍历地图实现类集合,是否有支持当前poi的地图类型
*/
@Override
public boolean supportMapType(PoiInfo poi) {
for (IMapService mapService : mapServiceList) {
if (mapService.supportMapType(poi)) {
return true;
}
}
return false;
}
@Override
public void queryPlaceOfInterest(PoiInfo poi) {
// TODO Auto-generated method stub
getSupportedMapServiceImpl(poi).queryPlaceOfInterest(poi);
}
/**
* 选择具体的地图实现类
* @param poi
* @return
*/
private IMapService getSupportedMapServiceImpl(PoiInfo poi) {
for (IMapService mapService : mapServiceList) {
if (mapService.supportMapType(poi)) {
return mapService;
}
}
throw new UnsupportedOperationException("不支持当前poi的地图类型!");
}
}
于是客户端代码就只要引用这个context类即可。
public class Client {
public static void main(String[] args) {
/**
* 客户端持有这个环境角色类context即可
*/
IMapService MapServiceContext = new MapServiceContext ();
// 获取一个待查询的兴趣点,使用谷歌地图
PoiInfo poi1 = new PoiInfo();
poi1.mapType = 0;
MapServiceContext.queryPlaceOfInterest(poi1);
// 获取一个待查询的兴趣点,使用百度地图
PoiInfo poi2 = new PoiInfo();
poi2.mapType = 1;
MapServiceContext.queryPlaceOfInterest(poi2);
}
}
结果如下:
google地图兴趣点查询!
baidu地图兴趣点查询!
这样就达到了我的预期目标,客户端不需要关系底层地图的具体的实现类型,他只要调用我提供的context类下面的方法就好了,因为context类也实现了IMapService的接口,这样就保持了它和具体实现类的统一性。客户端使用起来和直接使用地图具体实现类没有任何区别。当接口有变化,或者新的地图类型加入后,不会再影响到客户端的逻辑,只要维护地图服务层的逻辑即可。
总结
这就是我对该功能需求实现的最终代码结果了。后来在反复思考的过程中,我发现我的代码形式跟网上的策略模式的定义有点出入(尴尬)。比如:
我的环境角色类context实现了IMapService接口。
实现同一个接口有助于让环境角色类和策略类统一规范,这样使客户端用起来感觉不到任何差异性。
环境角色类context类是对具体策略类集合的引用,而不是直接持有一个策略类引用。
造成差别的原因是,策略模式中context类持有一个策略类引用,然后客户端再使用context类时要传一个具体的策略类给他,也就是说客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这恰恰是我不愿意看到的。所以我对其做了一定的修改。