使用简单工厂加接口加适配器模式来遵守开闭原则

原创
2019/08/26 23:12
阅读数 941

我们在平时开发中遇到最多的问题,无异于实体类属性的变化,可能我们开发出来的接口跟前端要的字段很多不一样,或者需求变更,需要返回的很多内容不一样。

假设我们现在有这么一个需求,返回一个配件的详细信息,也许我们之前返回的格式如下

{
"code" : 200 ,
"data" : {
"brand" : {
"code" : "001" ,
"firstChar" : "�" ,
"id" : 1 ,
"logoUrl" : "http://123.456.789" ,
"name" : "飞利浦" ,
"sort" : 1
},
"code" : "0002" ,
"freeShipping" : false ,
"levelName" : "高级项链" ,
"otherValues" : {
"innerMap" : {
"商品等级" : "国际" ,
"运费设置" : "包邮" ,
"生产厂家" : "飞利浦" ,
"包装规格" : "10" ,
"商品产地" : "呼和浩特"
},
"obj" : {
"$ref" : "$.data.otherValues.innerMap"
}
},
"product" : {
"hotSell" : false ,
"id" : 2459901248443253560 ,
"model" : "朝天DDL" ,
"name" : "项链天窗" ,
"onShelf" : false ,
"price" : {
"begin" : false ,
"normalPrice" : 3000.0000000000
},
"recommend" : false
},
"provider" : {
"code" : "0001" ,
"productProvider" : {
"id" : 2459698718186668856 ,
"logoUrl" : "http://123.456.23.12/12.jpg" ,
"name" : "大众4S店" ,
"productList" : []
},
"status" : false
}
},
"msg" : "操作成功"
}
但是由于现在需求变化,我们需要返回如下格式
{
    data: {
        product: { // 产品信息
            id: '',
            name: '', // 产品名称
            imgs: [''], // 产品图片
            price: 123, // 价格
        },
        store: { // 店铺信息
            name: '',
            id: '',
            logo: '',
            rate: 4.5, // 评价
            collect: 2032, // 收藏数
        },
        comments: { // 评论
            rate: 4.4, // 总评分
            list: [
                {
                    id: '',
                    content: '',
                    img: [''],
                    ...
                }
            ]
        },
        specs: [ // 自定义属性
            {name: '商品等级', value: '国际'}
        ],
        picTextContent: '<html />' // 图文详情
    }
}
业务相关有两个接口
/**
 * 配件商品提供者
 */
public interface Provider {
    /**
     * 添加商品提供者(包含门店,服务)
     * @param provider
     * @return
     */
    boolean addProvider(Provider provider);

    /**
     * 移除商品提供者
     * @param provider
     * @return
     */
    boolean removeProdvider(Provider provider);

    /**
     * 根据id获取一个商品提供者
     * @param id
     * @return
     */
    Provider findProvider(Long id);

    /**
     * 获取所有商品提供者
     * @return
     */
    List<Provider> allProvider();

    /**
     * 根据id获取下层商品提供者
     * @param id
     * @return
     */
    List<Provider> findContaint(Long id);
}
public interface ProductService {
    Page<Provider> showProduct(Map<String,Object> params);
    Provider findProduct(Long id);
    Long findProviderId(Long id);
}

需求变更前,我们定义的配件实体类如下

@Slf4j
@Data
@NoArgsConstructor
public class ProviderProduct implements Provider,ProductService {
    private Product product;
    private String code;
    private Brand brand;
    private String details;
    private String levelName;
    private boolean freeShipping;
    private DefaultProvider provider;
    private ExtBeanWrapper otherValues;

    public ProviderProduct(Product product,String code,Brand brand) {
        this.product = product;
        this.code = code;
        this.brand = brand;
    }
    @Override
    public boolean addProvider(Provider provider) {
        throw new RuntimeException("不支持此方法");
    }

    @Override
    public boolean removeProdvider(Provider provider) {
        throw new RuntimeException("不支持此方法");
    }

    @Override
    public Provider findProvider(Long id) {
        return null;
    }

    @Override
    public List<Provider> allProvider() {
        return null;
    }

    @Override
    public List<Provider> findContaint(Long id) {
        throw new RuntimeException("不支持此方法");
    }

    @Override
    public Page<Provider> showProduct(Map<String, Object> params) {
        ProductDao productDao = SpringBootUtil.getBean(ProductDao.class);
        int total = productDao.countProductInDefaultProvider(params);
        List<Provider> providers = Collections.emptyList();
        if (total > 0) {
            PageUtil.pageParamConver(params,false);
            providers = productDao.findAllProductSimpleByProviderId(params);
        }
        return new Page<>(total,providers);
    }

    @Override
    public Provider findProduct(Long id) {
        ProductDao productDao = SpringBootUtil.getBean(ProductDao.class);
        OtherPropertyDao otherPropertyDao = SpringBootUtil.getBean(OtherPropertyDao.class);
        Provider product = productDao.findProductById(id);
        Map map = ((ProviderProduct) product).getOtherValues().getInnerMap();
        Map<String,String> insteadMap = new HashMap<>();
        for (Object key : map.keySet()) {
            log.info("键名为:" + String.valueOf(key));
            String name = otherPropertyDao.findNameById(Long.parseLong(String.valueOf(key)));
            insteadMap.put(name,(String) map.get(key));
        }
        ((ProviderProduct) product).getOtherValues().setObj(insteadMap);
        return product;
    }

    @Override
    public Long findProviderId(Long id) {
        ProductDao productDao = SpringBootUtil.getBean(ProductDao.class);
        return productDao.findProviderIdById(id);
    }
}
/**
 * 配件提供者工厂
 */
public class ProviderFactory {
    public static ProductService createProviderProduct() {
        return new ProviderProduct();
    }
}

Controller如下(有删减)

@Slf4j
@RestController
public class ProductController {
    private ProductService productService = ProviderFactory.createProviderProduct();

    /**
     * 展示某个配件商的所有配件(带分页)
     * @param params
     * @return
     */
    @Transactional
    @SuppressWarnings("unchecked")
    @GetMapping("/productprovider-anon/showproduct")
    public Result<Page<Provider>> showProduct(@RequestParam Map<String,Object> params) {
        return Result.success(productService.showProduct(params));
    }

    /**
     * 查看某一个配件的详细信息
     * @param id
     * @return
     */
    @Transactional
    @SuppressWarnings("unchecked")
    @GetMapping("/productprovider-anon/findproduct")
    public Result<Provider> findProduct(@RequestParam("id") Long id) {
        return Result.success(productService.findProduct(id));
    }
}

业务变更后,我们创建新的实体类

@Slf4j
@NoArgsConstructor
public class ProductDetail implements ProductService,Provider {
    private ProviderProduct providerProduct = new ProviderProduct();
    @Getter
    @Setter
    private Long id;
    @Getter
    @Setter
    private String name;
    @Getter
    @Setter
    private List<String> imgUrl;
    @Getter
    @Setter
    private Price price;
    @Getter
    @Setter
    private ProductProvider provider;
    @Getter
    @Setter
    private ExtBeanWrapper otherValues;
    @Getter
    @Setter
    private List<OtherProperty> otherProperties;
    @Getter
    @Setter
    private Integer collectedNum;
    @Getter
    @Setter
    private Double avgStar;
    @Getter
    @Setter
    private List<Evaluate> evaluateList;
    @Getter
    @Setter
    private String details;

    @Data
    @AllArgsConstructor
    private class OtherProperty {
        private String name;
        private String value;
    }

    @Override
    public Page<Provider> showProduct(Map<String, Object> params) {
        return providerProduct.showProduct(params);
    }

    @Override
    public Provider findProduct(Long id) {
        ProductDetailDao productDetailDao = SpringBootUtil.getBean(ProductDetailDao.class);
        EvaluateClient evaluateClient = SpringBootUtil.getBean(EvaluateClient.class);
        OtherPropertyDao otherPropertyDao = SpringBootUtil.getBean(OtherPropertyDao.class);
        CollectionDao collectionDao = SpringBootUtil.getBean(CollectionDao.class);
        Provider product = productDetailDao.findProductById(id);
        String imgUrlStr = productDetailDao.findImgUrlById(id);
        String[] imgUrls = imgUrlStr.split(",");
        ((ProductDetail)product).setImgUrl(Arrays.asList(imgUrls));
        List<Evaluate> evaluates = evaluateClient.allEvaluateOfProduct(id, ((ProductDetail) product).getProvider().getId());
        log.info("门店评论:" + JSON.toJSONString(evaluates));
        ((ProductDetail)product).setEvaluateList(evaluates);
        OptionalDouble average = evaluates.stream().mapToDouble(Evaluate::getStar).average();
        ((ProductDetail)product).setAvgStar(average.orElse(0.0));
        Map map = ((ProductDetail)product).getOtherValues().getInnerMap();
        ((ProductDetail)product).setOtherProperties(new ArrayList<>());
        map.keySet().stream().forEach(key -> {
            log.info("键名为:" + String.valueOf(key));
            String name = otherPropertyDao.findNameById(Long.parseLong(String.valueOf(key)));
            ((ProductDetail)product).getOtherProperties().add(new OtherProperty(name,(String) map.get(key)));
        });
        ((ProductDetail)product).setOtherValues(null);
        ((ProductDetail)product).setCollectedNum(collectionDao.countCollectionByProviderId(
                ((ProductDetail) product).getProvider().getId()));
        return product;
    }

    @Override
    public Long findProviderId(Long id) {
        return providerProduct.findProviderId(id);
    }

    @Override
    public boolean addProvider(Provider provider) {
        return providerProduct.addProvider(provider);
    }

    @Override
    public boolean removeProdvider(Provider provider) {
        return providerProduct.removeProdvider(provider);
    }

    @Override
    public Provider findProvider(Long id) {
        return providerProduct.findProvider(id);
    }

    @Override
    public List<Provider> allProvider() {
        return providerProduct.allProvider();
    }

    @Override
    public List<Provider> findContaint(Long id) {
        return providerProduct.findContaint(id);
    }
}

按照需求一比一定义字段,我们可以看到新类也实现了这两个接口ProductService,Provider,由于我们只需要变更展示配件明细,其他的需求并不需要修改,则我们委托了一个private ProviderProduct providerProduct = new ProviderProduct()让其适配原有的业务即可,需要变更的是findProduct()方法

dao和mapper省略......

此时要使用新类来跑新需求,我们需要变更工厂的实现类

/**
 * 配件提供者工厂
 */
public class ProviderFactory {
    public static ProductService createProviderProduct() {
        return new ProductDetail();
    }
}

如此我们不需要修改任何原始代码就完成了需求变更,Controller也无需做修改。完全符合开闭原则,同时遵守了依赖倒置原则,李氏替换原则,接口隔离原则。

最后返回的结果如下

{
"code" : 200 ,
"data" : {
"avgStar" : 5.0 ,
"collectedNum" : 1 ,
"details" : "<html><body><a href='sfasffg'><img url='sdfsgwer' /></a></body></html>" ,
"evaluateList" : [
{
"content" : "产品非常好用,建议购买" ,
"date" : "2019-07-25T16:14:31.885+0800" ,
"id" : 2460462767098823480 ,
"nickName" : "测试1" ,
"orgId" : 2459698718186668856 ,
"otherId" : 2459906470049743672 ,
"otherType" : "Product" ,
"star" : 5.0 ,
"userId" : 1 ,
"userName" : "admin"
},
{
"content" : "产品非常好用,建议购买" ,
"date" : "2019-07-25T16:54:17.537+0800" ,
"id" : 2460465329046815544 ,
"nickName" : "测试1" ,
"orgId" : 2459698718186668856 ,
"otherId" : 2459906470049743672 ,
"otherType" : "Product" ,
"picUrls" : "http://123.234.444.23/12.jpg,http://234.342.223.333/23.jpg" ,
"star" : 5.0 ,
"userId" : 1 ,
"userName" : "admin"
}
],
"id" : 2459906470049743672 ,
"imgUrl" : [
"http://123.433.567.988"
],
"name" : "项链桌椅" ,
"otherProperties" : [
{
"name" : "商品等级" ,
"value" : "国际"
},
{
"name" : "包装规格" ,
"value" : "10"
},
{
"name" : "商品产地" ,
"value" : "呼和浩特"
},
{
"name" : "生产厂家" ,
"value" : "飞利浦"
},
{
"name" : "运费设置" ,
"value" : "包邮"
}
],
"price" : {
"begin" : false ,
"normalPrice" : 2000.0000000000
},
"provider" : {
"id" : 2459698718186668856 ,
"logoUrl" : "http://123.456.23.12/12.jpg" ,
"name" : "大众4S店" ,
"productList" : []
}
},
"msg" : "操作成功"
}
 
当然如果你实在连工厂类都不想改的话,只想加一个实现类就完事,可以做如下处理
增加一个标签,表示产品类的版本号
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProductVersion {
    int value();
}

给我们的原始类和新类打上版本标签

@Slf4j
@Data
@NoArgsConstructor
@ProductVersion(value = 1)
public class ProviderProduct implements Provider,ProductService
@Slf4j
@NoArgsConstructor
@ProductVersion(value = 2)
public class ProductDetail implements ProductService,Provider

注:这两个类必须放在同一个包下面

修改配件工厂类如下

public static ProductService createProviderProduct() {
        //搜索该包下的所有类,并放入集合classes中
        Set<Class<?>> classes = ClassUtil.getClassSet("com.cloud.productprovider.composite");
        Object instance = null;
        try {
            //过滤有@ProductVersion标签的类
            instance = classes.stream().filter(clazz -> clazz.isAnnotationPresent(ProductVersion.class))
                    //过滤实现了ProductService接口的类
                    .filter(clazz -> ProductService.class.isAssignableFrom(clazz))
                    //找出版本号大的类,并实例化为对象
                    .max(Comparator.comparingInt(clazz -> clazz.getAnnotation(ProductVersion.class).value()))
                    .get().newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return (ProductService)instance;
//        return new ProductDetail();
    }

以后如果再有需求变更,只需要在该包下再增加一个新的实现类,打上@ProductVersion标签,增加版本号就可以了。

搜索包下所有类的ClassUtil源码可以参考@Compenent,@Autowired,@PostConstruct自实现

现将总体思想总结如下

  1. Controller不与任何具体的类耦合,不论是方法的返回类型还是调用均采用接口,只在接口层面开发。调用的接口不允许使用具体的类来实例化,必须使用工厂类创建。因为一旦有需求变更,可能多个Controller会使用到该接口的服务,修改需要多处修改,而使用工厂类,即便不使用反射也只需修改一处。
  2. 工厂类创建时不创建具体的实现类,而是用反射来判断实例化哪个实现类。这样在变更需求时,我们还可以知道版本变更次数,以及无需在原始代码处做任何修改,添加实现类由标签来处理。
展开阅读全文
加载中

作者的其它热门文章

打赏
2
1 收藏
分享
打赏
0 评论
1 收藏
2
分享
返回顶部
顶部