-
2022-01-26 16:56:31
总结设计原则其实很早以前就在想了,但是起初我认为固定的原则会局限人的创新思维,陈旧的定律不一定是最好的。实际上随着代码量的沉积,你会发现无形之中你的程序设计会和这六大原则不谋而合。大道至简,前人走过的路,可能也是你将要走的路。
原则一:单一职责
这一块不管你写前端还是后端的项目,你会明白,所谓的设计模式(如MVC,MCP)就是帮助你实现部分代码的单一职责,就像是流水线上工作流程拆分,很多简单的步骤实现庞大的逻辑。当职责变得单一时,复用性提高,个体业务逻辑难度降低。这个原则算是应用最广,也最显而易见的。
原则二:里氏替换
这一原则主要针对继承,子类必须能完美的替换父类,且不影响业务逻辑。
这里只需要注意两件事:
- 1.避免子类过度重写而失去父类原有能力和属性。
- 2.避免子类的独有业务逻辑和业务属性侵入父类。(这一块深海颇有感触,当子类业务侵入父类时,不仅使得里氏替换难以实现,而且违背了单一职责的原则和依赖倒置原则)
原则三:依赖倒置
- 1.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
- 2.抽象不应该依赖于具体,具体应该依赖于抽象。
关于这个原则众说纷纭,这里理解成 AOP(面向切面)也可以。
但深海认为这个原则主要注意两个区分:类的高低层级区分和逻辑的具体与抽象区分。
不要倒行逆施,可能是依赖倒置的初衷。
原则四:接口隔离
- 1.一个类,不应该被强迫实现一些他们不会使用的接口。
- 2.一个类对另一个类的依赖应该建立在最小的接口上。
应该把胖接口中的方法分组,然后用多个接口替代它,每个接口服务于一个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好很多。
上面的解释时比较官方的说法,深海把他换成具体的实现:
- 1.假如某类实现了一个接口,却用不到这个接口的任何方法和属性,那么就是程序设计上出了问题,去除强制,或者拆分该接口。
- 2.一个接口不应该具有多种业务的功能,更不要说交叉的业务,这里违背单一职责的同时,也违背了接口隔离的原则,接口一定是最小单元的抽象,这样使得该接口的功能和职责十分清晰。
原则五:迪米特法则
一个对象应该对其他对象保持最少的了解。
深海认为可以理解为对象隔离,正好和接口隔离想呼应。
通俗一点说就是解耦。为什么要解耦?或者说迪米特原则解决了什么问题呢?
假如对象耦合过高,那么一个对象就可能因为另一个对象的改变而发生较大的改变,这样的程序出问题的概率和业务的扩展难度将会大大提高。
原则六:开放封闭
对扩展开放,对修改封闭。
这个原则也是众说纷纭,有人说时宏观定义,有人说时微观定义。
其实深海认为该原则主要强调两件事:
- 1.当新增业务或者逻辑时,避免对旧代码的修改,应当在旧代码的基础上扩展实现。
- 2.旧代码应当具有扩展能力,也就是 从设计程序的时候,就应当考虑到将来的业务扩充。
更多相关内容 -
设计模式之六大原则详解,Markdown笔记
2020-11-18 16:34:35详细介绍了设计模式六大原则,配有示例代码和图片,有开闭原则,单一职责原则,里氏替换原则,依赖倒置原则,接口隔离原则,迪米特法则等等。 -
设计模式+六大原则pdf
2019-04-23 21:40:42孙玉山主编的设计模式所有设计模式+体系结构题目案例源码 -
设计模式六大原则
2018-03-06 10:06:57对设计模式六大原则的一点总结,欢迎免费下载。 设计模式六大原则(1):单一职责原则 设计模式六大原则(2):里氏替换原则 设计模式六大原则(3):依赖倒置原则 设计模式六大原则(4):接口隔离原则 设计... -
设计模式六大原则(1):单一职责原则
2019-08-09 01:28:55NULL 博文链接:https://lijie-insist.iteye.com/blog/2190970 -
JAVA设计模式的六大原则
2017-03-09 15:08:42设计模式的六大原则 -
英语学习的六大原则
2020-12-17 07:50:04英语学习的六大原则可能不会让你取代另一个成功者,但英语学习的六大原则却可以给你带来新的生命力和创造...该文档为英语学习的六大原则,是一份很不错的参考资料,具有较高参考价值,感兴趣的可以下载看看 -
优化商品组合的六大原则
2020-12-21 14:17:05这一款整理发布的优化商品组合的六大原则,适合超市管理人员学习参考超市管理分类中的优化商品...该文档为优化商品组合的六大原则,是一份很不错的参考资料,具有较高参考价值,感兴趣的可以下载看看 -
设计模式的六大原则
2015-11-24 20:06:45设计模式的学习,可以增强自己的代码复用意识。 同时,也可以清晰地表达...本文将介绍设计模式的六大原则: • 单一职责原则; • 里氏替换原则; • 依赖倒置原则; • 接口隔离原则; • 迪米特法则; • 开闭原则; -
设计模式六大原则.doc
2020-05-06 22:41:28设计模式六大原则(1):单一职责原则 设计模式六大原则(2):里氏替换原则 设计模式六大原则(3):依赖倒置原则 设计模式六大原则(4):接口隔离原则 设计模式六大原则(5):迪米特法则 设计模式六大原则... -
(二)设计模式的六大原则
2021-01-08 10:37:50(二)设计模式的六大原则4.依赖倒置原则5.接口隔离原则6.开闭原则 4.依赖倒置原则 依赖倒置原则:高层模块不应该依赖于低层模块,应该通过抽象依赖,而不是依赖低层,这里的抽象指的是抽象类/接口,细节指的就是... -
创建易用 PCB 设计的六大原则-综合文档
2021-05-24 08:58:45创建易用 PCB 设计的六大原则 -
软件设计六大原则
2021-02-20 17:37:28一、七大设计原则 开闭原则 依赖倒置原则 单一职责原则 接口隔离原则 迪米特法则(最少知道原则) 里氏替换原则 合成/复用原则(组合/复用原则) 二、开闭原则 定义:一个软件实体如类、模块和函数应该对扩展开放...一、六大设计原则
- 开闭原则
- 依赖倒置原则
- 单一职责原则
- 接口隔离原则
- 迪米特法则(最少知道原则)
- 里氏替换原则
二、开闭原则
- 定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
- 用抽象构建框架,用实现扩展细节
- 优点:提高软件系统的可复用性和可维护性
如上图,接口ICourse定义了id、name、price,JavaCourse实现了它,现在需要获取到折扣价格,不能直接去修改ICourse接口以及JavaCourse基类(对修改关闭
),应该新建一个JavaDiscountCourse继承JavaCourse去实现功能(对扩展开放
)。
三、依赖倒置原则
- 定义:高层模块不应该依赖底层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节;细节应该依赖抽象
- 针对接口编程,不要针对实现编程
- 优点:可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险
场景:Geely需要学习Java、python、C等课程
面向实现编程
public class Geely { public void studyJavaCourse(){ System.out.println("Geely 在学习 Java 课程"); } public void studyPythonCourse(){ System.out.println("Geely 在学习 Python 课程"); } public void studyCCourse(){ System.out.println("Geely 在学习 C 课程"); } }
public class Test { public static void main(String[] args) { Geely geely = new Geely(); geely.studyCCourse(); geely.studyJavaCourse(); geely.studyPythonCourse(); }
以上面向实现编程,当Geely想要学习其他课程,需要修改Geely类,同时Test类也要增加相应的代码。
面向接口编程
public interface Icourse { public void studyCourse(); } public class CCourse implements Icourse { public void studyCourse() { System.out.println("Geely 在学习 C 课程"); } } public class JavaCourse implements Icourse { public void studyCourse() { System.out.println("Geely 在学习 Java 课程"); } } public class Geely { private Icourse icourse; public Geely() { } //通过setter注入 public void setIcourse(Icourse icourse) { this.icourse = icourse; } // 通过构造器注入 public Geely(Icourse icourse) { this.icourse = icourse; } public void studyImoocCourse(Icourse icourse){ icourse.studyCourse(); } } public class Test { //方法传参 public static void main(String[] args) { Geely geely = new Geely(); geely.studyImoocCourse(new JavaCourse()); geely.studyImoocCourse(new FECourse()); } //通过构造器注入的方式调用方法 public static void main(String[] args) { Geely geely = new Geely(new JavaCourse()); geely.studyImoocCourse(); geely = new Geely(new FECourse()); geely.studyImoocCourse(); } //通过setter注入的方式 public static void main(String[] args) { Geely geely = new Geely(); JavaCourse javaCourse = new JavaCourse(); geely.setIcourse(javaCourse); geely.studyImoocCourse(); } }
Geely不依赖具体的Course,Geely想学任何课,可以不去改动Geely、ICourse,直接新建一个类实现ICourse。四、单一职责原则
- 定义:不要存在多于一个导致类变更的原因
- 一个类/接口/方法只负责一项职责
- 优点:降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险
五、接口隔离原则
- 定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
- 一个类对一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
- 注意适度原则,一定要适度
- 优点:符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性
六、迪米特原则
- 定义:一个对象应该对其他对象保持最少的了解,又叫最少知道原则
- 尽量降低类与类之间的耦合
- 优点:降低类之间的耦合
- 强调只和朋友交流,不和陌生人说话
- 朋友:出现在成员变量、方法的输入、输出参数中的类成为成员朋友类,而出现在方法内部的类不属于朋友类。
public class Boss { // 对TeamLeader 下指令需要查询课程的数量 public void commandCheckNumber(TeamLeader teamLeader){ List<Course> courseList = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } teamLeader.checkNumberOfCourses(courseList); } } public class TeamLeader { public void checkNumberOfCourses(List<Course> courseList){ System.out.println("在线课程的数量是:" + courseList.size()); } } public class Course { } /** * Test 测试类 */ public class Test { public static void main(String[] args) { Boss boss = new Boss(); TeamLeader teamLeader = new TeamLeader(); boss.commandCheckNumber(teamLeader); } }
以上违背了迪米特原则,Boss想要课程数量,但是Boss又创建了Course类(陌生人
),Boss应该只与TeamLeader交流(朋友:入参
),TeamLeader应该直接返回结果。public class Boss { public void commandCheckNumber(TeamLeader teamLeader){ teamLeader.checkNumberOfCourses(); } } public class TeamLeader { public void checkNumberOfCourses(){ List<Course> courseList = new ArrayList<Course>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } System.out.println("在线课程的数量是:" + courseList.size()); } }
在写代码的时候,我们需要注意区别哪些是“朋友”,哪些是“陌生人”,遵循好迪米特原则。七、里氏替换原则
- 定义:所有引用基类的地方必须能透明化地使用其子类的对象
- 即子类可以扩展父类的功能,但是不能改变父类原有的功能。也就是说,在子类继承父类的时候,除了添加新的方法完成新增功能之外,尽量不要重写父类的方法
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更加宽松
-
面向对象的六大原则
2019-02-13 18:36:34一、面向对象的六大原则 现在的编程的主流语言基本上都是面向对象的。我们知道,面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开闭式...一、面向对象的六大原则
现在的编程的主流语言基本上都是面向对象的。我们知道,面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开闭式原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和数据的封装,这是为了提高程序的内聚性,而其他五个原则是通过抽象来实现的,目的是为了降低程序的耦合性以及提高可扩展性。在应用的开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让系统能够拥抱变化。拥抱变化也意味着在满足需求且不破坏系统稳定性的前提下保持高可扩展性、高内聚、低耦合,在经历了各个版本的变更之后依然能够保持清晰、灵活、稳定的系统架构。当然,这是一种比较理想的情况,由于各种各样的原因(开发水平差、工期短、产品奇葩需求等),我们的应用可能会变得难以维护,但是我们必须向更好的方向努力,那么遵循面向对象的六大原则就是我们走向灵活软件之路所迈出的第一步。本篇文章将以实现一个简单的图片加载框架为例,来说明面向对象六大原则的应用。
二、图片加载框架需求分析
需求描述
1、需要根据Url将图片加载到对应的ImageView上;
2、需要图片缓存功能;
3、框架提供的API尽可能简单,方便使用;
4、框架能够灵活的扩展,比如灵活的改变缓存功能、下载图片方法等;
需求分析
分析一下需求,我们至少需要使用以下技术:
1、首先,我们需要根据Url下载图片,这里暂定使用UrlConnection;
2、为了不阻塞UI线程,我们下载图片需要在子线程中执行,方便起见,我们直接使用线程池;
3、在子线程中下载图片后,我们需要将图片显示到ImageView上,由于Android的特性,我们需要在UI线程中更新UI,所以这里需要使用Handler来切换线程到UI线程中显示图片;
4、图片需要缓存功能,一般图片缓存需要有内存缓存和文件缓存,内存缓存就是将下载好的图片保存在内存中,下次使用时直接从内存中获取,这里采用Lru算法来控制图片缓存,文件缓存即将图片缓存到文件中,下次使用直接从文件中获取,相对来说,内存缓存会占用较多的内存,但是效率较高。
三、源码实现
我们首先只实现功能,而不管代码的好坏,后面我们再根据六大原则对代码进行优化,看看遵循六大原则究竟能够带来哪些好处。
public class ImageLoader { final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); private LruCache<String, Bitmap> mLruCache = new LruCache<>(maxMemory / 4); // 内存缓存 ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); // 下载图片使用的线程池 private Handler mDealHandler = new Handler() { // 处理下载好图片的Handler @Override public void handleMessage(Message msg) { if (msg != null && msg.obj != null) { ImageResponse imageResponse = (ImageResponse) msg.obj; imageResponse.imageView.setImageBitmap(imageResponse.bitmap); // 将图片显示到ImageView上 mLruCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存到缓存中 } } }; /** * 加载图片 * @param imageView * @param imageUrl */ public void displayImage(final ImageView imageView, final String imageUrl) { if (imageView == null || TextUtils.isEmpty(imageUrl)) { return; } Bitmap cacheBitmap = mLruCache.get(imageUrl); if (cacheBitmap == null) { // 使用缓存失败 mExecutorService.submit(new Runnable() { // 使用线程池下载图片 @Override public void run() { Bitmap bitmap = downLoadImage(imageUrl); // 下载图片 if (bitmap != null) { ImageResponse imageResponse = new ImageResponse(); // 图片下载好后,将信息封装成Message发送给Handler处理 imageResponse.bitmap = bitmap; imageResponse.imageUrl = imageUrl; imageResponse.imageView = imageView; Message message = Message.obtain(); message.obj = imageResponse; mDealHandler.sendMessage(message); } } }); } else { imageView.setImageBitmap(cacheBitmap); } } // 下载图片 public Bitmap downLoadImage(final String imageUrl) { Bitmap bitmap = null; try { URL url = new URL(imageUrl); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); } return bitmap; } public static class ImageResponse { public Bitmap bitmap; public ImageView imageView; public String imageUrl; } }
使用方法:
ImageLoader mImageLoader = new ImageLoader(); mImageLoader.displayImage(imageView, imageUrl);
在上面的代码中,我们不管三七二十一,将所有的功能都堆积到了ImageLoader这个类中,ImageLoader不仅承担了提供Api接口的功能,同时还承担了具体实现细节的功能,比如缓存功能、下载图片功能、显示图片功能等等,这样虽然也能实现图片加载的效果,但是确会让我们的代码变得臃肿复杂并且难以维护,不说别人看不懂,可能过个几天后连自己都看不懂了,所以,我们自然而然的要想到,我们需要根据功能对类进行细分,将某些相关性很强的功能分到独立的类中去处理,比如,图片的缓存我们可以划分为一个类,图片的下载可以作为另外一个类,通过类的划分,我们可以让代码变得清晰,这也就是面向对象六大原则中的第一个原则----单一职责原则。
优化代码第一步----单一职责原则----高内聚
单一职责原则的定义是:就一个类而言,应该只有一个引起它变化的原因。简单来说,一个类应该是一组相关性很高的函数和数据的封装。因为单一职责的划分界限并不是那么清晰,每个人的理解不一样,这就导致了不同的人划分的类承担的职责也不一样,就图片加载的例子来说,可能有的人就认为整个图片加载是一组相关性很高的功能,于是将其放入在一个类中处理。一般来说,我们首先需要具备单一职责原则的思想,如果发现一个类承担了太多的功能,这个时候就要考虑将某些功能划分到其他类中去处理,具体的划分细节要平开发者的个人经验。
下面,我们利用单一职责原则的思想对ImageLoader进行改造,根据功能来说,下载图片和图片缓存应该属于单独的功能,我们可以分别用两个类来实现,然后ImageLoader调用这两个类即可:
public class ImageLoader { ImageCache mImageCache = new ImageCache(); RequestImage mRequestImage = new RequestImage(); private Handler mDealHandler = new Handler() { // 处理下载好图片的Handler @Override public void handleMessage(Message msg) { if (msg != null && msg.obj != null) { ImageResponse imageResponse = (ImageResponse) msg.obj; imageResponse.imageView.setImageBitmap(imageResponse.bitmap); // 将图片显示到ImageView上 mImageCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存到缓存中 } } }; /** * 加载图片 * @param imageView * @param imageUrl */ public void displayImage(final ImageView imageView, final String imageUrl) { if (imageView == null || TextUtils.isEmpty(imageUrl)) { return; } Bitmap cacheBitmap = mImageCache.get(imageUrl); if (cacheBitmap == null) { // 使用缓存失败 mRequestImage.requestImage(imageView, imageUrl, mDealHandler); } else { imageView.setImageBitmap(cacheBitmap); } } public static class ImageResponse { public Bitmap bitmap; public ImageView imageView; public String imageUrl; } }
public class RequestImage { ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); // 下载图片使用的线程池 public void requestImage(final ImageView imageView, final String imageUrl, final Handler handler) { mExecutorService.submit(new Runnable() { // 使用线程池下载图片 @Override public void run() { Bitmap bitmap = downLoadImage(imageUrl); // 下载图片 if (bitmap != null) { ImageLoader.ImageResponse imageResponse = new ImageLoader.ImageResponse(); // 图片下载好后,将信息封装成Message发送给Handler处理 imageResponse.bitmap = bitmap; imageResponse.imageUrl = imageUrl; imageResponse.imageView = imageView; Message message = Message.obtain(); message.obj = imageResponse; handler.sendMessage(message); } } }); } // 下载图片 public Bitmap downLoadImage(final String imageUrl) { Bitmap bitmap = null; try { URL url = new URL(imageUrl); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); } return bitmap; } }
public class ImageCache { final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); private LruCache<String, Bitmap> mLruCache = new LruCache<>(maxMemory / 4); // 内存缓存 public void put(String url, Bitmap bitmap) { if (!TextUtils.isEmpty(url) && bitmap != null) { mLruCache.put(url, bitmap); } } public Bitmap get(String url) { return mLruCache.get(url); } }
可以看到,经过单一职责原则的优化,我们的ImageLoader变得简洁了许多,也更加易于维护,比如,我们想修改缓存相关功能,只需要查看ImageCache类即可。我们并没有对具体功能代码做修改,只是将相关性较高的功能单独提取了出来,封装成类。单一职责原则也就是我们经常说的高内聚。
更高的可扩展性----开闭式原则
经过单一职责原则的优化,我们的ImageLoader变得不再臃肿,但是,还远远没有达到要求,我们在需求中说过,ImageLoader需要支持内存缓存和文件缓存,现在我们只支持了内存缓存,可能你会说,在原来的基础上加上文件缓存不就可以了吗?我们先来看看,直接在ImageLoader中加入文件缓存后的代码会是什么样的:
MemoryImageCache mMemoryImageCache = new MemoryImageCache(); DiskImageCache mDiskImageCache = new DiskImageCache(App.getAppContext()); private int mCacheType = 1; public final static int MEMORY_CACHE_TYPE = 1; public final static int DISK_CACHE_TYPE = 2; /** * 设置缓存类型 * @param type */ public void setCacheType(int type) { mCacheType = type; }
首先,我们添加了一个变量mCacheType来保存缓存类型,并提供了一个setCacheType方法来设置缓存类型,然后在使用缓存时,我们需要根据保存的缓存类型来确定到底该使用哪个缓存:
switch (mCacheType) { case MEMORY_CACHE_TYPE: mMemoryImageCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存到内存缓存中 break; case DISK_CACHE_TYPE: mDiskImageCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存在磁盘缓存中 break; } Bitmap cacheBitmap = null; switch (mCacheType) { case MEMORY_CACHE_TYPE: cacheBitmap = mMemoryImageCache.get(imageUrl); // 从内存缓存中获取图片 break; case DISK_CACHE_TYPE: cacheBitmap = mDiskImageCache.get(imageUrl); // 从磁盘缓存中获取图片 break; }
可以看到,无论是获取缓存还是保存缓存,都需要进行判断,不仅麻烦还容易出错,试想一下,如果这时我们需要ImageLoader支持双缓存,即优先使用内存缓存,当内存缓存中没有时,我们需要使用文件缓存(在实际应用中,图片加载框架一般都是双缓存的,这样既能保证加载速度,又能尽可能的减少图片的下载),那么应该怎么办呢?难道还是和添加文件缓存一样,在ImageLoader中再加入一种缓存内型?显示,这种方式是不合理的,不仅让代码变得难以维护,而且扩展性极差,这个时候,开闭式原则就派上用场了。
开闭式原则的定义:软件中的对象应该对扩展是开放的,但是,对修改是关闭的。软件开发过程中,需求是不断变化的,因为变化、升级和维护等原因需要对原有的软件代码进行修改,而一旦对原有的代码进行修改,就有可能影响到原有的模块,引起bug,因此,在软件开发过程中,我们应该尽可能通过扩展的方式实现变化,而不是修改原有的代码,当然,这是一种理想的情况,在实际的软件开发中,完全通过扩展的方式实现变化是不现实的。那么,如何让对象对扩展是开放的呢?回顾一下面向对象的三大特性:封装、继承和多态,在单一职责原则中,我们使用到了封装的特性,而继承和多态还没有使用,现在是派上用场的时候了,继承和多态的精髓在于抽象,一般来说,高层次模块不应该直接依赖低层次模块的实现细节,而应该依赖其抽象,在ImageLoader这个例子中,ImageLoader直接依赖了具体的缓存类,这就让ImageLoader和具体的缓存类紧紧的耦合在一起,我们一直强调软件开发要做到高内聚、低耦合,高内聚通过单一职责原则可以达到,而低耦合则需要依赖抽象。
通过分析,我们不难发现无论是哪种缓存类,其都需要提供获取缓存和放入缓存的功能,即get和put,既然这样,我们为什么不将其抽象成接口呢?
/** * 缓存图片接口类 * 这个接口定义了缓存图片所需要的基本功能,缓存图片以及从缓存中获取图片 */ public interface ImageCache { public void put(String url, Bitmap bitmap); // 缓存图片 public Bitmap get(String url); // 获取图片 }
然后具体的缓存类实现ImageCache接口:
/** * ImageLoader内存缓存类,采用Lru算法 */ public class MemoryImageCache implements ImageCache { final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); private LruCache<String, Bitmap> mLruCache = new LruCache<>(maxMemory / 4); @Override public void put(String url, Bitmap bitmap) { if (!TextUtils.isEmpty(url) && bitmap != null) { mLruCache.put(url, bitmap); } } @Override public Bitmap get(String url) { return mLruCache.get(url); } }
/** * ImageLoader文件缓存 */ public class DiskImageCache implements ImageCache { private Context mContext; public DiskImageCache() { this.mContext = App.getContext(); } @Override public void put(String url, Bitmap bitmap) { FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(DiskCacheFileUtils.getImageCacheFile(url, this.mContext)); bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); fileOutputStream.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { CloseUtils.close(fileOutputStream); } } @Override public Bitmap get(String url) { String imageCachePath = DiskCacheFileUtils.getImageCachePath(url, this.mContext); if (DiskCacheFileUtils.isFileExists(imageCachePath)) { return BitmapFactory.decodeFile(imageCachePath); } return null; } }
/** * ImageLoader双缓存类 */ public class DoubleImageCache implements ImageCache { private ImageCache mMemoryImageCache; private ImageCache mDiskImageCache; public DoubleImageCache() { mMemoryImageCache = new MemoryImageCache(); mDiskImageCache = new DiskImageCache(); } @Override public void put(String url, Bitmap bitmap) { mMemoryImageCache.put(url, bitmap); mDiskImageCache.put(url, bitmap); } @Override public Bitmap get(String url) { Bitmap bitmap = mMemoryImageCache.get(url); if (bitmap != null) { return bitmap; } bitmap = mDiskImageCache.get(url); if (bitmap != null) { return bitmap; } return null; } }
上面的三个具体的缓存类,都实现了ImageCache接口,表示其具备了缓存能力,我们再来看一下在ImageLoader中如何设置缓存:
private ImageCache mImageCache; /** * 设置缓存 * @param imageCache(默认使用MemoryImageCache缓存) * @return */ public ImageLoader setImageCache(ImageCache imageCache) { this.mImageCache = imageCache; return this; }
使用缓存:
if (mImageCache != null) { // 将图片放入缓存 mImageCache.put(url, bitmap); } Bitmap cacheBitmap = mImageCache.get(url); // 获取缓存
可以看到,ImageLoader并没有依赖于具体的缓存实现类,而只是依赖了缓存类接口ImageCahe,通过这种方式,我们让ImageLoader具备了可以兼容任何实现了ImageCache接口的缓存类的能力,比如,现在ImageLoader想使用双缓存,只需要调用如下代码即可:
imageLoader.setImageCache(new DoubleImageCache());
再比如,我们想使用其他的缓存方案,只需要定义一个缓存类,实现ImageCache接口即可:
imageLoader.setImageCache(new ImageCache() { @Override public void put(String url, Bitmap bitmap) { // 具体的放入缓存实现 } @Override public Bitmap get(String url) { // 具体的获取缓存实现 return null; } });
通过遵循开闭式原则,我们大大提高了缓存功能的可扩展性,并且去除了ImageLoader与具体缓存类的耦合,这一切都要归功于抽象。
里氏替换原则
通过前面的优化,我们的ImageLoader已经大体上满足需求了,我们再来看一下里氏替换原则的定义:所有引用基类的地方都必须能够透明的使用其子类,这句话是什么意思呢,以我们的ImageLoader为例,所有引用到ImageCache的地方应该都可以替换成具体的子类对象。想象一下,如果我们的ImageCache中的ImageCache不能够被具体的缓存类所替换,那我们应该如何给ImageCahce设置缓存呢?难道还是使用原来的mCacheType的方式吗?显然不是,里氏替换原则就是给这类问题提供了指导原则,通过建立抽象,让高层次模块依赖于抽象类,在运行时在替换成具体的实现类,保证了系统的扩展性和灵活性。
依赖倒置原则
依赖倒置原则指的是高层次模块不应该依赖于低层次模块的具体实现,两者都应该依赖其抽象,具体如下:
1、高层次模块不应该依赖低层次模块的具体实现,两者都应该依赖其抽象;
2、抽象不应该依赖细节;
3、细节应该依赖抽象。
在面向对象语言中,抽象指的是接口或者抽象类,两者都不能被实例化,而细节指的是具体的实现类。其实一句话就可以概括,面向接口编程,或者说面向抽象编程,试想一下,如果类和类之间之间依赖细节,那么这两个类将会紧紧的耦合在一起,这就意味着,修改了其中一个类,很可能需要对另外一个类也需要进行修改,并且这样也大大限制了系统的可扩展性。依赖倒置原则提供了一种解耦方式,通过抽象让类和类之间不再依赖细节,而是在具体运行时再进行替换。以我们的ImageLoader为例,通过建立抽象类ImageCache,我们让ImageLoader不再依赖于具体的缓存类,并且能够灵活的扩展使用其他的缓存功能。从上面几个原则来看,想要让系统具备更好的可扩展性,抽象似乎成为了唯一的手段。
接口隔离原则
接口隔离原则的定义:类间的依赖关系应该建立在最小的接口之上。接口隔离原则将非常庞大、臃肿的接口拆分成更小更具体的接口。 一个接口定义的过于臃肿,则代表它的每一个实现类都要考虑所有的实现逻辑。如果一个类实现了某个接口,也就是说这个类承载了这个接口所有的功能,维护这些功能成为了自己的职责。这就无形中增加了一个类的负担。这里有一点需要说明,接口定义要小,但是要有限度,对接口细化可以增加灵活性,但是过度细化则会使设计复杂化。同时接口的使用率不高,提高了代码的维护成本。这种极端的体现就是每个接口只含有一个方法,这显然是不合适的。以前面的ImageCache为例,图片的缓存类必须提供两个功能,放入缓存和获取缓存,所以ImageCache封装了这两个抽象方法,而不应该再进行细分。我们再举一个接口隔离的例子,我们在文件缓存类中用到了FileOutputStream,在Java中,在使用了可关闭对象后,需要调用其close方法对其进行关闭:
public void put(String url, Bitmap bitmap) { FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(DiskCacheFileUtils.getImageCacheFile(url, this.mContext)); bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); fileOutputStream.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (fileOutputStream != null) { // 关闭fileOutputStream对象 try { fileOutputStream .close(); } catch (IOException e) { e.printStackTrace(); } } } }
我们可以看到这段代码的可读性非常差,各种try...catch,并且,在Java中,类似FileOutputStream这种需要关闭的对象有很多,难道我们每次使用时都要加上这种判断吗?有没有什么办法可以优化呢?答案是肯定的,在Java中,这种需要关闭的对象都继承了Closeable接口,表示这个对象是可以关闭的,Closeable接口的定义如下:
public interface Closeable extends AutoCloseable { public void close() throws IOException; }
我们可以建立一个工具类来专门处理继承了Closeable接口的类对象的关闭处理:
public class CloseUtils { public static void close(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException e) { e.printStackTrace(); } } } }
这样,我们就可以将关闭FileOutputStream的代码修改为:
CloseUtils.close(fileOutputStream);
并且这个工具类适用于关闭所用继承了Closeable接口的对象。
通过前面的学习,我们发现这几大原则可以总结为下面几个关键点:单一职责、抽象、最小化。在实际开发中,我们要灵活的运行这几大原则。
迪米特原则
迪米特原则的定义:一个对象应该对其他对象有最小的了解。迪米特原则也称作最小知道原则,即类和类直接应该建立在最小的依赖之上,一个类应该对其依赖的类有最小的了解,即只需要知道其所需要的方法即可,至于其内部具体是如何实现的则不用关心。迪米特原则的目的是减少类和类之间的耦合性,类和类之间的耦合性越大,当一个类发生修改后,会其他类造成的影响也越大。以前面的ImageLoader为例,使用者在用ImageLoader加载图片时,应该只需要和ImageLoader打交道,而不用关心具体的图片时如何下载、缓存以及显示的,并且我们在封装ImageLoader类时,还应该只将一些必要的方法设置为public方法,这些public方法表示提供给使用者使用的方法,比如加载图片和设置缓存,而对于其他的方法应该设置为private,将其隐藏起来。迪米特原则总结来说就是通过最小了解来降低类之间的耦合。
总结
在这篇文章中,我们通过设计一个简易的图片加载框架来说明面向对象六大原则的应用,六大原则指的是单一职责原则、开闭式原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和数据的封装,这是为了提高程序的内聚性,而其他五个原则是通过抽象来实现的,目的是为了降低程序的耦合性以及提高可扩展性。六大原则的目的是为了让程序达到高内聚、低耦合,提高可扩展性的目的,其实现手段是面向对象的三大特性:封装、继承以及多态。
-
揭秘外企用人的六大原则
2020-08-19 03:20:35文章简单介绍了外企用人的几大原则 -
绩效沟通的六大原则.docx
2021-10-04 22:33:44绩效沟通的六大原则.docx -
面向对象的三大特性和六大原则
2018-04-22 23:11:43一、三大特性封装: 一个类封装了数据以及操作数据的代码逻辑体。定义了数据的可访问属性(私有、公有)...二、六大原则单一功能原则 : 每个类型(包括接口和抽象)功能要求单一,只负责一件事情。 开放封闭原则...一、三大特性
- 封装: 一个类封装了数据以及操作数据的代码逻辑体。定义了数据的可访问属性(私有、公有)
- 继承 : 可以让一个类型获取另外一个类型的属性的方式。分为实现继承和接口继承
- 多态 : 类实例的一个方法在不同情形下有不同的表现形式,即不同的外在行为。使具有不同的内部结构的对象可以共享相同的外部接口。
二、六大原则
单一功能原则 : 每个类型(包括接口和抽象)功能要求单一,只负责一件事情。
开放封闭原则:一个软件实体应该对扩展开发,对修改关闭。可扩展但是不可更改。核心:用抽象构建框架,用实现类实现扩展。替换原则(里氏代换原则):子类能够替换父类,出现在父类能够出现的任何地方当使用继承时,尽量遵循历史替换原则,尽量不要去重写或者重载父类的方法, 以免破坏整个继承体系的 。 因为父类在定义或者实现某些方法时,规定了必须遵守的规则和契约。依赖原则:具体依赖抽象,上层依赖下层。核心思想是面向接口编程。两个模块之间依赖的应该是抽象(接口或抽象类)而不是细节。细节(实现类)依赖于抽象。依赖原则基于的事实:相对于实现类的多变性,抽象的东西要稳定得多,基于抽象的构架也比基于实现的架构更加稳定,且扩展性更高接口分离原则:模块间要通过具体接口分离开,而不是通过类强耦合。例如A类对B类的依赖,可以抽象接口I,B实现I,A类依赖I来实现。但是抽象接口必须功能最小化(与单一功能原则有点不谋而合)。迪米特原则:最小依赖原则,一个类对其他类尽可能少的了解,只与朋友通信。降低耦合
三、细节
单一职责原则
单一职责原则的定义是就一个类而言,应该仅有一个引起他变化的原因。也就是说一个类应该只负责一件事情。如果一个类负责了方法M1,方法M2两个不同的事情,当M1方法发生变化的时候,我们需要修改这个类的M1方法,但是这个时候就有可能导致M2方法不能工作。这个不是我们期待的,但是由于这种设计却很有可能发生。所以这个时候,我们需要把M1方法,M2方法单独分离成两个类。让每个类只专心处理自己的方法。
单一职责原则的好处如下:- 可以降低类的复杂度,一个类只负责一项职责,这样逻辑也简单很多
- 提高类的可读性,和系统的维护性,因为不会有其他奇怪的方法来干扰我们理解这个类的含义
- 当发生变化的时候,能将变化的影响降到最小,因为只会在这个类中做出修改。
开闭原则
开闭原则和单一职责原则一样,是非常基础而且一般是常识的原则。开闭原则的定义是软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是关闭的。
当需求发生改变的时候,我们需要对代码进行修改,这个时候我们应该尽量去扩展原来的代码,而不是去修改原来的代码,因为这样可能会引起更多的问题。
这个准则和单一职责原则一样,是一个大家都这样去认为但是又没规定具体该如何去做的一种原则。
开闭原则我们可以用一种方式来确保他,我们用抽象去构建框架,用实现扩展细节。这样当发生修改的时候,我们就直接用抽象了派生一个具体类去实现修改。里氏替换原则
里氏替换原则是一个非常有用的一个概念。他的定义
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。
这样说有点复杂,其实有一个简单的定义
所有引用基类的地方必须能够透明地使用其子类的对象。
里氏替换原则通俗的去讲就是:子类可以去扩展父类的功能,但是不能改变父类原有的功能。他包含以下几层意思:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类可以增加自己独有的方法。
- 当子类的方法重载父类的方法时候,方法的形参要比父类的方法的输入参数更加宽松。
- 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。
里氏替换原则之所以这样要求是因为继承有很多缺点,他虽然是复用代码的一种方法,但同时继承在一定程度上违反了封装。父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。这也导致了,如果需求变更,子类对父类的方法进行一些复写的时候,其他的子类无法正常工作。所以里氏替换法则被提出来。
确保程序遵循里氏替换原则可以要求我们的程序建立抽象,通过抽象去建立规范,然后用实现去扩展细节,这个是不是很耳熟,对,里氏替换原则和开闭原则往往是相互依存的。依赖倒置原则
依赖倒置原则指的是一种特殊的解耦方式,使得高层次的模块不应该依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。
这也是一个让人难懂的定义,他可以简单来说就是高层模块不应该依赖底层模块,两者都应该依赖其抽象
抽象不应该依赖细节
细节应该依赖抽象在Java 中抽象指的是接口或者抽象类,两者皆不能实例化。而细节就是实现类,也就是实现了接口或者继承了抽象类的类。他是可以被实例化的。高层模块指的是调用端,底层模块是具体的实现类。在Java中,依赖倒置原则是指模块间的依赖是通过抽象来发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口是来实现的。这就是俗称的面向接口编程。
我们下面有一个例子来讲述这个问题。这个例子是工人用锤子来修理东西。我们的代码如下:public class Hammer { public String function(){ return "用锤子修理东西"; } } public class Worker { public void fix(Hammer hammer){ System.out.println("工人" + hammer.function()); } public static void main(String[] args) { new Worker().fix(new Hammer()); } }
这个是一个很简单的例子,但是如果我们要新增加一个功能,工人用 螺丝刀来修理东西,在这个类,我们发现是很难做的。因为我们Worker类依赖于一个具体的实现类Hammer。所以我们用到面向接口编程的思想,改成如下的代码:
public interface Tools { public String function(); }
然后我们的Worker是通过这个接口来于其他细节类进行依赖。代码如下:
public class Worker { public void fix(Tools tool){ System.out.println("工人" + tool.function()); } public static void main(String[] args) { new Worker().fix(new Hammer()); new Worker().fix(new Screwdriver()); } }
我们的Hammer类与Screwdriver类实现这个接口
public class Hammer implements Tools{ public String function(){ return "用锤子修理东西"; } } public class Screwdriver implements Tools{ @Override public String function() { return "用螺丝刀修理东西"; } }
这样,通过面向接口编程,我们的代码就有了很高的扩展性,降低了代码之间的耦合度,提高了系统的稳定性。
接口隔离原则
接口隔离原则的定义是
客户端不应该依赖他不需要的接口
换一种说法就是类间的依赖关系应该建立在最小的接口上。这样说好像更难懂。我们通过一个例子来说明。我们知道在Java中一个具体类实现了一个接口,那必然就要实现接口中的所有方法。如果我们有一个类A和类B通过接口I来依赖,类B是对类A依赖的实现,这个接口I有5个方法。但是类A与类B只通过方法1,2,3依赖,然后类C与类D通过接口I来依赖,类D是对类C依赖的实现但是他们却是通过方法1,4,5依赖。那么是必在实现接口的时候,类B就要有实现他不需要的方法4和方法5 而类D就要实现他不需要的方法2,和方法3。这简直就是一个灾难的设计。
所以我们需要对接口进行拆分,就是把接口分成满足依赖关系的最小接口,类B与类D不需要去实现与他们无关接口方法。比如在这个例子中,我们可以把接口拆成3个,第一个是仅仅由方法1的接口,第二个接口是包含2,3方法的,第三个接口是包含4,5方法的。
这样,我们的设计就满足了接口隔离原则。
以上这些设计思想用英文的第一个字母可以组成SOLID ,满足这个5个原则的程序也被称为满足了SOLID准则。迪米特原则
迪米特原则也被称为最小知识原则,他的定义
一个对象应该对其他对象保持最小的了解。
因为类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,所以这也是我们提倡的软件编程的总的原则:低耦合,高内聚。
迪米特法则还有一个更简单的定义只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
这里我们可以用一个现实生活中的例子来讲解一下。比如我们需要一张CD,我们可能去音像店去问老板有没有我们需要的那张CD,老板说现在没有,等有的时候你们来拿就行了。在这里我们不需要关心老板是从哪里,怎么获得的那张CD,我们只和老板(直接朋友)沟通,至于老板从他的朋友那里通过何种条件得到的CD,我们不关心,我们不和老板的朋友(陌生人)进行通信,这个就是迪米特的一个应用。说白了,就是一种中介的方式。我们通过老板这个中介来和真正提供CD的人发生联系。
总结:依赖原则告诉我们要面向接口编程;接口分离原则告诉我们设计接口的时候功能要单一;里式替换告诉我们不要破坏继承体系,而是去扩展;单一功能原则告诉实现类要功能单一。开放封闭原则则是总纲,对扩展开放,对修改封闭。这些原则其实都是应对不断改变的需求。每当需求变化的时候,我们利用这些原则来使我们的代码改动量最小,而且所造成的影响也是最小的。但是我们在看这些原则的时候,我们会发现很多原则并没有提供一种公式化的结论,而即使提供了公式化的结论的原则也只是建议去这样做。这是因为,这些设计原则本来就是从很多实际的代码中提取出来的,他是一个经验化的结论。怎么去用它,用好他,就要依靠设计者的经验。否则一味者去使用设计原则可能会使代码出现过度设计的情况。大多数的原则都是通过提取出抽象和接口来实现,如果发生过度的设计,就会出现很多抽象类和接口,增加了系统的复杂度。让本来很小的项目变得很庞大,当然这也是Java的特性(任何的小项目都会做成中型的项目)。
-
软件开发_六大原则
2020-10-25 14:26:46软件开发_六大原则 1,开闭原则 ① 修改时,执行关闭原则;扩展时,执行开放原则 ② 增加新功能代码时,尽量不修改已有代码,然后将扩展的代码增加到项目中; 2,迪米特原则 ① 高内聚,低耦合 ② 在开发代码时,类... -
面向对象程序设计六大原则
2013-07-23 14:17:12面向对象程序设计六大原则 一、“开-闭”原则(Open-Closed Principle,OCP) 1.1“开-闭”原则的定义及优点 1)定义:一个软件实体应当对扩展开放,对修改关闭( Software entities should be open for extension,but... -
面向对象编程(OOP)的六大原则
2018-08-17 14:40:45面向对象编程是一种很重要的编程思想,相应的,也有它自己的原则,接下来我将为大家简单的介绍一下面向对象编程的六大原则。 一、单一职责原则——SRP(Single-Responsibility Principle) 单一职责,字面意思就是... -
设计模式六大原则与类的六种关系
2012-11-06 15:04:53个人整理的比较全面的 设计模式六大原则与类的六种关系 -
软件设计的六大原则
2019-03-10 14:56:02一、单一职责原则(SRP: Single responsibility principle) 二、开放封闭原则(OCP: Open Closed Principle) 三、里氏替换原则 ( LSP: Liskov Substitution Principle) 四、接口隔离原则( ISP: Interface ... -
大话设计模式——六大原则(SOLID)
2017-08-05 21:06:08在软件系统中,一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些...