上海网站制作网,杭州做网站的好公司,seo教程合集,凡客网站设计目录
前言六大原则 单一职责原则开闭原则里氏替换原则依赖倒置原则接口隔离原则迪米特原则总结 前言
很久没有写博客了#xff0c;一直给自己找借口说太忙了#xff0c;过几天有空再写#xff0c;几天之后又几天#xff0c;时间就这么快速的消逝。说到底就是自己太懒了一直给自己找借口说太忙了过几天有空再写几天之后又几天时间就这么快速的消逝。说到底就是自己太懒了不下点决心真是不行。我决定逼自己一把从今天开始学习设计模式系列并写成博文记录下来做不到的话就罚自己一个月不玩游戏 (作孽啊。。。。)
六大原则
言归正传这是我学习设计模式系列的第一篇文章本文主要讲的是面向对象设计应该遵循的六大原则掌握这些原则能帮助我们更好的理解面向对象的概念也能更好的理解设计模式。这六大原则分别是
单一职责原则——SRP开闭原则——OCP里式替换原则——LSP依赖倒置原则——DIP接口隔离原则——ISP迪米特原则——LOD
单一职责原则
单一职责原则Single Responsibility Principle简称SRP。其定义是应该有且仅有一个类引起类的变更这话的意思就是一个类只担负一个职责。
举个例子在创业公司里由于人力成本控制和流程不够规范的原因往往一个人需要担任N个职责一个工程师可能不仅要出需求还要写代码甚至要面谈客户光背的锅就好几种简单用代码表达大概如此
public class Engineer {public void makeDemand(){}public void writeCode(){}public void meetClient(){}
}代码看上去好像没什么问题因为我们平时就是这么写的啊但是细读一下就能发现这种写法很明显不符合单一职责的原则因为引起类的变化不只有一个至少有三个方法都可以引起类的变化比如有天因为业务需要出需求的方法需要加个功能 (比如需求的成本分析)或者是见客户也需要个参数之类的那样一来类的变化就会有多种可能性了其他引用该类的类也需要相应的变化如果引用类的数目很多的话代码维护的成本可想而知会有多高。所以我们需要把这些方法拆分成独立的职责可以让一个类只负责一个方法每个类只专心处理自己的方法即可。
单一职责原则的优点
类的复杂性降低实现什么职责都有明确的定义逻辑变得简单类的可读性提高了而且因为逻辑简单代码的可维护性也提高了变更的风险降低因为只会在单一的类中的修改。
开闭原则
开闭原则Open Closed Principle是Java世界里最基础的设计原则其定义是 一个软件实体如类、模块和函数应该对扩展开放对修改关闭 也就是说一个软件实体应该通过扩展来实现变化而不是通过修改已有的代码实现变化。这是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
在我们编码的过程中需求变化是不断的发生的当我们需要对代码进行修改时我们应该尽量做到能不动原来的代码就不动通过扩展的方式来满足需求。
遵循开闭原则的最好手段就是抽象例如前面单一职责原则举的工程师类我们说的是把方法抽离成单独的类每个类负责单一的职责但其实从开闭原则的角度说更好的方式是把职责设计成接口例如把写代码的职责方法抽离成接口的形式同时我们在设计之初需要考虑到未来所有可能发生变化的因素比如未来有可能因为业务需要分成后台和前端的功能这时设计之初就可以设计成两个接口
public interface BackCode{void writeCode();
}public interface FrontCode{void writeCode();
}如果将来前端代码的业务发生变化我们只需扩展前端接口的功能或者修改前端接口的实现类即可后台接口以及实现类就不会受到影响这就是抽象的好处。
里氏替换原则
里氏替换原则英文名Liskov Substitution Principle它的定义是 如果对每一个类型为T1的对象o1都有类型为T2的对象o2使得以T1定义的所有程序P在所有对象o1都替换成o2的时候程序P的行为都没有发生变化那么类型T2是类型T1的子类型。 看起来有点绕口它还有一个简单的定义 所有引用基类的地方必须能够透明地使用其子类的对象。 通俗点说只要父类能出现的地方子类就可以出现而且替换为子类也不会产生任何异常。 但是反过来就不行了因为子类可以扩展父类没有的功能同时子类还不能改变父类原有的功能。
我们都知道面向对象的三大特征是封装、继承和多态这三者缺一不可但三者之间却并不 “和谐“。因为继承有很多缺点当子类继承父类时虽然可以复用父类的代码但是父类的属性和方法对子类都是透明的子类可以随意修改父类的成员。如果需求变更子类对父类的方法进行了一些复写的时候其他的子类可能就需要随之改变这在一定程度上就违反了封装的原则解决的方案就是引入里氏替换原则。
里氏替换原则为良好的继承定义了一个规范它包含了4层含义
1、子类可以实现父类的抽象方法但是不能覆盖父类的非抽象方法。
2、子类可以有自己的个性可以有自己的属性和方法。
3、子类覆盖或重载父类的方法时输入参数可以被放大。
比如父类有一个方法参数是HashMap
public class Father {public void test(HashMap map){System.out.println(父类被执行。。。。。);}
}那么子类的同名方法输入参数的类型可以扩大例如我们输入参数为Map
public class Son extends Father{public void test(Map map){System.out.println(子类被执行。。。。);}
}我们写一个场景类测试一下父类的方法执行效果
public class Client {public static void main(String[] args) {Father father new Father();HashMap map new HashMap();father.test(map);}
}结果输出父类被执行。。。。。
因为里氏替换原则只要父类能出现的地方子类就可以出现而且替换为子类也不会产生任何异常。我们改下代码调用子类的方法
public class Client {public static void main(String[] args) {Son son new Son();HashMap map new HashMap();father.test(map);}
}运行结果是一样的因为子类方法的输入参数类型范围扩大了子类代替父类传递到调用者中子类的方法永远不会被执行这样的结果其实是正确的如果想让子类方法执行可以重写方法体。
反之如果子类的输入参数类型范围比父类还小比如父类中的参数是Map而子类是HashMap那么执行上述代码的结果就会是子类的方法体有人说这难道不对吗子类显示自己的内容啊。其实这是不对的因为子类没有复写父类的同名方法方法就被执行了这会引起逻辑的混乱如果父类是抽象类子类是实现类你传递一个这样的实现类就违背了父类的意图了容易引起逻辑混乱所以子类覆盖或重载父类的方法时输入参数必定是相同或者放大的。
4、子类覆盖或重载父类的方法时输出结果可以被缩小也就是说返回值要小于或等于父类的方法返回值。
确保程序遵循里氏替换原则可以要求我们的程序建立抽象通过抽象去建立规范然后用实现去扩展细节所以它跟开闭原则往往是相互依存的。
依赖倒置原则
依赖倒置原则Dependence Inversion Principle简称DIP它的定义是 高层模块不应该依赖底层模块两者都应该依赖其抽象 抽象不应该依赖细节 细节应该依赖抽象 什么是高层模块和底层模块呢不可分割的原子逻辑就是底层模块原子逻辑的再组装就是高层模块。
在Java语言中抽象就是指接口或抽象类两者都不能被实例化而细节就是实现接口或继承抽象类产生的类也就是可以被实例化的实现类。依赖倒置原则是指模块间的依赖是通过抽象来发生的实现类之间不发生直接的依赖关系其依赖关系是通过接口是来实现的这就是俗称的面向接口编程。
我们用歌手唱歌来举例比如一个歌手唱国语歌用代码表示就是
public class ChineseSong {public String language() {return 国语歌;}
}
public class Singer {//唱歌的方法public void sing(ChineseSong song) {System.out.println(歌手 song.language());}
}
public class Client {public static void main(String[] args) {Singer singer new Singer();ChineseSong song new ChineseSong();singer.sing(song);}
}运行main方法结果就会输出歌手唱国语歌
现在我们需要给歌手加一点难度比如说唱英文歌在这个类中我们发现是很难做的。因为我们Singer类依赖于一个具体的实现类ChineseSong也许有人会说可以在加一个方法啊但这样一来我们就修改了Singer类了如果以后需要增加更多的歌种那歌手类不是一直要被修改也就是说依赖类已经不稳定了这显然不是我们想看到的。
所以我们需要用面向接口编程的思想来优化我们的方案改成如下的代码
public interface Song {public String language();
}
public class ChineseSong implements Song{public String language() {return 唱国语歌;}
}
public class EnglishSong implements Song {public String language() {return 唱英语歌;}
}
public class Singer {//唱歌的方法public void sing(Song song) {System.out.println(歌手 song.language());}
}
public class Client {public static void main(String[] args) {Singer singer new Singer();EnglishSong englishSong new EnglishSong();// 唱英文歌singer.sing(englishSong);}
}我们把歌单独抽成一个接口Song每个歌种都实现该接口并重写方法这样一来歌手的代码不必改动如果需要添加歌的种类只需写多一个实现类继承Song即可。
通过这样的面向接口编程我们的代码就有了更好的扩展性同时也降低了耦合提高了系统的稳定性。
接口隔离原则
接口隔离原则Interface Segregation Principle简称ISP其定义是 客户端不应该依赖它不需要的接口 意思就是客户端需要什么接口就提供什么接口把不需要的接口剔除掉这就需要对接口进行细化保证接口的纯洁性。换成另一种说法就是类间的依赖关系应该建立在最小的接口上也就是建立单一的接口。
你可能会疑惑建立单一接口这不是单一职责原则吗其实不是单一职责原则要求的是类和接口职责单一注重的是职责一个职责的接口是可以有多个方法的而接口隔离原则要求的是接口的方法尽量少模块尽量单一如果需要提供给客户端很多的模块那么就要相应的定义多个接口不要把所有的模块功能都定义在一个接口中那样会显得很臃肿。
举个例子现在的智能手机非常的发达几乎是人手一部的社会状态在我们年轻人的观念里好的智能手机应该是价格便宜外观好看功能丰富的由此我们可以定义一个智能手机的抽象接口 ISmartPhone代码如下所示
public interface ISmartPhone {public void cheapPrice();public void goodLooking();public void richFunction();
}接着我们定义一个手机接口的实现类实现这三个抽象方法
public class SmartPhone implements ISmartPhone{public void cheapPrice() {System.out.println(这手机便宜~~~~~);}public void goodLooking() {System.out.println(这手机外观好看~~~~~);}public void richFunction() {System.out.println(这手机功能真多~~~~~);}
}然后定义一个用户的实体类 User并定义一个构造方法以ISmartPhone 作为参数传入同时我们也定义一个使用的方法usePhone 来调用接口的方法
public class User {private ISmartPhone phone;public User(ISmartPhone phone){this.phone phone;}public void usePhone(){phone.cheapPrice();phone.goodLooking();phone.richFunction();}
}可以看出当我们实例化User类并调用其方法usePhone后控制台上就会显示手机接口三个方法的方法体信息这种设计看上去没什么大毛病但是我们可以仔细想下ISmartPhone这个接口的设计是否已经达到最优了呢很遗憾答案是没有接口其实还可以再优化。
因为除了年轻人之外中年商务人士也在用智能手机在他们的观念里智能手机并不需要丰富的功能甚至不用考虑是否便宜 (有钱就是任性~~~~)因为成功人士都比较忙对智能手机的要求大多是外观大气功能简单即可这才是他们心中好的智能手机的特征这样一来我们定义的 ISmartPhone 接口就无法适用了因为我们的接口定义了智能手机必须满足三个特性如果实现该接口就必须三个方法都实现而对商务人员的标准来说我们定义的方法只有外观符合且可以重用而已。你可能会说我可以重写一个实现类啊只实现外观的方法另外两个方法置空什么都不写这不就行了吗但是这也不行因为 User 引用的是ISmartPhone 接口它调用三个方法你只实现了两个那么打印信息就少了两条了只靠外观的特性使用者怎么知道智能手机是否符合自己的预期
分析到这里我们大概就明白了其实ISmartPhone的设计是有缺陷的过于臃肿了按照接口隔离原则我们可以根据不同的特性把智能手机的接口进行拆分这样一来每个接口的功能就会变得单一保证了接口的纯洁性也进一步提高了代码的灵活性和稳定性。
迪米特原则
迪米特原则Law of Demeter简称LoD也被称为最少知识原则它描述的规则是 一个对象应该对其他对象有最少的了解 也就是说一个类应该对自己需要耦合或调用的类知道的最少类与类之间的关系越密切耦合度越大那么类的变化对其耦合的类的影响也会越大这也是我们面向设计的核心原则低耦合高内聚。
迪米特法则还有一个解释只与直接的朋友通信。
什么是直接的朋友呢每个对象都必然与其他对象有耦合关系两个对象的耦合就成为朋友关系这种关系的类型很多例如组合、聚合、依赖等。其中我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友而出现在局部变量中的类则不是直接的朋友。也就是说陌生的类最好不要作为局部变量的形式出现在类的内部。
举个例子上体育课之前老师让班长先去体务室拿20个篮球等下上课的时候要用。根据这一场景我们可以设计出三个类 Teacher(老师)Monitor (班长) 和 BasketBall (篮球)以及发布命令的方法command 和 拿篮球的方法takeBall
public class Teacher {// 命令班长去拿球public void command(Monitor monitor) {ListBasketBall ballList new ArrayListBasketBall();// 初始化篮球数目for (int i 0;i20;i){ballList.add(new BasketBall());}// 通知班长开始去拿球monitor.takeBall(ballList);}
}
public class BasketBall {
}
public class Monitor {// 拿球public void takeBall(ListBasketBall balls) {System.out.println(篮球数目 balls.size());}
}然后我们写一个情景类进行测试
public class Client {public static void main(String[] args) {Teacher teacher new Teacher();teacher.command(new Monitor());}
}结果显示如下
篮球数目20虽然结果是正确的但我们的程序其实还是存在问题因为从场景来说老师只需命令班长拿篮球即可Teacher只需要一个朋友----Monitor但在程序里Teacher的方法体中却依赖了BasketBall类也就是说Teacher类与一个陌生的类有了交流这样Teacher的健壮性就被破坏了因为一旦BasketBall类做了修改那么Teacher也需要做修改这很明显违背了迪米特法则。
因此我们需要对程序做些修改在Teacher的方法中去掉对BasketBall类的依赖只让Teacher类与朋友类Monitor产生依赖修改后的代码如下
public class Teacher {// 命令班长去拿球public void command(Monitor monitor) {// 通知班长开始去拿球monitor.takeBall();}
}
public class Monitor {// 拿球public void takeBall() {ListBasketBall ballList new ArrayListBasketBall();// 初始化篮球数目for (int i 0;i20;i){ballList.add(new BasketBall());}System.out.println(篮球数目 ballList.size());}
}这样一来Teacher类就不会与BasketBall类产生依赖了即时日后因为业务需要修改BasketBall也不会影响Teacher类。
总结
好了面向对象的六大原则就介绍到这里了。其实我们不难发现六大原则虽说是原则但它们并不是强制性的更多的是建议。遵照这些原则固然能帮助我们更好的规范我们的系统设计和代码习惯但并不是所有的场景都适用就例如接口隔离原则在现实系统开发中我们很难完全遵守一个模块一个接口的设计否则业务多了就会出现代码设计过度的情况让整个系统变得过于庞大增加了系统的复杂度甚至影响自己的项目进度得不偿失啊。
所以还是那句话在合适的场景选择合适的技术