使用簡單工廠加接口加適配器模式來遵照開閉原則

咱們在平時開發中遇到最多的問題,無異於實體類屬性的變化,可能咱們開發出來的接口跟前端要的字段不少不同,或者需求變動,須要返回的不少內容不同。html

假設咱們如今有這麼一個需求,返回一個配件的詳細信息,也許咱們以前返回的格式以下前端

{
"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);
}

需求變動前,咱們定義的配件實體類以下app

@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以下(有刪減)ide

@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()方法this

dao和mapper省略......lua

此時要使用新類來跑新需求,咱們須要變動工廠的實現類url

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

如此咱們不須要修改任何原始代碼就完成了需求變動,Controller也無需作修改。徹底符合開閉原則,同時遵照了依賴倒置原則,李氏替換原則,接口隔離原則。spa

最後返回的結果以下.net

{
"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. 工廠類建立時不建立具體的實現類,而是用反射來判斷實例化哪一個實現類。這樣在變動需求時,咱們還能夠知道版本變動次數,以及無需在原始代碼處作任何修改,添加實現類由標籤來處理。
相關文章
相關標籤/搜索