《Android 源码设计模式解析与实战》——第1章,第1.2节让程序更稳定、更灵活——开闭原则

简介:

本节书摘来自异步社区《Android 源码设计模式解析与实战》一书中的第1章,第1.2节让程序更稳定、更灵活——开闭原则,作者 何红辉 , 关爱民,更多章节内容可以访问云栖社区“异步社区”公众号查看

1.2 让程序更稳定、更灵活——开闭原则
开闭原则的英文全称是Open Close Principle,缩写是OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。

软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。而产品需要升级,修改原来的代码就可能会引发其他的问题。那么,如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是,尽量遵守本章讲述的开闭原则。

勃兰特·梅耶在1988年出版的《面向对象软件构造》一书中提出这一原则——开闭原则。这一想法认为,程序一旦开发完成,程序中一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类实现,新建的类可以通过继承的方式来重用原类的代码。显然,梅耶的定义提倡实现继承,已存在的实现类对于修改是封闭的,但是新的实现类可以通过覆写父类的接口应对变化。

说了这么多,想必大家还是半懂不懂,还是让我们以一个简单示例说明一下。

在对ImageLoader进行了一次重构之后,小民的这个开源库获得了一些用户,小民第一次感受成功的快乐,对开源的热情也越发高涨起来!通过动手实现一些开源库来深入学习相关技术,不仅能够提升自我,也能更好地将这些技术运用到工作中,从而开发出更稳定、更优秀的应用,这就是小民的真实想法。

小民第一轮重构之后的ImageLoader职责单一、结构清晰,不仅获得了主管的一点肯定,还得到了用户的夸奖,算是个不错的开始。随着用户的增多,有些问题也暴露出来了,小民的缓存系统就是大家“吐槽”最多的地方。通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来已经加载过的图片将会丢失,这样重启之后就需要重新下载!这又会导致加载缓慢、耗费用户流量的问题。小民考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了。小民在和主管讨论了该问题之后就投入了编程中,下面就是小民的代码。

DiskCache.java类,将图片缓存到SD卡中:

public class DiskCache {
  static String cacheDir = "sdcard/cache/";
  // 从缓存中获取图片
  public  Bitmap get(String url) {
    return BitmapFactory.decodeFile(cacheDir + url);
  }

  // 将图片缓存到内存中
  public void put(String url, Bitmap bmp) {
    FileOutputStream fileOutputStream = null;
    try {
      fileOutputStream = new FileOutputStream(cacheDir + url);
      bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
    } catch (FileNotFoundException e) {
         e.printStackTrace();
    } final ly {
      if (fileOutputStream != null) {
        try {
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
      }
    }
  }
}
因为需要将图片缓存到SD卡中,所以,ImageLoader代码有所更新,具体代码如下:

public class ImageLoader {
  // 内存缓存
  ImageCache mImageCache = new ImageCache();
  // SD卡缓存
  DiskCache mDiskCache = new DiskCache();
  // 是否使用SD卡缓存
  boolean isUseDiskCache = false;
  // 线程池,线程数量为CPU的数量
  ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.  
  getRuntime().availableProcessors());

  public void displayImage(final String url, final ImageView imageView) {
    // 判断使用哪种缓存
    Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mImageCache.get (url);
    if (bitmap != null) {
      imageView.setImageBitmap(bitmap);
      return;
      }
     // 没有缓存,则提交给线程池进行下载
  }

  public void useDiskCache(boolean useDiskCache) {
    isUseDiskCache = useDiskCache ;
  }
}

从上述的代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如:

ImageLoader imageLoader = new ImageLoader() ;
 // 使用SD卡缓存
imageLoader.useDiskCache(true);
// 使用内存缓存
imageLoader.useDiskCache(false);

通过useDiskCache方法可以让用户设置不同的缓存,非常方便啊!小民对此很满意,于是提交给主管做代码审核。“小民,你思路是对的,但是有些明显的问题,就是使用内存缓存时用户就不能使用SD卡缓存。类似地,使用SD卡缓存时用户就不能使用内存缓存。用户需要这两种策略的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络上获取,这才是最好的缓存策略。”主管的解释真是一针见血,小民这时才如梦初醒,刚才还得意洋洋的脸上突然有些泛红……

于是小民按照主管的指点新建了一个双缓存类DoubleCache,具体代码如下:

/**
 * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片,再从SD卡中获取。
 * 缓存图片也是在内存和SD卡中都缓存一份
 */
public class DoubleCache {
   ImageCache mMemoryCache = new ImageCache();
   DiskCache mDiskCache = new DiskCache();

  // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
  public  Bitmap get(String url) {
    Bitmap bitmap = mMemoryCache.get(url);
    if (bitmap == null) {
      bitmap = mDiskCache.get(url);
    }
    return bitmap;
  }

  // 将图片缓存到内存和SD卡中
  public void put(String url, Bitmap bmp) {
    mMemoryCache.put(url, bmp);
    mDiskCache.put(url, bmp);
  }
}
我们再看看最新的ImageLoader类,代码更新也不多:

public class ImageLoader {
  // 内存缓存
  ImageCache mImageCache = new ImageCache();
  // SD卡缓存
  DiskCache mDiskCache = new DiskCache();
  // 双缓存
  DoubleCache mDoubleCache = new DoubleCache() ;
  // 使用SD卡缓存
  boolean isUseDiskCache = false;
  // 使用双缓存
  boolean isUseDoubleCache = false;
  // 线程池,线程数量为CPU的数量
  ExecutorService mExecutorService = Executors.newFixedThreadPool 
  (Runtime.getRuntime().availableProcessors());

  public void displayImage(final String url, final ImageView imageView) {
    Bitmap bmp = null;
    if (isUseDoubleCache) {
        bmp = mDoubleCache.get(url);
    } else if (isUseDiskCache) {
        bmp = mDiskCache.get(url);
     } else {
      bmp = mImageCache.get(url);
   }
  if ( bmp != null ) {
    imageView.setImageBitmap(bmp);
  }
  // 没有缓存,则提交给线程池进行异步下载图片
  }

  public void useDiskCache(boolean useDiskCache) {
    isUseDiskCache = useDiskCache ;
  }

  public void useDoubleCache(boolean useDoubleCache) {
    isUseDoubleCache = useDoubleCache ;
  }
}

通过增加短短几行代码和几处修改就完成了如此重要的功能。小民已越发觉得自己Android开发已经到了得心应手的境地,顿时感觉一阵春风袭来,小民感觉今天天空比往常敞亮许多。

“小民,你每次加新的缓存方法时都要修改原来的代码,这样很可能会引入Bug,而且会使原来的代码逻辑变得越来越复杂。按照你这样的方法实现,用户也不能自定义缓存实现呀!”到底是主管水平高,一语道出了小民这缓存设计上的问题。

我们还是来分析一下小民的程序。小民每次在程序中加入新的缓存实现时都需要修改ImageLoader类,然后通过一个布尔变量来让用户选择使用哪种缓存,因此,就使得在ImageLoader中存在各种if-else判断语句,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码变得越来越复杂、脆弱,如果小民一不小心写错了某个if条件(条件太多,这是很容易出现的),那就需要更多的时间来排除,整个ImageLoader类也会变得越来越臃肿。最重要的是,用户不能自己实现缓存注入到ImageLoader中,可扩展性差,可扩展性可是框架的最重要特性之一。

“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放——关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。”小民的主管补充到,小民听得云里雾里的。主管看小民这等反应,于是亲自“操刀”,为他画下了图1-2所示的UML图。


acb6e655ed24c2f82d1d8ea3a44fac8f0ab999c3

小民看到图1-2似乎明白些什么,但又不是很明确如何修改程序。主管看到小民这般模样,只好亲自上阵,带着小民把ImageLoader程序按照图1-2进行了一次重构,具体代码如下:

public class ImageLoader {
  // 图片缓存
  ImageCache mImageCache = new MemoryCache();
  // 线程池,线程数量为CPU的数量
  ExecutorService mExecutorService = Executors.newFixedThreadPool 
  (Runtime.getRuntime().availableProcessors());

  // 注入缓存实现
  public void setImageCache(ImageCache cache) {
    mImageCache = cache;
   }

  public void displayImage(String imageUrl, ImageView imageView) {
  Bitmap bitmap = mImageCache.get(imageUrl);
    if (bitmap != null) {
      imageView.setImageBitmap(bitmap);
      return;
      }
      // 图片没缓存,提交到线程池中下载图片
    submitLoadRequest(imageUrl, imageView);
   }

  private void submitLoadRequest(final String imageUrl,
  final ImageView imageView) {
    imageView.setTag(imageUrl);
    mExecutorService.submit(new Runnable() {

      @Override
      public void run() {
        Bitmap bitmap = downloadImage(imageUrl);
        if (bitmap == null) {
          return;
        }
        if (imageView.getTag().equals(imageUrl)) {
          imageView.setImageBitmap(bitmap);
      }
      mImageCache.put(imageUrl, bitmap);
      }
    });
  }

  public Bitmap downloadImage(String imageUrl) {
    Bitmap bitmap = null;
    try {
      URL url = new URL(imageUrl);
      final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      bitmap = BitmapFactory.decodeStream(conn.getInputStream());
      conn.disconnect();
    } catch (Exception e) {
    e.printStackTrace();
    }

  return bitmap;
  }
}

经过这次重构,没有了那么多的if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰、简单了很多,小民对主管的崇敬之情又“泛滥”了起来。需要注意的是,这里的ImageCache类并不是小民原来的那个ImageCache,这次重构程序,主管把它提取成一个图片缓存的接口,用来抽象图片缓存的功能,我们看看该接口的声明:

public interface ImageCache {
  public  Bitmap get(String url);
  public void put(String url, Bitmap bmp);
}

ImageCache接口简单定义了获取、缓存图片两个函数,缓存的key是图片的url,值是图片本身。内存缓存、SD卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现:

// 内存缓存MemoryCache类
public class MemoryCache implements ImageCache {
  private LruCache<String, Bitmap> mMemeryCache;

  public MemoryCache() {
    // 初始化LRU缓存
  }

   @Override
  public  Bitmap get(String url) {
    return mMemeryCache.get(url);
  }

  @Override
  public void put(String url, Bitmap bmp) {
    mMemeryCache.put(url, bmp);
   }
}

// SD卡缓存DiskCache类
public class DiskCache implements ImageCache {
   @Override
  public  Bitmap get(String url) {
    return null/* 从本地文件中获取该图片 */;
  }

  @Override
  public void put(String url, Bitmap bmp) {
    // 将Bitmap写入文件中
  }
}

// 双缓存DoubleCache类
public class DoubleCache implements ImageCache{
  ImageCache mMemoryCache = new MemoryCache();
  ImageCache mDiskCache = new DiskCache();

  // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
  public Bitmap get(String url) {
    Bitmap bitmap = mMemoryCache.get(url);
    if (bitmap == null) {
      bitmap = mDiskCache.get(url);
    }
    return bitmap;
   }

  // 将图片缓存到内存和SD卡中
  public void put(String url, Bitmap bmp) {
    mMemoryCache.put(url, bmp);
    mDiskCache.put(url, bmp);
  }
}

细心的读者可能注意到了,ImageLoader类中增加了一个etImageCache(ImageCache cache)函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面就看看用户是如何设置缓存实现的:

ImageLoader imageLoader = new ImageLoader() ;
    // 使用内存缓存
imageLoader.setImageCache(new MemoryCache());
    // 使用SD卡缓存
imageLoader.setImageCache(new DiskCache());
    // 使用双缓存
imageLoader.setImageCache(new DoubleCache());
    // 使用自定义的图片缓存实现
imageLoader.setImageCache(new ImageCache() {

   @Override
   public void put(String url, Bitmap bmp) {
        // 缓存图片
      }

      @Override
      public Bitmap get(String url) {
        return null/*从缓存中获取图片*/;
      }
    });

在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们的一个特点是,都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache cache)注入到ImageLoader中,这样ImageLoader就实现了千变万化的缓存策略,且扩展这些缓存策略并不会导致ImageLoader类的修改。经过这次重构,小民的ImageLoader已经基本算合格了。咦!这不就是主管说的开闭原则么!“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。而遵循开闭原则的重要手段应该是通过抽象……”小民细声细语地念叨着,陷入了思索中……

开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的。当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以便使代码恢复到正常的“进化”过程,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。

相关文章
|
3月前
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
165 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
7月前
|
设计模式 存储 缓存
「全网最细 + 实战源码案例」设计模式——享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在减少大量相似对象的内存消耗。通过分离对象的内部状态(可共享、不变)和外部状态(依赖环境、变化),它有效减少了内存使用。适用于存在大量相似对象且需节省内存的场景。模式优点包括节省内存和提高性能,但会增加系统复杂性。实现时需将对象成员变量拆分为内在和外在状态,并通过工厂类管理享元对象。
245 92
|
6月前
|
人工智能 API 开发者
HarmonyOS Next~鸿蒙应用框架开发实战:Ability Kit与Accessibility Kit深度解析
本书深入解析HarmonyOS应用框架开发,聚焦Ability Kit与Accessibility Kit两大核心组件。Ability Kit通过FA/PA双引擎架构实现跨设备协同,支持分布式能力开发;Accessibility Kit提供无障碍服务构建方案,优化用户体验。内容涵盖设计理念、实践案例、调试优化及未来演进方向,助力开发者打造高效、包容的分布式应用,体现HarmonyOS生态价值。
288 27
|
6月前
|
数据采集 JSON 数据可视化
JSON数据解析实战:从嵌套结构到结构化表格
在信息爆炸的时代,从杂乱数据中提取精准知识图谱是数据侦探的挑战。本文以Google Scholar为例,解析嵌套JSON数据,提取文献信息并转换为结构化表格,通过Graphviz制作技术关系图谱,揭示文献间的隐秘联系。代码涵盖代理IP、请求头设置、JSON解析及可视化,提供完整实战案例。
407 4
JSON数据解析实战:从嵌套结构到结构化表格
|
6月前
|
XML JavaScript Android开发
【Android】网络技术知识总结之WebView,HttpURLConnection,OKHttp,XML的pull解析方式
本文总结了Android中几种常用的网络技术,包括WebView、HttpURLConnection、OKHttp和XML的Pull解析方式。每种技术都有其独特的特点和适用场景。理解并熟练运用这些技术,可以帮助开发者构建高效、可靠的网络应用程序。通过示例代码和详细解释,本文为开发者提供了实用的参考和指导。
165 15
|
6月前
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
|
6月前
|
数据采集 机器学习/深度学习 存储
可穿戴设备如何重塑医疗健康:技术解析与应用实战
可穿戴设备如何重塑医疗健康:技术解析与应用实战
216 4
|
7月前
|
设计模式 存储 算法
「全网最细 + 实战源码案例」设计模式——命令模式
命令模式(Command Pattern)是一种行为型设计模式,将请求封装成独立对象,从而解耦请求方与接收方。其核心结构包括:Command(命令接口)、ConcreteCommand(具体命令)、Receiver(接收者)和Invoker(调用者)。通过这种方式,命令的执行、撤销、排队等操作更易扩展和灵活。 适用场景: 1. 参数化对象以操作。 2. 操作放入队列或远程执行。 3. 实现回滚功能。 4. 解耦调用者与接收者。 优点: - 遵循单一职责和开闭原则。 - 支持命令组合和延迟执行。 - 可实现撤销、恢复功能。 缺点: - 增加复杂性和类数量。
213 14
「全网最细 + 实战源码案例」设计模式——命令模式
|
7月前
|
设计模式 存储 Java
「全网最细 + 实战源码案例」设计模式——责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,允许将请求沿着处理者链进行发送。每个处理者可以处理请求或将其传递给下一个处理者,从而实现解耦和灵活性。其结构包括抽象处理者(Handler)、具体处理者(ConcreteHandler)和客户端(Client)。适用于不同方式处理不同种类请求、按顺序执行多个处理者、以及运行时改变处理者及其顺序的场景。典型应用包括日志处理、Java Web过滤器、权限认证等。
138 13
「全网最细 + 实战源码案例」设计模式——责任链模式
|
6月前
|
机器学习/深度学习 人工智能 Java
Java机器学习实战:基于DJL框架的手写数字识别全解析
在人工智能蓬勃发展的今天,Python凭借丰富的生态库(如TensorFlow、PyTorch)成为AI开发的首选语言。但Java作为企业级应用的基石,其在生产环境部署、性能优化和工程化方面的优势不容忽视。DJL(Deep Java Library)的出现完美填补了Java在深度学习领域的空白,它提供了一套统一的API,允许开发者无缝对接主流深度学习框架,将AI模型高效部署到Java生态中。本文将通过手写数字识别的完整流程,深入解析DJL框架的核心机制与应用实践。
344 3

推荐镜像

更多