网站制作进度表,海口建设网站的公司,广州有几个区图片,脱发严重是什么原因引起的文章目录 1、什么是设计模式2、单一职责原则3、开闭原则4、接口隔离原则5、依赖倒置原则6、迪米特法则#xff08;最少知道原则#xff09;7、里式替换原则8、组合优于继承 设计模式主要是为了满足一个字
变#xff0c;这个字#xff0c;可能是需求变更、可能是场景变更最少知道原则7、里式替换原则8、组合优于继承 设计模式主要是为了满足一个字
变这个字可能是需求变更、可能是场景变更但是运用好设计模式后我们写出的代码就能很好的应对不断变化的场景。 1、什么是设计模式
设计模式是前辈们不断总结、优化、打磨出来的设计方法不同设计模式适用于不同的场景
但要明确一点没有任何一种设计模式能达到适用于所有场景的效果
只有运用好设计原则和设计模式才能让我们写出更加优秀的代码或者设计更好软件架构
设计模式有23种其中每个设计模式又依赖于七大设计原则中的一个或多个
单一职责原则开闭原则接口隔离原则里氏替换原则依赖倒置原则组合优于继承原则迪米特法则最少知道原则
下面我们详细聊聊七大设计原则
2、单一职责原则
单一职责是什么呢 核心思想每个方法、每个类、每个框架都只负责一件事情 举个栗子 Math.round() 只负责完成四舍五入的功能其他的不管方法 Reader类只负责读取文本文件类 Spring MVC只负责简化MVC开发框架
单一职责讲究一个”分“字将功能尽可能的拆分然后使用的时候进行组合
优点
1.代码重用性提高
2.代码可读性提高此时的代码就像一个大纲一样
现在需求来了统计一个文本文件中有多少个单词
我们先来看一个栗子
public class nagtive {public static void main(String[] args) {try{//统计一个文本文件中有多少个单词//Reader默认查询的码表是与操作系统一致的码表我们的操作系统是中文的所以Reader就会使用GBK码表//GBK码表一个汉字占2个字节 且汉字的两个字节都是以1开头utf8码表一个汉字占3个字节//读取到记事本中的数字45489--- GBK ---北 ---unicode --- 21271//总之一句话字符流读取文件会查询码表字节流不会查询码表Reader in new FileReader(E:\\1.txt);BufferedReader bufferedReader new BufferedReader(in);String line null;StringBuilder sb new StringBuilder();while((line bufferedReader.readLine()) ! null){sb.append(line);sb.append( );}//对内容进行分割String[] words sb.toString().split([^a-zA-Z]);System.out.println(words.length);bufferedReader.close();} catch (IOException e) {e.printStackTrace();}}
}相信很多同学拿到需求上来就是梭哈搞定同一个方法将所有的事做完了让它去做文件读取还让它去做内容分割。这就违背了单一职责原则。
之前聊到设计模式讲究一个”变“字那现在需求变了我们需要统计文本文件中有多少个句子那我们的做法是什么呢?重新写一个方法将读取文件的内容部分的代码复制粘贴过去然后改一下分割条件这样虽然能解决问题但是你有没有发现代码变得很臃肿呀这显然是不合理的。
那正确的做法是什么呢应该是将读取文件内容部分封装成一个方法将内容分割也封装成一个方法然后根据需求进行组合
看下面这个栗子
public class demo {//读取文件的内容public static StringBuilder loadFile(String path) throws IOException {Reader in new FileReader(path);BufferedReader bufferedReader new BufferedReader(in);String line null;StringBuilder sb new StringBuilder();while ((line bufferedReader.readLine()) ! null) {sb.append(line);sb.append( );}bufferedReader.close();return sb;}//对内容进行分割public static String[] getSplit(String regex, StringBuilder sb){return sb.toString().split(regex);}//--------------------------------------------------------------------------//需求//统计一个文本文件中有多少个单词public static Integer getWords() throws IOException {//读取文件的内容StringBuilder sb loadFile(E:\\1.txt);//对内容进行分割String[] words getSplit([^a-zA-Z], sb);return words.length;}//统计一个文本文件中有多少个句子public static Integer getSentence() throws IOException {//读取文件的内容StringBuilder sb loadFile(E:\\1.txt);//对内容进行分割String[] words getSplit([.,!?], sb);return words.length;}public static void main(String[] args) throws IOException {System.out.println(getWords());System.out.println(getSentence());}
}遵守单一原则可以给我们带来的好处是提高了代码的可重用性同时还让得到的数据不再有耦合完成我们的需求。
3、开闭原则
简单来说就是 对扩展开放对修改关闭 在程序需要进行拓展的时候不能去修改原有的代码。
举个栗子我现在有一个刮胡刀刮胡刀的功能应该就是刮胡子但是我现在想要它拥有吹风机的能力
违法开闭原则的做法是把吹风机的功能加上了可能就不能刮胡子了符合开闭原则的做法是把吹风功能加上且没有影响之前刮胡子的功能
例如我现在有一个商品类Goods这个类之前有一个方法是获取它的价格例如
public class Goods {private BigDecimal price;public void setPrice(BigDecimal price) {this.price price;}public BigDecimal getPrice() {return this.price;}
}现在变化来了当前商品需要打8折进行销售不符合开闭原则的做法就是直接在原来的代码中进行修改
public BigDecimal getPrice() {// BigDecimal可以防止精度丢失return this.price.multiply(new BigDecimal(0.8));
}这样显然是不合理的因为我们对源代码进行了修改如果下次是打七折那是不是又要去改源代码呢
正确的做法应该是写一个子类DiscountGoods来拓展父类的功能再在子类上进行修改这样就不会破坏父类的功能又能满足需求
public class DiscountGoods extends Goods{Overridepublic BigDecimal getPrice() {return super.getPrice().multiply(new BigDecimal(0.8));}
}这就叫对扩展开发对修改关闭。我们在用设计模式编码时应该时刻注意的是改源码是一件非常危险的事情因为一个功能并不是只有你在使用很容易造成牵一发而动全身的效果
但是如果我们因为要遵守开闭原则每次对功能进行修改的时候都去新写一个类这样的会很繁琐所以我们的准则是
如果这个类是自己写的自己修改不会影响该类在其他地方的效果不会牵一发而动全身那就可以随意修改如果这个类不是自己写的自己不清楚修改后会带来什么样的影响那就不要修改要符合开闭原则
4、接口隔离原则
接口隔离原则也是满足一个字 ”分“将接口的功能尽可能的拆分 应该使用多个专门的接口而不是使用单一的总接口 即客户端不应该依赖于那些它不需要的接口 举个栗子现在设计一个动物的接口统一动物的行为可能会这样写
public interface Animal {void eat();void fiy(); void swim();
}这三个行为分别是 吃、飞和游泳似乎并没有什么问题,但是动物这个接口太广了并不是所有的动物都有着这三种行为
例如小狗的栗子
public class Dog implements Animal {Overridepublic void eat() {System.out.println(小狗啃骨头);}Overridepublic void swim() {System.out.println(小狗会狗刨);}Overridepublic void fly() {throw new UnsupportedOperationException(小狗不会飞你行你来);}
}小狗并不具备飞的属性
正确的做法是将动物这个总接口拆分成多个单独的小接口
interface Eatable{void eat();
}interface Swimable{void swim();
}interface Flyable{void fly();
}再不断的组合实现不同的接口 核心思想还是高内聚低耦合通过不断组合不可分割的功能完成最终需要的功能 我们改进一下小狗的栗子
public class Dog implements Eatable, Swimable {Overridepublic void eat() {System.out.println(小狗啃骨头);}Overridepublic void swim() {System.out.println(小狗会狗刨);}
}客户端依赖的接口中不应该存在他所不需要的方法。
如果某一接口太大导致这一情况发生应该拆分这一接口使用接口的客户端只需要知道它需要使用的接口及该接口中的方法即可。
5、依赖倒置原则
面向接口编程依赖于抽象而不依赖于具体 上层不应该依赖于下层它们都应该依赖于抽象 区分上下层的方法为调用别的方法的就是上层被调用的就是下层
举个栗子人喂养动物
class Person {public void feed(Dog dog) {System.out.println(开始喂dog...);}
}class Dog {public void eat() {System.out.println(狗啃骨头);}
}------------------------------------------------------------
public class AppTest {public static void main(String[] args) {Person person new Person();Dog dog new Dog();person.feed(dog);}
}上述代码好像并没有什么问题但是设计模式是为了应对变化现在变化来了现在客户端Person不仅需要喂狗还需要喂猫。
直接添加一个Cat类
class Cat {public void eat() {System.out.println(小猫吃鱼);}
}
public class AppTest {public static void main(String[] args) {Person person new Person();Dog dog new Dog();Cat cat new Cat();// 喂狗person.feed(dog);// 喂猫person.feed(cat);}
}这样明显会报错因为之前的代码中只能喂狗不能喂猫
那怎么办呢我直接重载一个方法让Person类可以喂猫不就好了
class Person {public void feed(Dog dog) {System.out.println(开始喂dog...);}public void feed(Cat dog) {System.out.println(开始喂Cat...);}
}好家伙这是不是为了应对变化直接改源码了首当其冲的就是破坏了开闭原则其次如果每次要多喂养一种动物就要去重载一个方法似乎并不合理。
每当一个新的类需要依赖时就要重载一个方法这里就违反了依赖倒置原则每当下层发生改变时上层要一起改变下层多个猫上层要重载喂猫这样的设计没有拓展性我们不应该依赖于具体的类而应该依赖于抽象的接口
我们聊回来猫和狗都属于什么是动物狗和猫只是动物的实现人应该去喂养动物而不是具体的实现所以我们应该进行依赖倒置依赖抽象不依赖实现这里我们只需要依赖一个抽象的动物类或者接口即可
class Person {public void feed(Animal animal) {System.out.println(开始喂动物...);}
}interface Animal {void eat();
}class Dog implements Animal{Overridepublic void eat() {System.out.println(狗啃骨头);}
}class Cat implements Animal{Overridepublic void eat() {System.out.println(小猫吃鱼);}
}---------------------------------------------------
public class AppTest {public static void main(String[] args) {Person person new Person();Dog dog new Dog();Cat cat new Cat();// 喂狗person.feed(dog);// 喂猫person.feed(cat);}
}看一下类图的变化 这里有读者可能有疑问了为什么是依赖倒置呢
看类图之前箭头是向下的依赖于具体实现之后大家都指向抽象面向抽象编程这就是依赖倒置。
6、迪米特法则最少知道原则 一个实体应当尽量少的与其他实体之间发生相互作用使得系统功能模块相对独立 一个类对于其他类要知道的越少越好封装的思想封装内部细节向外暴露提供功能的接口
只和朋友通讯朋友是指
类中的字段方法的参数方法的返回值方法中实例化出来的对象对象本身集合中的泛型
我们来看一个栗子现在有一个电脑需要关闭它
class Compute {public void saveData() {System.out.println(正在保存数据);}public void killProcess() {System.out.println(正在关闭程序);}public void closeScreen() {System.out.println(正在关闭屏幕);}public void powerOff() {System.out.println(正在断电);}
}class Person {Compute compute new Compute();public void shutDownCompute() {compute.saveData();compute.killProcess();compute.closeScreen();compute.powerOff();}
}好像没有什么问题
但对于用户来说知道的细节太多了要是不小心搞错了步骤那岂不是玩完所以他不想知道关闭电脑的具体步骤只想按一下按钮封装就好了
我们改一下上面的代码
class Compute {private void saveData() {System.out.println(正在保存数据);}private void killProcess() {System.out.println(正在关闭程序);}private void closeScreen() {System.out.println(正在关闭屏幕);}private void powerOff() {System.out.println(正在断电);}//封装细节public void shutDownCompute() {this.saveData();this.killProcess();this.closeScreen();this.powerOff();}
}class Person {Compute compute new Compute();public void shutDown() {compute.shutDownCompute();}
}那么对于朋友而言的最少知道原则是什么呢 如果对于作为返回类型、方法参数、成员属性、局部变量的类不需要过多的封装应该提供应有的细节由调用者自己弄清楚细节并承担异常的后果这样由我们直接创造的对象我们就能把它称为我们的朋友 但是如果这个对象不是我们自己获得的而是由被人提供的就不是朋友即朋友的朋友并不是自己的朋友
public class AppTest {public void func() {AppBean appBean BeanFactory.getAppBean();// 朋友的朋友就不是朋友了appBean.getStr();}}class BeanFactory {public static AppBean getAppBean() {return new AppBean();}
}class AppBean {public String getStr() {return ;}
}那么想要和这个AppBean做朋友该怎么办呢比如给它转换成方法参数
public class AppTest {public void func() {AppBean appBean BeanFactory.getAppBean();// 朋友的朋友就不是朋友了this.getStr(appBean);}/* 将朋友的朋友的细节转换为自己熟悉的方法 */public String getStr(AppBean appBean){return appBean.getStr();}
}相信很多同学看到这里很谜这不是制造了很多小方法吗确实迪米特法则的缺点就是如此在系统里造出大量的小方法这些方法仅仅是传递间接的调用与系统的业务逻辑无关。所以在开发中适当的违反一下也是可以的。
因此前人总结出一些方法论以供我们参考
优先考虑将一个类设置成不变类。尽量降低一个类的访问权限。谨慎使用Serializable。尽量降低成员的访问权限。
虽然规矩很多但是理论需要深刻理解实战需要经验积累。路还很长。
7、里式替换原则 任何能够使用父类对象的地方都应该能透明的替换为子类 也就是说子类对象能够随时随地替换父类对象并且替换完之后语法不会报错业务逻辑也不会出现问题
我们先聊一下方法重写的定义
在子类和父类中出现了返回类型相同、方法名相同、方法参数相同的方法时构成了方法重写。
方法重写的两个限制
子类重写父类的方法时子类方法的访问修饰符不能比父类更严格子类重写父类的方法时子类方法不能抛出比父类更多的异常
为什么要有这两个限制呢
就是为了保证代码符合里氏替换原则
举个栗子
正常情况下如果子类抛出的异常比父类少父类在执行方法时就会进行catch并且能够捕获子类中的异常所以这样进行替换时就不会影响代码的结构做到透明、无感知
有很多的例子都可以用里式替换进行解释著名的例子有长方形正方形问题
接下来我们具体看看长方形正方形的问题先来回顾下继承方面的知识
继承的作用
提高代码重用性多态的前提
两个类能发生继承关系的依据是什么
先看两个类有咩有” is a “ 关系在两个类有了 is a 关系之后还要考虑子类对象在替换了父类对象之后业务逻辑是否发生变化。如果变化就不能发生继承关系
正方形和长方形是 is a 关系那么我们能不能让正方形类直接去继承长方形类呢
答案是不能为什么呢因为还要考虑具体的业务场景看看在具体的业务场景下正方形替换了长方形之后业务逻辑是否变化
举个栗子
public class AppTest {//长方形GetterSetterstatic class Rectangular {private Integer width;private Integer length;}//正方形static class Square extends Rectangular {private Integer sideWidth;Overridepublic Integer getWidth() {return sideWidth;}Overridepublic void setWidth(Integer width) {this.sideWidth width;}Overridepublic Integer getLength() {return sideWidth;}Overridepublic void setLength(Integer length) {this.sideWidth length;}}static class Utils{public static void transform(Rectangular graph){while ( graph.getWidth() graph.getLength() ){graph.setWidth(graph.getWidth() 1);System.out.println(长graph.getLength() : 宽graph.getWidth());}}}public static void main(String[] args) {// Rectangular graph new Rectangular();Rectangular graph new Square();graph.setWidth(20);graph.setLength(30);Utils.transform(graph);}
}替换后运行将是无限死循环。
要知道在向上转型的时候方法的调用只和new的对象有关才会造成不同的结果。在使用场景下需要考虑替换后业务逻辑是否受影响。
由此引出里氏替换原则的使用需要考虑的条件
是否有is-a关系子类可以扩展父类的功能但是不能改变父类原有的功能。
鸵鸟非鸟问题
在我们看来鸵鸟属于鸟科但是现在有个需求是送信飞鸽传书这个业务场景能将鸵鸟子类替换为鸟父类吗鸵鸟不会飞所以这显然是不可以的。
8、组合优于继承 复用别人的代码时不宜使用继承应该使用组合。 组合是一种强关联关系整体对象和局部对象的生命周期是一样的类似于大雁和翅膀的关系 整体对象负责局部对象的生命周期局部对象不能被其他对象共享;如果整体对象被销毁或破坏那么局部对象也一定会被销毁或破坏
聚和它是一种弱关联是 【整体和局部】之间的关系且局部可以脱离整体独立存在类似于雁群和其中一只大雁的关系 代表局部的对象有可能会被多个代表整体的对象所共享而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏甚至代表局部的对象的生命周期可以超越整体
总而言之组合是值的关联Aggregation by Value而聚合是引用的关联Aggregation by Reference
实心菱形的是组合、空心菱形的是聚和如果不区分就用虚线指向组合是作为成员变量作为另一个类的引用聚和是作为形参或者局部变量作为另一个类的引用 组合大家在平时编码的时候一定经常使用举一个简单的例子如果我们现在要有链表实现队列应该怎么做呢队列的特点就是先进先出完全可以用链表实现我们可以用继承关系来做
public class Queue E extends LinkedListE {/*** 入队*/public void enQueue(E element){this.add(element);}/*** 出队*/public E deQueue(){return this.remove(0);}}似乎并没有什么问题队列类继承自链表类并暴露自己提供给外界的方法但是当我们调用这个Queue时就会发现问题 好家伙我的Queue本来只需要入队和出队两个方法但是居然有这么多细节的方法供我使用这就违背了迪米特法则一个类的内部实现应该不要提供给外界只暴露该提供的方法这就是继承的问题继承复用破坏包装因为继承将基类的实现都暴露给派生类
如果我们换成组合该怎么做呢
public class QueueE {// 成员变量 - 组合关系LinkedListE list new LinkedList();/*** 入队*/public void enQueue(E element) {list.add(element);}/*** 出队*/public E deQueue() {return list.remove(0);}
}所以如果我们仅仅只是为了复用代码可以优先考虑组合如果是为了实现多态可以优先继承
我们也来看一个反例叭其实在Java中有很多不合理的设计例如Serializable接口Date类等等这里就讲一个java.util.Stack的糟糕设计 点进源码中看我们发现原来是继承了Vector类让其拥有了链表的能力看着这个兄弟设计模式也没学好 官方也注意到了这个设计不合理的地方推荐我们使用Deque来实现栈 其实我们看完了这些设计原则就会发现其实都是为了应对不断变化的在看一些源码中例如Spring的源码、dubbo的源码、netty的源码中也是非常严谨的遵守这些开发规范的。
本文部分内容参考了我老大的博文设计模式学习汇总版
大佬的文章写的太好了
关于设计模式我们后面接着聊…