设计模式之策略模式

142 阅读5分钟

技术解析之策略模式

1、引出

  • 在开发中遇到需要使用不同的方式进行上传以及登录
  • 采用if和switch结构可以解决,但是代码耦合度太高

2、设计模式之策略模式

  • 自定义接口

    • /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      public interface SearchStrategy {
      
          /**
           * 搜索文章
           *
           * @param keywords 关键字
           * @return {@link List <ArticleSearchDTO>} 文章列表
           */
          List<ArticleSearchDTO> searchArticle(String keywords);
      
      }
      
    • package com.lc.strategy;
      
      import com.lc.dto.UserInfoDTO;
      
      /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      public interface SocialLoginStrategy {
      
          /**
           * 登录
           *
           * @param data 数据
           * @return {@link UserInfoDTO} 用户信息
           */
          UserInfoDTO login(String data);
      
      }
      
    • package com.lc.strategy;
      
      import org.springframework.web.multipart.MultipartFile;
      
      /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      public interface UploadStrategy {
      
          /**
           * 实现文件的上传
           *
           * @param file 文件
           * @param path 上传路径
           * @return 文件地址
           */
          String uploadFile(MultipartFile file, String path);
      }
      
  • 创建实现对应接口的类

    • 创建抽象类来实现一些公共的方法

      • /**
         * 第三方登录抽象模板
         *
         * @author Const
         * @version 1.0
         * @email 1357042069@qq.com
         * @date 2022/03/06
         */
        @Service
        public abstract class AbstractSocialLoginStrategyImpl implements SocialLoginStrategy {
        
            @Autowired
            private UserAuthMapper userAuthMapper;
        
            @Autowired
            private UserInfoMapper userInfoMapper;
        
            @Autowired
            private UserRoleMapper userRoleMapper;
        
            @Autowired
            private UserDetailServiceImpl userDetailsService;
        
            @Autowired
            private HttpServletRequest request;
        
            @Override
            public UserInfoDTO login(String data) {
                // 创建登录信息
                UserDetailDTO userDetailDTO;
                // 获取第三方token信息
                SocialTokenDTO socialToken = getSocialToken(data);
                // 获取用户ip信息
                String ipAddress = IpUtils.getIpAddress(request);
                String ipSource = IpUtils.getIpSource(ipAddress);
                // 判断是否已注册
                UserAuth user = getUserAuth(socialToken);
                if (Objects.isNull(user)) {
                    // 返回数据库用户信息
                    userDetailDTO = getUserDetail(user, ipAddress, ipSource);
                } else {
                    // 获取第三方用户信息,保存到数据库返回
                    userDetailDTO = saveUserDetail(socialToken, ipAddress, ipSource);
                }
                // 判断账号是否禁用
                if (userDetailDTO.getIsDisable().equals(CommonConst.TRUE)) {
                    throw new BizException("账号已被禁用!");
                }
                // 将登录信息交给springSecurity管理
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetailDTO, null, userDetailDTO.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);
                // 返回用户信息
                return BeanCopyUtils.copyObject(userDetailDTO, UserInfoDTO.class);
            }
        
            /**
             * 获取用户信息
             *
             * @param user      用户账号
             * @param ipAddress ip地址
             * @param ipSource  ip源
             * @return {@link UserDetailDTO} 用户信息
             */
            @Transactional(rollbackFor = Exception.class)
            UserDetailDTO getUserDetail(UserAuth user, String ipAddress, String ipSource) {
                // 更新登录信息
                userAuthMapper.update(new UserAuth(), new LambdaUpdateWrapper<UserAuth>()
                        .set(UserAuth::getLastLoginTime, LocalDateTime.now())
                        .set(UserAuth::getIpAddress, ipAddress)
                        .set(UserAuth::getIpSource, ipSource)
                        .eq(UserAuth::getId, user.getId()));
                // 封装信息
                return userDetailsService.covertUserDetail(user, request);
            }
        
            /**
             * 获取用户账号
             *
             * @return {@link UserAuth} 用户账号
             */
            private UserAuth getUserAuth(SocialTokenDTO socialTokenDTO) {
                return userAuthMapper.selectOne(new LambdaQueryWrapper<UserAuth>()
                        .eq(UserAuth::getUsername, socialTokenDTO.getOpenId())
                        .eq(UserAuth::getLoginType, socialTokenDTO.getLoginType()));
            }
        
      /**
       * 获取第三方token信息
       *
       * @param data 数据
       * @return {@link SocialTokenDTO} 第三方token信息
       */
      public abstract SocialTokenDTO getSocialToken(String data);

      /**
       * 获取第三方用户信息
       *
       * @param socialTokenDTO 第三方token信息
       * @return {@link SocialUserInfoDTO} 第三方用户信息
       */
      public abstract SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO);

      /**
       * 新增用户信息
       *
       * @param socialToken token信息
       * @param ipAddress      ip地址
       * @param ipSource       ip源
       * @return {@link UserDetailDTO} 用户信息
       */
      @Transactional(rollbackFor = Exception.class)
      UserDetailDTO saveUserDetail(SocialTokenDTO socialToken, String ipAddress, String ipSource) {
          // 获取第三方用户信息
          SocialUserInfoDTO socialUserInfo = getSocialUserInfo(socialToken);
          // 保存用户信息
          UserInfo userInfo = UserInfo.builder()
                  .nickname(socialUserInfo.getNickname())
                  .avatar(socialUserInfo.getAvatar())
                  .build();
          userInfoMapper.insert(userInfo);
          // 保存账号信息
          UserAuth userAuth = UserAuth.builder()
                  .userInfoId(userInfo.getId())
                  .username(socialToken.getOpenId())
                  .password(socialToken.getAccessToken())
                  .loginType(socialToken.getLoginType())
                  .lastLoginTime(LocalDateTime.now(ZoneId.of(ZoneEnum.SHANGHAI.getZone())))
                  .ipAddress(ipAddress)
                  .ipSource(ipSource)
                  .build();
          userAuthMapper.insert(userAuth);
          // 绑定角色
          UserRole userRole = UserRole.builder()
                  .userId(userInfo.getId())
                  .roleId(RoleEnum.USER.getRoleId())
                  .build();
          userRoleMapper.insert(userRole);
          return userDetailsService.covertUserDetail(userAuth, request);
      }
  }
  • /**
     * @author Const
     * @version 1.0
     * @email 1357042069@qq.com
     * @date 2022/03/06
     */
    @Service
    public abstract class AbstractUploadStrategyImpl implements UploadStrategy {
    
  @Override
  public String uploadFile(MultipartFile file, String path) {
      try {
          // 获取文件md5加密后的值
          String md5 = FileUtils.getMd5(file.getInputStream());
          // 获取文件拓展名
          String extName = FileUtils.getExtName(file.getOriginalFilename());
          // 重新生成文件名
          String fileName = md5 + extName;
          // 判断该文件是否已经存在
          if (!exists(path + fileName)) {
              // 不存在则上传
              upload(path, fileName, file.getInputStream());
          }
          // 返回文件访问路径
          return getFileAccessUrl(path + fileName);
      } catch (Exception e) {
          e.printStackTrace();
          throw new BizException("文件上传失败!请检查文件格式与大小是否正确");
      }
  }

  /**
   * 判断文件是否存在
   *
   * @param filePath 文件路径
   * @return {@link Boolean}
   */
  public abstract Boolean exists(String filePath);


  /**
   * 上传
   *
   * @param path        路径
   * @param fileName    文件名
   * @param inputStream 输入流
   * @throws IOException io异常
   */
  public abstract void upload(String path, String fileName, InputStream inputStream) throws IOException;

  /**
   * 获取文件访问url
   *
   * @param filePath 文件路径
   * @return {@link String}
   */
  public abstract String getFileAccessUrl(String filePath);

}

  • 创建抽象类继承类来拓展剩余的方法

    • /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      @Log4j2
      @Service("esSearchStrategyImpl")
      public class EsSearchStrategyImpl implements SearchStrategy {
      
          @Autowired
          private ElasticsearchRestTemplate elasticsearchRestTemplate;
      
          @Override
          public List<ArticleSearchDTO> searchArticle(String keywords) {
              if (StringUtils.isBlank(keywords)) {
                  return new ArrayList<>();
              }
              return search(buildQuery(keywords));
          }
      
          /**
           * 搜索文章构造
           *
           * @param keywords 关键字
           * @return es条件构造器
           */
          private NativeSearchQueryBuilder buildQuery(String keywords) {
              // 条件构造器
              NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
              BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
              // 根据关键词搜索文章标题或内容
              boolQueryBuilder.must(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("articleTitle", keywords))
                      .should(QueryBuilders.matchQuery("articleContent", keywords)))
                      .must(QueryBuilders.termQuery("isDelete", CommonConst.FALSE))
                      .must(QueryBuilders.termQuery("status", ArticleStatusEnum.PUBLIC.getStatus()));
              nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
              return nativeSearchQueryBuilder;
          }
      
          /**
           * 文章搜索结果高亮
           *
           * @param nativeSearchQueryBuilder es条件构造器
           * @return 搜索结果
           */
          private List<ArticleSearchDTO> search(NativeSearchQueryBuilder nativeSearchQueryBuilder) {
              // 添加文章标题高亮
              HighlightBuilder.Field titleField = new HighlightBuilder.Field("articleTitle");
              titleField.preTags(CommonConst.PRE_TAG);
              titleField.postTags(CommonConst.POST_TAG);
              // 添加文章内容高亮
              HighlightBuilder.Field contentField = new HighlightBuilder.Field("articleContent");
              contentField.preTags(CommonConst.PRE_TAG);
              contentField.postTags(CommonConst.POST_TAG);
              contentField.fragmentSize(200);
              nativeSearchQueryBuilder.withHighlightFields(titleField, contentField);
              // 搜索
              try {
                  SearchHits<ArticleSearchDTO> search = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), ArticleSearchDTO.class);
                  return search.getSearchHits().stream().map(hit -> {
                      ArticleSearchDTO article = hit.getContent();
                      // 获取文章标题高亮数据
                      List<String> titleHighLightList = hit.getHighlightFields().get("articleTitle");
                      if (CollectionUtils.isNotEmpty(titleHighLightList)) {
                          // 替换标题数据
                          article.setArticleTitle(titleHighLightList.get(0));
                      }
                      // 获取文章内容高亮数据
                      List<String> contentHighLightList = hit.getHighlightFields().get("articleContent");
                      if (CollectionUtils.isNotEmpty(contentHighLightList)) {
                          // 替换内容数据
                          article.setArticleContent(contentHighLightList.get(contentHighLightList.size() - 1));
                      }
                      return article;
                  }).collect(Collectors.toList());
              } catch (Exception e) {
                  log.error(e.getMessage());
              }
              return new ArrayList<>();
          }
      
      }
      
    • /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      @Service("localUploadStrategyImpl")
      public class LocalUploadStrategyImpl extends AbstractUploadStrategyImpl{
      
          /**
           * 本地路径
           */
          @Value("${upload.local.path}")
          private String localPath;
      
          /**
           * 访问url
           */
          @Value("${upload.local.url}")
          private String localUrl;
      
          @Override
          public Boolean exists(String filePath) {
              return new File(localPath + filePath).exists();
          }
      
          @Override
          public void upload(String path, String fileName, InputStream inputStream) throws IOException {
              // 判断目录是否存在
              File directory = new File(localPath + path);
              if (!directory.exists()) {
                  if (!directory.mkdirs()) {
                      throw new BizException("创建目录失败!");
                  }
              }
              // 写入文件
              File file = new File(localPath + path + fileName);
              if (file.createNewFile()) {
                  BufferedInputStream bis = new BufferedInputStream(inputStream);
                  BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
                  byte[] bytes = new byte[1024];
                  int length;
                  while ((length = bis.read(bytes)) != -1) {
                      bos.write(bytes, 0, length);
                  }
                  bos.flush();
                  inputStream.close();
                  bis.close();
                  bos.close();
              }
          }
      
          @Override
          public String getFileAccessUrl(String filePath) {
              return localUrl + filePath;
          }
      }
      
    • /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      @Service("mySqlSearchStrategyImpl")
      public class MySqlSearchStrategyImpl implements SearchStrategy {
      
          @Autowired
          private ArticleMapper articleMapper;
      
          @Override
          public List<ArticleSearchDTO> searchArticle(String keywords) {
              // 校验
              if (StringUtils.isNullOrEmpty(keywords)) {
                  return new ArrayList<>();
              }
              // 搜索文章
              // select ta.* from tb_article ta where is_delete = 0 and status = 1 and (article_tile like %keyword%  or article_content like %keyword%)
              List<Article> articleList = articleMapper.selectList(new LambdaQueryWrapper<Article>()
                      .eq(Article::getIsDelete, CommonConst.FALSE)
                      .eq(Article::getStatus, ArticleStatusEnum.PUBLIC.getStatus())
                      .and(item -> item.like(Article::getArticleTitle, keywords))
                      .or()
                      .like(Article::getArticleContent, keywords));
              // 再次处理(高亮)
              return articleList.stream().map(item -> {
                  // 获取关键词第一次出现的位置
                  String articleContent = item.getArticleContent();
                  int index = articleContent.indexOf(keywords);
                  if (index != -1) {
                      // 获取关键词前面的文字
                      int preIndex = index > 25 ? index - 25 : 0;
                      String preText = articleContent.substring(preIndex, index);
                      // 获取关键词到后面的文字
                      int last = index + keywords.length();
                      int postLength = articleContent.length();
                      int postIndex = postLength > 175 ? last + 175 : last + postLength;
                      String postText = articleContent.substring(index, postIndex);
                      // 文章内容高亮
                      articleContent = (preText + postText).replaceAll(keywords, CommonConst.PRE_TAG + keywords + CommonConst.POST_TAG);
                  }
                          // 文章标题高亮
                  String articleTitle = item.getArticleTitle().replaceAll(keywords, CommonConst.PRE_TAG + keywords + CommonConst.POST_TAG);
                  return ArticleSearchDTO.builder()
                          .id(item.getId())
                          .articleTitle(articleTitle)
                          .articleContent(articleContent)
                          .build();
              }).collect(Collectors.toList());
          }
      }
      
    • /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      @Service("ossUploadStrategyImpl")
      public class OssUploadStrategyImpl extends AbstractUploadStrategyImpl {
          @Autowired
          private OssConfigProperties ossConfigProperties;
      
          @Override
          public Boolean exists(String filePath) {
              return getOssClient().doesObjectExist(ossConfigProperties.getBucketName(), filePath);
          }
      
          @Override
          public void upload(String path, String fileName, InputStream inputStream) {
              getOssClient().putObject(ossConfigProperties.getBucketName(), path + fileName, inputStream);
          }
      
          @Override
          public String getFileAccessUrl(String filePath) {
              return ossConfigProperties.getUrl() + filePath;
          }
      
          /**
           * 获取ossClient
           *
           * @return {@link OSS} ossClient
           */
          private OSS getOssClient() {
              return new OSSClientBuilder().build(ossConfigProperties.getEndpoint(), ossConfigProperties.getAccessKeyId(), ossConfigProperties.getAccessKeySecret());
          }
      
      }
      
    • /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      @Service("qqLoginStrategyImpl")
      public class QQLoginStrategyImpl extends AbstractSocialLoginStrategyImpl {
          @Autowired
          private QQConfigProperties qqConfigProperties;
          @Autowired
          private RestTemplate restTemplate;
      
          @Override
          public SocialTokenDTO getSocialToken(String data) {
              QQLoginVO qqLoginVO = JSON.parseObject(data, QQLoginVO.class);
              // 校验QQ token信息
              checkQQToken(qqLoginVO);
              // 返回token信息
              return SocialTokenDTO.builder()
                      .openId(qqLoginVO.getOpenId())
                      .accessToken(qqLoginVO.getAccessToken())
                      .loginType(LoginTypeEnum.QQ.getType())
                      .build();
          }
      
          @Override
          public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) {
              // 定义请求参数
              Map<String, String> formData = new HashMap<>(3);
              formData.put(SocialLoginConst.QQ_OPEN_ID, socialTokenDTO.getOpenId());
              formData.put(SocialLoginConst.ACCESS_TOKEN, socialTokenDTO.getAccessToken());
              formData.put(SocialLoginConst.OAUTH_CONSUMER_KEY, qqConfigProperties.getAppId());
              // 获取QQ返回的用户信息
              QQUserInfoDTO qqUserInfoDTO = JSON.parseObject(restTemplate.getForObject(qqConfigProperties.getUserInfoUrl(), String.class, formData), QQUserInfoDTO.class);
              // 返回用户信息
              return SocialUserInfoDTO.builder()
                      .nickname(Objects.requireNonNull(qqUserInfoDTO).getNickname())
                      .avatar(qqUserInfoDTO.getFigureurl_qq_1())
                      .build();
          }
      
          /**
           * 校验qq token信息
           *
           * @param qqLoginVO qq登录信息
           */
          private void checkQQToken(QQLoginVO qqLoginVO) {
              // 根据token获取qq openId信息
              Map<String, String> qqData = new HashMap<>(1);
              qqData.put(SocialLoginConst.ACCESS_TOKEN, qqLoginVO.getAccessToken());
              try {
                  String result = restTemplate.getForObject(qqConfigProperties.getCheckTokenUrl(), String.class, qqData);
                  QQTokenDTO qqTokenDTO = JSON.parseObject(CommonUtils.getBracketsContent(Objects.requireNonNull(result)), QQTokenDTO.class);
                  // 判断openId是否一致
                  if (!qqLoginVO.getOpenId().equals(qqTokenDTO.getOpenid())) {
                      throw new BizException(StatusCodeEnum.QQ_LOGIN_ERROR);
                  }
              } catch (Exception e) {
                  e.printStackTrace();
                  throw new BizException(StatusCodeEnum.QQ_LOGIN_ERROR);
              }
          }
      
      }
      
    • /**
       * @author Const
       * @version 1.0
       * @email 1357042069@qq.com
       * @date 2022/03/06
       */
      @Service("weiboLoginStrategyImpl")
      public class WeiboLoginStrategyImpl extends AbstractSocialLoginStrategyImpl {
      
          @Autowired
          private WeiboConfigProperties weiboConfigProperties;
          @Autowired
          private RestTemplate restTemplate;
      
      @Override
      public SocialTokenDTO getSocialToken(String data) {
          WeiboLoginVO weiBoLoginVO = JSON.parseObject(data, WeiboLoginVO.class);
          // 获取微博token信息
          WeiboTokenDTO weiboToken = getWeiboToken(weiBoLoginVO);
          // 返回token信息
          return SocialTokenDTO.builder()
                  .openId(weiboToken.getUid())
                  .accessToken(weiboToken.getAccess_token())
                  .loginType(LoginTypeEnum.WEIBO.getType())
                  .build();
      }

      @Override
      public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) {
          // 定义请求参数
          Map<String, String> data = new HashMap<>(2);
          data.put(SocialLoginConst.UID, socialTokenDTO.getOpenId());
          data.put(SocialLoginConst.ACCESS_TOKEN, socialTokenDTO.getAccessToken());
          // 获取微博用户信息
          WeiboUserInfoDTO weiboUserInfoDTO = restTemplate.getForObject(weiboConfigProperties.getUserInfoUrl(), WeiboUserInfoDTO.class, data);
          // 返回用户信息
          return SocialUserInfoDTO.builder()
                  .nickname(Objects.requireNonNull(weiboUserInfoDTO).getScreen_name())
                  .avatar(weiboUserInfoDTO.getAvatar_hd())
                  .build();
      }

      /**
       * 获取微博token信息
       *
       * @param weiBoLoginVO 微博登录信息
       * @return {@link WeiboTokenDTO} 微博token
       */
      private WeiboTokenDTO getWeiboToken(WeiboLoginVO weiBoLoginVO) {
          // 根据code换取微博uid和accessToken
          MultiValueMap<String, String> weiboData = new LinkedMultiValueMap<>();
          // 定义微博token请求参数
          weiboData.add(SocialLoginConst.CLIENT_ID, weiboConfigProperties.getAppId());
          weiboData.add(SocialLoginConst.CLIENT_SECRET, weiboConfigProperties.getAppSecret());
          weiboData.add(SocialLoginConst.GRANT_TYPE, weiboConfigProperties.getGrantType());
          weiboData.add(SocialLoginConst.REDIRECT_URI, weiboConfigProperties.getRedirectUrl());
          weiboData.add(SocialLoginConst.CODE, weiBoLoginVO.getCode());
          HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(weiboData, null);
          try {
              return restTemplate.exchange(weiboConfigProperties.getAccessTokenUrl(), HttpMethod.POST, requestEntity, WeiboTokenDTO.class).getBody();
          } catch (Exception e) {
              throw new BizException(StatusCodeEnum.WEIBO_LOGIN_ERROR);
          }
      }
  }

-   **创建上下文Context类来维护对应的类的引用**

    -   **创建搜索策略执行类**

        ```
        /**
         * @author Const
         * @version 1.0
         * @email 1357042069@qq.com
         * @date 2022/03/06
         */
        @Service
        public class SearchStrategyContext {

            /**
             * 搜索模式
             */
            @Value("${search.mode}")
            private String searchMode;

            @Autowired
            private Map<String, SearchStrategy> searchStrategyMap;

            /**
             * 执行搜索策略
             *
             * @param keywords 关键字
             * @return {@link List < ArticleSearchDTO >} 搜索文章
             */
            public List<ArticleSearchDTO> executeSearchStrategy(String keywords) {
                return searchStrategyMap.get(SearchModeEnum.getStrategy(searchMode)).searchArticle(keywords);
            }
        }
        ```

    -   **创建登录方式策略执行类**

        ```
        /**
         * @author Const
         * @version 1.0
         * @email 1357042069@qq.com
         * @date 2022/03/06
         */
        @Service
        public class SocialLoginStrategyContext {
            @Autowired
            private Map<String, SocialLoginStrategy> socialLoginStrategyMap;

            /**
             * 执行第三方登录策略
             *
             * @param data          数据
             * @param loginTypeEnum 登录枚举类型
             * @return {@link UserInfoDTO} 用户信息
             */
            public UserInfoDTO executeLoginStrategy(String data, LoginTypeEnum loginTypeEnum) {
                return socialLoginStrategyMap.get(loginTypeEnum.getStrategy()).login(data);
            }
        }
        ```

    -   **创建上传模式策略执行类**

        ```
        /**
         * @author Const
         * @version 1.0
         * @email 1357042069@qq.com
         * @date 2022/03/06
         */
        @Service
        public class UploadStrategyContext {

            /**
             * 上传模式
             */
            @Value("${upload.mode}")
            private String uploadMode;

            @Autowired
            private Map<String, UploadStrategy> uploadStrategyMap;

            /**
             * 上传文件
             *
             * @param file 文件
             * @param path 路径
             * @return {@link String} 文件地址
             */
            public String executeUploadStrategy(MultipartFile file, String path) {
                return uploadStrategyMap.get(UploadModeEnum.getStrategy(uploadMode)).uploadFile(file, path);
            }
        }
        ```

### 3、总结

-   策略模式在一个**算法**的**多个实现**方面起到了很好的解耦作用。

-   **步骤**

    -   创建对应接口。
    -   创建对应实现类(抽象或具体)。
    -   创建Context上下文对象来执行对应的具体实现类的方法。