设计原则一
一、开闭原则
顾名思义,在软件设计中应当遵循对扩展开放,而对修改关闭。也即在实际开发过程中,当需求变动业务调整时,在不改动源码的情况下可以扩展以支撑新的功能;这也要求了在设计之初制定技术方案时应有前瞻性。遵循开闭原则的好处:提高代码的复用性、可维护性、有利于单元测试。实现:在面向对象的设计中,通常可以通过定义接口或者抽象类来约束相同属性或者一般通用的实现(抽象),这样具体派生实现类可以将具体的实现封装在内部。即使业务变化,我们只需要相应的派生出一个实现类就可以实现扩展。不过在实际中,这种对业务的抽象能力要求还是比较高的。如果抽象的粒度太小,那么会伴随着繁杂的实现类;如果粒度太大却不利于扩展。经验的积累与思考很重要。1.实际问题
商品价格变动模拟,如打折促销、涨价等定义顶层的商品接口(仅仅包含ID、名称、价格)public interface Product { long getId(); long getPrice(); String getName(); }新建水果中香蕉的实现类public class Banana implements Product { private long id; private long price; private String name; public Banana(long id, long price, String name) { this.id = id; this.price = price; this.name = name; } public void setId(long id) { this.id = id; } public void setPrice(long price) { this.price = price; } public void setName(String name) { this.name = name; } @Override public long getId() { return this.id; } @Override public long getPrice() { return this.price; } @Override public String getName() { return this.name; } }香蕉不易保存的特性决定了,如果库存较多只能打折进行处理。
如果直接修改Banana实现类中价格getPrice()势必会对其他的地方的调用产生影响,违背了开闭原则。因此增加BananaDiscountImp折扣类,当然这其实也是不合理的,仅仅作为举例,如果都是这种,会增加很多不必要的实现类,使得项目膨胀冗杂。public class BananaDiscountImp extends Banana { public BananaDiscountImp(long id, long price, String name) { super(id, price, name); } /** * 原始价格 */ @Override public long getPrice() { return super.getPrice(); } /** * 折后价格 * (需借助BigDecimal转换,包括保留小数位等,80相当于8折) */ public long getOriginalPrice() { return getPrice() * 80L; } }二、里氏替换原则含义:通俗地讲在继承过程中子类可以对基类的功能进行扩展,但不能改变基类原有的功能。在面向对象的程序设计中,继承作为三大特性之一。虽然带来了很大的便利性,但同时也增加了耦合性,侵入性。里氏替换原则实际上更是对继承过程中的一种规范与约束:1.子类可以增加自身特有的方法;2.子类可以实现基类的抽象方法,但不能覆盖基类的非抽象方法;3.当子类重载基类的方法时,方法的入参应该比基类更宽松;4.当子类实现基类的抽象方法时,方法的返回值应该比基类更严格;5.如果子类必须重写基类的方法时,应该考虑替换当前的继承关系,同时继承更加一般的基类,或者使用组合、聚合、依赖等其他方式替代。1.实际问题
比较经典的"正方形非长方形问题",另外我们知道鸵鸟是不会飞的,但是奔跑的速度很快。顶层的抽象动物类public class Animal { /** * 米每秒 */ private long moveSpeed; public long getMoveTime(long distance) { return distance / moveSpeed; } public void setMoveSpeed(long moveSpeed) { this.moveSpeed = moveSpeed; } }较为一般的鸟类public class Bird extends Animal { private long flySpeed; public void setFlySpeed(long flySpeed) { this.flySpeed = flySpeed; } public long getFlyTime(long distance) { return distance / flySpeed; } }
在定义的过程,无非就是根据一些鸟类的特性,比如有羽毛,会飞,有喙等等;但是往往会存在特例。鸵鸟除了没有飞的能力其他都是包含的,如果继承Bird类,当求导飞行速度时势必会出现错误,因为鸵鸟的飞行速度为0。具体到某一种鸟类-麻雀public class Sparrow extends Bird { @Override public void setFlySpeed(long flySpeed) { super.setFlySpeed(flySpeed); } }鸵鸟类(错误的继承) public class Ostrich extends Bird { @Override public void setFlySpeed(long flySpeed) { //鸵鸟的飞行速度为零,重写了 flySpeed = 0; super.setFlySpeed(flySpeed); } }
当测试时,肯定是会出现系统异常的情况,这里违背了里氏替换的原则-不能覆盖基类的非抽象方法;从而导致了错误的结果,此时应该考虑取消继承关系,改为更加通用的基类,也即继承Animal,动物都有移动的速度。鸵鸟类继承Animalpublic class Ostrich extends Animal { public Ostrich() {} @Override public void setMoveSpeed(long moveSpeed) { super.setMoveSpeed(moveSpeed); } public static void main(String[] args) { //测试 Animal ostrich = new Ostrich(); ostrich.setMoveSpeed(90); } }实际开发的过程中应避免对滥用继承,实现子类时遵循里氏替换的原则能够帮助我们对子类更好地约束,建立起更健壮、易维护的系统。当然不遵循程序也能跑,随着项目的复杂度增加,出现问题的概率也大大增加。三、依赖倒置原则
高层结构的模块不应该依赖低层结构的模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。1.一般含义通俗的解释,依赖倒置的核心思想-面向接口编程。面向接口编程的好处不言而喻,相对于实现细节的多变性,抽象的概念则稳定得多,很多同学包括自己在实际开发中有时候也会陷入到实现的细节中,试想以具体的实现类来构建系统自然是不够稳定的,同样不利于扩展。对于这种,首先考虑的是制定抽象的接口、抽象类层,以接口来约束和规范实现,而不关心具体的实现细节。2.作用既然都面向了接口,类与类之间的耦合度降低了(依赖倒置原则降低了类之间的耦合度)。耦合度低,提高了系统的稳定性(稳定性)。抽象的规范与约束作用,提高了代码的可维护性,可读性,当然既然存在继承,那么在设计与实现的过程中应遵循里氏替换原则(可维护性、可读性)。3.如何设计面向接口-尽量使用使用接口或者抽象类,或者两者都包含来代替类传递。对于变量的申明类型尽量使用接口或者抽象类,而不是具体的实现类。继承遵循里氏替换原则4.实际问题
以大学生学习课程为例定义课程的接口/** * Created by Sai * on: 05/01/2022 23:54. */ public interface ICourse { void selected(); }具体课程类-物理课/** * Created by Sai * on: 05/01/2022 23:58. */ public class PhysicsCourse implements ICourse { @Override public void selected() { System.out.println("物理课被选修了"); } }具体课程类-英语课/** * Created by Sai * on: 06/01/2022 00:00. */ public class EnglishCourse implements ICourse { @Override public void selected() { System.out.println("英语课被选修了"); } }学生类/** * on: 06/01/2022 00:01. * Description: */ public class Student { //依赖注入 private ICourse course; public Student() {} public ICourse getCourse() { return course; } public void setCourse(ICourse course) { this.course = course; } public void study() { if (null != course) { course.selected(); } } public static void main(String[] args) { Student stu = new Student(); stu.setCourse(new EnglishCourse()); stu.study(); stu.setCourse(new PhysicsCourse()); stu.study(); } } //英语课被选修了 //物理课被选修了 //Process finished with exit code 0前面提到依赖倒置的核心-面向接口编程,理解了面向接口编程的含义与运用,依赖倒置原则自然而然就掌握了,当然这离不开实践过程中的积累与思考。
于光影中游走,创造不同寻常带上相机花时间与自己相处一声快门留下的影像它讲述着彼时映入眼帘的风景以及映入生命中的景观与文化哈苏大师MariaSvarbova从2014年至今,致力在斯洛伐克寻找上个世纪的老旧公
徕卡代表性摄影师马克吕布与中国印象1957年,故宫照片照亮了我们对昔日往事的留念,那些白驹过隙般的美好时光因为照片而重拾。马克吕布(MarcRiboud),20世纪伟大的摄影师之一,1952年成为玛马格南图片社一员
Protobuf一种更小更快更高效的协议前言之前分享关于JSON的使用例说嵌入式实用知识之JSON数据。JSON类型数据可读性很好,但是整个数据包中会带有一些无用的数据,自然会在一定程度上带来通信负担。本篇文章我们来分享
文件系统有很多,但这几个最为重要Linux一个重要的哲学是一切皆文件。而文件与文件系统是密切相关的,本篇笔记我们来一起学习理清那些令我们眼花缭乱的文件系统。文件系统粗略的分类下面依次进行介绍根文件系统根文件系统(
一个不该被遗忘的打印接口我们大多都使用printf来打印一些信息,其它的接口都比较少用。这里介绍一个被我们遗忘的打印输出函数perror函数。perror函数简介perror(s)用来将上一个函数发生错误
如何查看Linux命令工具的源码?上一篇分享了两个使用的小工具如何同时输出调试信息到终端与文件?。有位小伙伴留言问道tee工具的代码在哪这篇文章我们就来简单分享一下。首先,我们需要了解Linux下的命令分为大致分为
Pinctrl子系统与GPIO子系统初探(附实例)1024G嵌入式资源大放送!包括但不限于CC单片机Linux等。私信回复1024,即可免费获取!前言之前我们已经通过几篇笔记来学习点灯了Linux驱动基础篇LED驱动基于总线设备驱
Linux驱动基础篇hello驱动上一篇分享的从单片机工程师的角度看嵌入式Linux中有简单提到Linux的三大类驱动我们学习编程的时候都会从hello程序开始。同样的,学习Linux驱动我们也从最简单的hello
Linux总线设备驱动模型之前的笔记有分享程序设计思想分层分离抽象,按照这样的思想来设计我们的程序可以更容易写出耦合性低独立性强可重用性强的代码。Linux内核中更是存在着更多的分离分层思想的代码,plat
C基础经典问题交换ab值较好的方法?交换ab的值在C语言的学习中是很常见的问题。最常用的方法就是引入一个中间变量当做中间介质来交换ab的值。代码如下voidchangeab(inta,intb)inttemp0tem
C基础经典字符串逆序分享一道经典的C语言题目。题目字符串逆序。如helloworld转换为dlrowolleh。流程图代码程序功能字符串反转作者ZhengN公众号嵌入式大杂烩includestdio。