设计模式理解

  1. 设计模式,不是一种新的语言,也不是什么新api,更不是什么新的语法。
  2. 设计模式,是前辈们,不断总结,不断打磨出的设计方法。不同设计模式适用于不同的场景。
  3. 设计模式,公认的有23种设计模式,分别对应23种设计场景。这些设计模式,我们不用怀疑它的功能!因为这些设计模式是经过长期的实践考验,而留存下来的。
  4. 千万不要以为有任何一种设计模式,能解决任何问题,每一种设计模式只能用于适用的场景,而不是万能的。
  5. 设计模式有优点,也有缺点。我们不要为了适用设计模式而使用设计模式。切记防止“模式的滥用”!
  6. 23种设计模式,背后其实是7大设计原则。也就是说,每个设计模式都归属于一个或多个设计原则。
  7. 7大设计原则背后又是一个字——分,体会“分”字:活字印刷的例子
  8. 7大设计原则分别是:

a.单一职责
b.里氏替换原则
c.依赖倒置原则
d.开闭原则
e.迪米特法则(最少知道原则)
f.接口隔离原则
g.组合优于继承原则

  1. 学习设计模式,牢记两点

a. 开发代码的程序员,分为两种:作者(服务端程序员),用户(客户端程序员)
b.

七大设计原则

1.单一职责

单一职责解释:每个方法、每个类、每个框架只负责一件事情
比如:
Math.round() 只负责四舍五入功能,其他不管(方法)
Reader类,只负责只负责读取文本文件(类)
SpringMVC,只负责简化MVC的开发(框架)

  • 案例:统计文本文件中有多少个单词。

    1. public class nagtive {
    2. public static void main(String[] args) {
    3. try{
    4. //读取文件的内容,统计一个文本文件中,有多少个字符。
    5. //Reader默认查询的码表是与操作系统一致的码表,
    6. //我们的操作系统是中文的,所以Reader就会使用GBK码表,而GBK码表一个汉字占2个字节
    7. //汉字字节以1开头,读到1时会与后一个字节结合读取,1开头一次读2个,0开头一次读1个
    8. Reader in = new FileReader("E:\\1.txt");
    9. BufferedReader bufferedReader = new BufferedReader(in);
    10. String line = null;
    11. StringBuilder sb = new StringBuilder("");
    12. while((line =bufferedReader.readLine()) != null){
    13. sb.append(line);
    14. sb.append(" ");
    15. }
    16. //对内容进行分割
    17. String[] words = sb.toString().split("[^a-zA-Z]+");
    18. System.out.println(words.length);
    19. bufferedReader.close();
    20. } catch (IOException e) {
    21. e.printStackTrace();
    22. }
    23. }
    24. }

    以上代码,违反了单一职责,缺点是:

  1. 代码的重用性不高,如果有其他需求,必须,需要统计一个文件中的句子数量,则必须把文件加载到字符串中的代码,再写一遍。
  2. 可读性低,别人一看这个方法,首先会被具体的算法搞晕,看代码的人根本看不出这个代码要干什么。

    1. public class postive {
    2. public static StringBuilder loadFile(String fileLocation) throws IOException {
    3. //读取文件的内容
    4. Reader in = new FileReader("E:\\1.txt");
    5. BufferedReader bufferedReader = new BufferedReader(in);
    6. String line = null;
    7. StringBuilder sb = new StringBuilder("");
    8. while ((line = bufferedReader.readLine()) != null) {
    9. sb.append(line);
    10. sb.append(" ");
    11. }
    12. bufferedReader.close();
    13. return sb;
    14. }
    15. public static String[] getWords(String regex, StringBuilder sb){
    16. //对内容进行分割
    17. return sb.toString().split(regex);
    18. }
    19. public static void main(String[] args) throws IOException {
    20. //读取文件的内容
    21. StringBuilder sb = loadFile("E:\\1.txt");
    22. //对内容进行分割
    23. String[] words = getWords("[^a-zA-Z]+", sb);
    24. System.out.println(words.length);
    25. }
    26. }

    修改后的代码符合单一职责,优点如下:

  3. 代码重用性提高

  4. 可读性提高,代码就像大纲一样

遵守单一原则,可以给我们带来的好处是,提高了代码的可重用性,同时还让得到的数据不再有耦合,可以用来完成我们的个性化需求。


2.里氏替换原则

里氏替换原则解释:任何能够使用父类对象的地方,都应该能够透明的替换为子类对象。也就是说,子类对象可以随时随地替换父类对象,且替换完以后,语法不会报错,业务逻辑也不会出现问题!
方法重写:在子类和父类中,出现了返回类型相同、方法名相同、方法参数相同的方法时,构成方法重写方法重写的两个限制:

  1. 子类重写父类的方法时,子类方法的访问修饰符不能比父类的更严格;
  2. 子类重写父类的方法时,子类方法不能抛出比父类更多的异常。

为什么要有以上这2个限制。就是为了保证,在子类对象替换父类对象后,进行透明的替换,不需要更改代码,就是为了代码符合里氏替换原则。

继承的作用:

  1. 提高代码重用性。
  2. 多态的前提。

两个类能不能发生继承关系的依据是什么?

  1. 有没有”is a”关系;
  2. 在两个类有了”is a”关系之后,还要考虑子类对象在替换了父类对象之后,业务逻辑是否变化!如果变化,则不能发生继承关系。

案例:将长方形的宽改成比长大 1 。
反例:在父类Rectangular下,业务场景符合逻辑。现有子类Square,替换后如何。

  1. public class negtive {
  2. static class Rectangular {
  3. private Integer width;
  4. private Integer length;
  5. public Integer getWidth() {
  6. return width;
  7. }
  8. public void setWidth(Integer width) {
  9. this.width = width;
  10. }
  11. public Integer getLength() {
  12. return length;
  13. }
  14. public void setLength(Integer length) {
  15. this.length = length;
  16. }
  17. }
  18. static class Square extends Rectangular {
  19. private Integer sideWidth;
  20. @Override
  21. public Integer getWidth() {
  22. return sideWidth;
  23. }
  24. @Override
  25. public void setWidth(Integer width) {
  26. this.sideWidth = width;
  27. }
  28. @Override
  29. public Integer getLength() {
  30. return sideWidth;
  31. }
  32. @Override
  33. public void setLength(Integer length) {
  34. this.sideWidth = length;
  35. }
  36. }
  37. static class Utils{
  38. public static void transform(Rectangular graph){
  39. while ( graph.getWidth() <= graph.getLength() ){
  40. graph.setWidth(graph.getWidth() + 1);
  41. System.out.println("长:"+graph.getLength()+" : " +
  42. "宽:"+graph.getWidth());
  43. }
  44. }
  45. }
  46. public static void main(String[] args) {
  47. // Rectangular graph = new Rectangular();
  48. Rectangular graph = new Square();
  49. graph.setWidth(20);
  50. graph.setLength(30);
  51. Utils.transform(graph);
  52. }
  53. }

替换后运行将是无限死循环,因为在正方形方法中,长宽修改的是一个变量,宽每次增一,长也会增一,所以宽永远追不上长,产生无限循环。
在向上转型的时候,方法的调用只和new的对象有关,才会造成不同的结果。在使用场景下,需要考虑替换后业务逻辑是否受影响。


3.依赖倒置原则

UML类图规则:

关联:一个类的对象,作为另一个类的字段
画法:实线加箭头
class Test{
}
class Test2{
private Test test;
//Test2关联Test
//Test被关联
}
image.png
依赖:一个类的方法中定义了另一个类作为局部字段
画法:虚线加箭头
class Test{
}
class Test2{
pubilic void test(){
Test test= new Test;
}
}
image.png
继承画法:实现加空心箭头
实现画法:虚线加空心箭头
image.png
依赖倒置原则解释:上层不能依赖于下层,它们都应该依赖于抽象。
什么是上层?什么是下层?调用别方法的,就是上层;被其他方法调用的,就是下层。
反例:人喂养动物

  1. static class Person{
  2. public void feed(Dog dog){
  3. dog.eat();
  4. }
  5. }
  6. static class Dog{
  7. public void eat() {
  8. System.out.println("狗啃骨头");
  9. }
  10. }
  11. public static void main(String[] args) {
  12. Person p = new Person();
  13. Dog d = new Dog();
  14. p.feed(d);
  15. }
  1. 在上面的代码中,人要喂狗,依赖于有一条狗,人作为上层依赖于下层,这样有什么坏处呢?<br />坏处是,当变化来临时,比如,人又养了一只猫,那么上层人这个类当中,就必须在添加喂猫的方法;人又养了一只熊猫,那么上层人这个类当中,就必须在添加喂熊猫的方法。因此每当下层变动时,上层也会跟着变动,而我们希望下层变动时,上层不会跟着改变,即下层增加一个动物时,上层不知道,也不需要改。<br />**正例**:他们都应该依赖于抽象。
  1. interface Animal{
  2. void eat();
  3. }
  4. static class Person{
  5. public void feed(Animal animal){
  6. animal.eat();
  7. }
  8. }
  9. static class Dog implements Animal{
  10. public void eat() {
  11. System.out.println("狗啃骨头");
  12. }
  13. }
  14. public static void main(String[] args) {
  15. Person p = new Person();
  16. Dog d = new Dog();
  17. p.feed(d);
  18. }
  1. 依赖倒转原则就是指:代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。通过面向接口编程,抽象不应该依赖于细节,细节应该依赖于抽象。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/21546143/1620289763286-709ab622-7142-49dc-bda1-de55369a9124.png#clientId=ue7d538fd-e73b-4&from=paste&height=197&id=u24b7cc58&margin=%5Bobject%20Object%5D&name=image.png&originHeight=662&originWidth=1143&originalType=binary&size=63868&status=done&style=none&taskId=ue1362a15-c851-4f5d-b6d5-d20f272136d&width=340)![image.png](https://cdn.nlark.com/yuque/0/2021/png/21546143/1620289741175-a01c272a-e2ef-4ab1-bc8a-76db769308b9.png#clientId=ue7d538fd-e73b-4&from=paste&height=206&id=KdWJh&margin=%5Bobject%20Object%5D&name=image.png&originHeight=707&originWidth=1232&originalType=binary&size=74357&status=done&style=none&taskId=u8f69e2bf-dff3-4936-a373-a21bc8d96b5&width=359)<br />箭头本是向下指,依赖于抽象后,箭头方向改变,依赖发生了改变,称为依赖倒置。上下层都依赖于抽象,指向抽象,上下层解耦。

4.开闭原则

开闭原则解释:a.对扩展新功能是开放的b.对修改原有功能是关闭的
比如:
有一个刮胡刀,刮胡刀的作用就是刮胡子,现在想让刮胡刀具备吹风机能力。
违反开闭原则的做法就是:把吹风功能加上了,可能不能刮胡子了。
符合开闭原则的做法就是:把吹风功能加上了,且没有影响以前的刮胡子功能。
案例:创建了一个汽车对象

  1. class Car {
  2. private String band;
  3. private String color;
  4. private float price;
  5. public String getBand() {
  6. return band;
  7. }
  8. public void setBand(String band) {
  9. this.band = band;
  10. }
  11. public String getColor() {
  12. return color;
  13. }
  14. public void setColor(String color) {
  15. this.color = color;
  16. }
  17. public float getPrice() {
  18. return price;
  19. }
  20. public void setPrice(float price) {
  21. this.price = price;
  22. }
  23. @Override
  24. public String toString() {
  25. return "Car{" +
  26. "band='" + band + '\'' +
  27. ", color='" + color + '\'' +
  28. ", price=" + price +
  29. '}';
  30. }
  31. }
  32. public static void main(String[] args) {
  33. Car car = new Car();
  34. car.setBand("bm");
  35. car.setColor("red");
  36. car.setPrice(13.3f);
  37. System.out.println(car.toString());
  38. }

当变化来临时,例如汽车的价格现在需要打折(打8折),这时,我们在Car的源代码中修改,就违反了开闭原则。在开发时,我们应该,必须去考虑可能会变化的需求,属性在任何时候都可能发生改变,对于需求的变化,在要求遵守开闭原则的前提下,我们应该在开发中去进行扩展,而不是修改源代码。
反例:修改了源代码

  1. public void setPrice(float price) {
  2. this.price = (price*0.08f);
  3. }

开发时,我们应该要去考虑变化的需求,属性会在任何时刻都有可能产生变化。
对待变化时,我们应该选择扩展,而不是去修改源代码(ps:在强制要求符合开闭原则的情况下!)
正例:进行扩展

  1. //进行扩展
  2. class DiscountCar extends Car{
  3. @Override
  4. public void setPrice(float price) {
  5. super.setPrice(price*0.08f);
  6. }
  7. }
  8. public static void main(String[] args) {
  9. //使用向上转型时,方法的调用只和new的对象有关
  10. Car car = new DiscountCar();
  11. car.setBand("bm");
  12. car.setColor("red");
  13. car.setPrice(130000f);
  14. System.out.println(car.toString());
  15. }

开闭原则应该遵循应用场景去考虑,如果源代码就是你自己写的,而且需求是稳定的,那么,直接修改源代码也是一个简单的做法,但当源代码是别人的代码或架构是,我们就要去符合开闭原则,防止破坏结构的完整性!


5.迪米特法则

迪米特法则解释:也叫最少知道原则。一个类对于其他类,要知道越少越好,体现封装的特性。只和朋友通信,不和陌生人通话。
反例:

  1. public class negtive {
  2. class Computer{
  3. public void closeFile(){
  4. System.out.println("关闭文件");
  5. }
  6. public void closeScreen(){
  7. System.out.println("关闭屏幕");
  8. }
  9. public void powerOff(){
  10. System.out.println("断电");
  11. }
  12. }
  13. class Person{
  14. private Computer computer;
  15. public void offComputer(){
  16. computer.closeFile();
  17. computer.closeScreen();
  18. computer.powerOff();
  19. }
  20. }
  21. }

这时候,Person知道了Computer的很多细节,对于用户来说不够友好,而且,用户还可能会调用错误,先断电,再保存文件,显然不符合逻辑,会导致文件出现未保存的错误。
其实对于用户来说,知道进行关机就行了。
正例:封装细节

  1. public class postive {
  2. class Computer{
  3. public void closeFile(){
  4. System.out.println("关闭文件");
  5. }
  6. public void closeScreen(){
  7. System.out.println("关闭屏幕");
  8. }
  9. public void powerOff(){
  10. System.out.println("断电");
  11. }
  12. public void turnOff(){ //将细节实现进行封装
  13. this.closeFile();
  14. this.closeScreen();
  15. this.powerOff();
  16. }
  17. }
  18. class Person{
  19. private Computer computer;
  20. public void offComputer(){
  21. computer.turnOff();
  22. }
  23. }
  24. }

前面说的,只和朋友通信,不和陌生人说话。先来明确一下什么才叫做朋友:

什么是朋友?

  1. 类中的字段
  2. 方法的返回值
  3. 方法的参数
  4. 方法中的实例对象
  5. 对象本身
  6. 集合中的泛型

总的来说,只要在自身内定义的就是朋友,通过其他方法得到的都只是朋友的朋友;但是,朋友的朋友不是我的朋友。
举个反例:

  1. Copypublic class negtive {
  2. class Market{
  3. private Computer computer;
  4. public Computer getComputer(){
  5. return this.computer;
  6. }
  7. }
  8. static class Computer{
  9. public void closeFile(){
  10. System.out.println("关闭文件");
  11. }
  12. public void closeScreen(){
  13. System.out.println("关闭屏幕");
  14. }
  15. public void powerOff(){
  16. System.out.println("断电");
  17. }
  18. }
  19. class Person{
  20. private Market market;
  21. Computer computer =market.getComputer();
  22. // //此时的 computer 并不是 Person 的朋友,只是 Market 的朋友。
  23. }
  24. }

在实际开发中,要完全符合迪米特法则,也会有缺点:

  • 在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的业务逻辑无关。
  • 遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。

因此,前人总结出,一些方法论以供我们参考:

  1. 优先考虑将一个类设置成不变类。
  2. 尽量降低一个类的访问权限。
  3. 谨慎使用Serializable。
  4. 尽量降低成员的访问权限。

虽然规矩很多,但是理论需要深刻理解,实战需要经验积累。路还很长。


6.接口隔离原则

接口隔离原则解释:设计接口时,接口的抽象应该是有意义的
反例:动物接口中定义的方法并不是被所有动物需要的

  1. interface Animal{
  2. void eat();
  3. void fly();
  4. void swim();
  5. }
  6. class Bird implements Animal{
  7. @Override
  8. public void eat() {
  9. System.out.println("吃东西");
  10. }
  11. @Override
  12. public void fly() {
  13. System.out.println("飞");
  14. }
  15. //鸟不会游泳,并不需要实现
  16. @Override
  17. public void swim() {
  18. System.out.println("游泳");
  19. }
  20. }

正例:接口抽象出有意义的层级,供需要的类去实现

  1. interface Flyable{
  2. void fly();
  3. }
  4. interface Swimable{
  5. void swim();
  6. }
  7. interface Eatable{
  8. void eat();
  9. }
  10. class Bird implements Flyable,Eatable{
  11. .....
  12. }
  13. class Dog implements Swimable,Eatable{
  14. .....
  15. }

客户端不应该依赖那些它不需要的接口。一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。使用多个接口比使用一个总接口要好。


7.组合优于继承原则

组合由于继承原则解释:复用别人的代码时,不宜使用继承,应该使用组合。
继承就是一个类继承另外一个类。我们已经知道,类和类之间有3种:a.继承 b.依赖 c.关联
“关联”可以细分为:

  1. 组合
  2. 聚合

所谓的组合,是关系强,聚合是关系弱。
需求:制作一个组合,该集合能够记录下曾经添加过多少元素。(不只是统计某一时刻)
反例 #1:

  1. Copypublic class negtive_1 {
  2. static class MySet extends HashSet{
  3. private int count = 0;
  4. public int getCount() {
  5. return count;
  6. }
  7. @Override
  8. public boolean add(Object o) {
  9. count++;
  10. return super.add(o);
  11. }
  12. }
  13. public static void main(String[] args) {
  14. MySet mySet = new MySet();
  15. mySet.add("111111");
  16. mySet.add("22222222222222");
  17. mySet.add("2333");
  18. Set hashSet = new HashSet();
  19. hashSet.add("集合+11111");
  20. hashSet.add("集合+22222222");
  21. hashSet.add("集合+233333");
  22. mySet.addAll(hashSet);
  23. System.out.println(mySet.getCount());
  24. }
  25. }

看似解决了需求,add方法可以成功将count进行自加,addAll方法通过方法内调用add,可以成功将count进行增加操作。
缺陷:JDK版本如果未来进行更新,addAll方法不再通过方法内调用add,那么当调用addAll进行集合添加元素时,count将不无从进行自加。需求也将无法满足。HashMap 就在1.6 1.7 1.8就分别更新了三次。
反例 #2:

  1. Copypublic class negtive_2 {
  2. static class MySet extends HashSet{
  3. private int count = 0;
  4. public int getCount() {
  5. return count;
  6. }
  7. @Override
  8. public boolean add(Object o) {
  9. count++;
  10. return super.add(o);
  11. }
  12. @Override
  13. public boolean addAll(Collection c) {
  14. boolean modified = false;
  15. for (Object e : c)
  16. if (add(e))
  17. modified = true;
  18. return modified;
  19. }
  20. }
  21. public static void main(String[] args) {
  22. MySet mySet = new MySet();
  23. mySet.add("111111");
  24. mySet.add("22222222222222");
  25. mySet.add("2333");
  26. Set hashSet = new HashSet();
  27. hashSet.add("集合+11111");
  28. hashSet.add("集合+22222222");
  29. hashSet.add("集合+233333");
  30. mySet.addAll(hashSet);
  31. System.out.println(mySet.getCount());
  32. }
  33. }

亲自再重写addAll方法,确保addAll方法一定能调用到add方法,也就能对count进行增加操作。
但是,问题还是有的:

  • 如果未来,HashSet新增了一个addSome方法进行元素的添加,那就白给了。
  • 重写了addAll、add这两个方法,如果JDK中其他类的某些方法依赖于HashMap中的这两个方法,那么JDK中其他类依赖于HashMap中的这两个方法的某些方法就会有出错、崩溃等风险。

这时候,可以得出一些结论:
当我们不属于继承父类的开发团队时,是没办法保证父类代码不会被修改,或者修改时一定被通知到,这时候,就可能会出现需求满足有缺陷的情况。所以,但我们去复用父类的代码时,避免去重写或者新建方法,这样可以防止源代码结构发生改变带来的打击。也就是说,我们在重用代码时,应该是组合优于继承。
正例:

  1. Copypublic class postive {
  2. static class MySet{
  3. private HashSet hashSet = new HashSet();
  4. private int count = 0;
  5. public int getCount() {
  6. return count;
  7. }
  8. public boolean add(Object o) {
  9. count++;
  10. return hashSet.add(o);
  11. }
  12. public boolean addAll(Collection c) {
  13. count += c.size();
  14. return hashSet.addAll(c);
  15. }
  16. }
  17. public static void main(String[] args) {
  18. MySet mySet = new MySet();
  19. mySet.add("111111");
  20. mySet.add("22222222222222");
  21. mySet.add("2333");
  22. Set hashSet = new HashSet();
  23. hashSet.add("集合+11111");
  24. hashSet.add("集合+22222222");
  25. hashSet.add("集合+233333");
  26. mySet.addAll(hashSet);
  27. System.out.println(mySet.getCount());
  28. }
  29. }

利用组合,实现解耦,将HashSet和自定义类MySet由原来的继承关系改为了低耦合的组合关系。
JDK中的反例:Stack继承了Vector,因此违反了先进后出的原则。

设计模式

创建型

一、简单工厂

image.png

  1. 每个模块负责自己的职责(单一职责),各个模块之间通过接口隔离
  2. 每个模块都应该“承诺”自己对外暴露的接口是不变的。当模块内部发生变化时,其他模块是不需要知道的。这便是依赖于抽象而不依赖于实现(里氏替换原则)
  3. 上层模块只需要知道下层模块暴露出的接口即可,至于实现细节不需要也不应该知道的(迪米特法则)
    工厂模式相关概念:
  4. 产品: new 出的对象,都是产品。
  5. 抽象产品: 抽象类、接口,都称之为抽象产品。
  6. 产品簇: 多个抽象产品组成一个产品系列。
  7. 产品等级: 产品簇中的每一个抽象产品都是产品等级。

简述:创建产品接口,需要产品时,利用工厂进行创建即可。

  1. //反例
  2. public class negtive {
  3. /*===============服务端======================*/
  4. interface Food{
  5. void eat();
  6. }
  7. static class Noodles implements Food{
  8. @Override
  9. public void eat() {
  10. System.out.println("吃面条。。。。。");
  11. }
  12. }
  13. /*=================客户端===================*/
  14. public static void main(String[] args) {
  15. Food food01 = new Noodles();
  16. food01.eat();
  17. }
  18. }

以上代码, 客户端除了要知道接口的类型,还必须知道接口的实现类的类型,失败!!这样做的缺点是:当服务端代码在新的版本中把具体类的类名修改了,则客户端代码直接“崩塌”!!
UML类图如下:
设计模式学习笔记 - 图6
这时候,产品来改需求来了,咱们现在这Noodles改名了,得改个特牛逼的名字Spaghetti,让用户记住咱们这是西餐意大利面。
因为原有设计是上面的反例,你得能从修改服务端的源代码开始,再修改客户端源代码。
这种设计过于脆弱,因为这样服务端源代码和客户端源代码是耦合的,改变会牵一发而动全身。
针对于以上缺点, 重构代码如下:

  1. 添加了一个简单工厂: 根据参数的不同,而返回不同的具体产品。
  2. 优点是:客户端再也不需要知道,具体产品的细节了(类名)

    1. //正例:
    2. public class postive {
    3. /*===============服务端======================*/
    4. interface Food{
    5. void eat();
    6. }
    7. static class Spaghetti implements Food {
    8. @Override
    9. public void eat() {
    10. System.out.println("吃西餐面条。。。。。");
    11. }
    12. }
    13. static class FoodFactory {
    14. public Food getFood(int num){
    15. Food food =null;
    16. switch (num){
    17. case 1 :
    18. food = new Spaghetti();
    19. }
    20. return food;
    21. }
    22. }
    23. /*=================客户端===================*/
    24. public static void main(String[] args) {
    25. FoodFactory foodFactory = new FoodFactory();
    26. Food food01 = foodFactory.getFood(1);
    27. food01.eat();
    28. }
    29. }

    缺点:

  3. 从某种程度上违反单一职责。

  4. 如果具体产品特别多,则简单工厂的代码将会十分臃肿。 (这也是有人把简单工厂叫做“上帝类”的原因。)
  5. 增加了客户端代码的复杂度,客户端程序员,必须知道每个数字,都分别映射什么具体产品
  6. 最关键的缺点:现在,客户端要添加新产品。客户端倒是能自己扩展一个新产品,但是为了把新产品加入简单工厂中,势必要违反“开闭原则”。

UML类图如下:
设计模式学习笔记 - 图7
通过这样一个正例,把创建对象的代码全交给服务端处理,将服务端代码和客户端代码进行了解耦。这样做的好处,不只是服务端开发人员受益,当服务端代码修改时,客户端也不知道,也不需要知道。
简单工厂模式的缺点:

  • 客户必须记住工厂中常量和具体产品的映射关系。
  • 一旦产品品种体量增大到一定程度,工厂类将变得非常臃肿。
  • 最致命的缺陷,增加产品时,就要修改工厂类。违反开闭原则。

由此,就引出了下一个设计模式: 工厂方法设计模式

二、工厂方法

  1. // 正例
  2. public class postive {
  3. /*===============服务端======================*/
  4. //-----------------------产品--------------------
  5. interface Food{
  6. void eat();
  7. }
  8. static class Spaghetti implements Food {
  9. @Override
  10. public void eat() {
  11. System.out.println("吃西餐面条。");
  12. }
  13. }
  14. //新增产品
  15. static class Rice implements Food {
  16. @Override
  17. public void eat() {
  18. System.out.println("吃米饭。");
  19. }
  20. }
  21. //--------------------------工厂-----------------------
  22. interface FoodFactory {
  23. Food getFood();
  24. }
  25. static class SpaghettiFactory implements FoodFactory{
  26. @Override
  27. public Food getFood() {
  28. return new Spaghetti();
  29. }
  30. }
  31. //新增产品工厂
  32. static class RiceFactory implements FoodFactory{
  33. @Override
  34. public Food getFood() {
  35. return new Rice();
  36. }
  37. }
  38. /*=================客户端===================*/
  39. public static void main(String[] args) {
  40. FoodFactory foodFactory = new SpaghettiFactory();
  41. Food food01 = foodFactory.getFood();
  42. food01.eat();
  43. }
  44. }

UML类图如下:
设计模式学习笔记 - 图8
工厂方法的优点:

  1. 仍然具有简单工厂的优点:服务器端修改了具体产品的类名以后,客户端不知道!
  2. 当客户端需要扩展一个新的产品时,不需要修改作者原来的代码,只是扩展一个新的工厂而已!

杠点:

  1. 我们已经知道,简单工厂也好,工厂方法也好,都有一个优点,就是服务器端的具体产品类名变化了以后,客户端不知道!但是,反观我们现在的代码,客户端仍然依赖于具体的工厂的类名呀!此时,如果服务器端修改了具体工厂的类名,那么客户端也要随之一起修改!感觉折腾了一圈,又回到了原点!!!
  • 对1的解释:工厂的名字,是为视为接口的。作者有责任,有义务,保证工厂的名字是稳定的。也就是说,虽然客户端依赖于工厂的具体类名,可是在IT业内,所有工厂的名字都是趋向于稳定(并不是100%不会变)。至少工厂类的名字,要比具体产品类的名字更加稳定!
  1. 既然产品是我们自己客户端扩展出来的,那为什么不直接自己实例化呢?毕竟这个扩展出来的Lp这个产品,我们自己就是作者。我们想怎么改类名自己都能把控!为什么还要为自己制作的产品做工厂呢?
  • 对2的解释:因为,作者在开发功能时,不仅仅只会开发一些抽象产品、具体产品、对应的工厂,还会配套地搭配一些提前做好的框架。

针对简单工厂违反开闭原则的这一缺陷,工厂方法模式进行优化。可以看到此时再去增加产品,不再需要修改工厂类,而是增加相应的产品类和工厂类即可。这是符合开闭原则的。

  1. 如果源代码作者修改相关工厂类的类名,那这时候调用工厂类的客户端代码就需要修改了,这不如简单工厂呢?

首先这里要明确一个概念,工厂类在实际使用中,是相当于接口类的,接口类一般不允许进行修改(非必须),工厂类作者有责任,有义务保证工厂类的类名是稳定的,也就是说,工厂类是比产品类更加稳定的。

  1. 既然使我们后面自己扩展的Rice类,为什么不直接实例化它,直接使用。我们就是作者,为什么不能直接使用?

这里需要扩展一下,有时候一个产品类并不是孤立的,它和其他类一起组成一个服务框架。
下面增加一些类:

  1. /*===============服务端======================*/
  2. //------------------------产品质检流程-----------------------、
  3. static class QualityInspection {
  4. public void checking(FoodFactory foodFactory){
  5. System.out.println("我是质检员。。。。。准备开吃 -_- ");
  6. Food food = foodFactory.getFood();
  7. food.eat();
  8. }
  9. }
  10. /*=================客户端===================*/
  11. public static void main(String[] args) {
  12. FoodFactory foodFactory01 = new SpaghettiFactory();
  13. FoodFactory foodFactory02 = new RiceFactory();
  14. QualityInspection inspection = new QualityInspection();
  15. inspection.checking(foodFactory02);
  16. inspection.checking(foodFactory01);
  17. }

UML类图如下:
设计模式学习笔记 - 图9
这时候,如果Rice没有他的工厂类,甚至都没办法参加质检,那还怎么卖?
所以编写工厂类并不只是单纯为了实例化某些产品类,而是能让配套服务通过工厂接口,得以调用工厂创建产品实例。
那为什么QualityInspection的checking方法不直接调用Food接口再进行产品的实例化呢?这时候回到简单工厂模式,产品类不同于工厂类,它是善变的,它会随着需求的变化而变化,这时候,直接依赖产品类的各种方法,将需要被修改,违反开闭原则。这是死路。
工厂方法模式的缺陷:

  • 当业务需要的类型变多,目前只有食物,当产生饮料,日用品等类别时,我们又要创建新的工厂来实现,造成代码重复臃肿。

    三、抽象工厂

    针对工厂方法模式的缺陷,抽象工厂模式将进行改进,一个工厂负责创建一个产品簇的对象。
    工厂中的概念,产品(类),抽象产品(抽象类、接口),产品簇(多个有内在联系或逻辑关系的产品),产品等级()。
    设计模式学习笔记 - 图10 简述:在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。
    抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

    1. public class postive {
    2. /*===============服务端======================*/
    3. //-----------------------产品--------------------
    4. /*----------------螺丝---------------------*/
    5. interface Screw{
    6. void createScrew();
    7. }
    8. static class Screw_06 implements Screw {
    9. @Override
    10. public void createScrew() {
    11. System.out.println("create Screw_06 666666。。。。。");
    12. }
    13. }
    14. static class Screw_08 implements Screw {
    15. @Override
    16. public void createScrew() {
    17. System.out.println("create Screw_08 8888888。。。。。");
    18. }
    19. }
    20. /*----------------螺母---------------------*/
    21. interface Nut{
    22. void createNut();
    23. }
    24. static class Nut_06 implements Nut {
    25. @Override
    26. public void createNut() {
    27. System.out.println("create Nut_06 666666。。。。。");
    28. }
    29. }
    30. static class Nut_08 implements Nut {
    31. @Override
    32. public void createNut() {
    33. System.out.println("create Nut_08 8888888。。。。。");
    34. }
    35. }
    36. //--------------------------工厂-----------------------
    37. interface ComponentsFactory {
    38. Screw getScrew();
    39. Nut getNut();
    40. }
    41. /*----------------6号工厂---------------------*/
    42. static class Factory_666 implements ComponentsFactory {
    43. @Override
    44. public Screw getScrew() {
    45. return new Screw_06();
    46. }
    47. @Override
    48. public Nut getNut() {
    49. return new Nut_06();
    50. }
    51. }
    52. /*----------------8号工厂---------------------*/
    53. static class Factory_888 implements ComponentsFactory {
    54. @Override
    55. public Screw getScrew() {
    56. return new Screw_08();
    57. }
    58. @Override
    59. public Nut getNut() {
    60. return new Nut_08();
    61. }
    62. }
    63. //------------------------产品质检流程-----------------------、
    64. static class QualityInspection {
    65. public void checking(ComponentsFactory Factory){
    66. System.out.println("我是人肉质检员。。。。。等待产出零件 -_- ");
    67. Screw screw = Factory.getScrew();
    68. Nut nut = Factory.getNut();
    69. screw.createScrew();
    70. nut.createNut();
    71. System.out.println("开始质检.......");
    72. System.out.println(" ");
    73. }
    74. }
    75. /*=================客户端===================*/
    76. public static void main(String[] args) {
    77. ComponentsFactory Factory01 = new Factory_666();
    78. ComponentsFactory Factory02 = new Factory_888();
    79. QualityInspection inspection = new QualityInspection();
    80. inspection.checking(Factory01);
    81. inspection.checking(Factory02);
    82. }
    83. }

    UML类图如下:
    设计模式学习笔记 - 图11
    可以看到,如果在需要进行一种N号螺丝或者螺母的扩展,只需要增加一个实现N号螺丝或者螺母接口的产品类,利用一个新增N号工厂进行创建即可。
    可以看到,抽象工厂仍然保持着简单工厂模式和工厂方法模式的优点:

  • 服务端代码和客户端代码是低耦合的。(简单工厂模式)

  • 所有这一切动作都是新增,不是修改,符合开闭原则。

还新增了一个特有的优点:

  • 抽象工厂有效减少了工厂的数量,一个工厂就生产同一个产品簇的产品。

再次强调,一个抽象工厂负责创建同一个产品簇的对象。而产品簇是指多个存在内在联系的或者存在逻辑关系的产品。也就是6号工厂只生产6号的零部件,不负责生产8号零部件。不能不同产品簇的产品混合到一个工厂中进行生产。
缺陷:当增加产品簇时(增加6、8号螺帽的生产),这时候就要修改以前工厂(6、8号工厂)的源代码了。
解释:抽象工厂中,可以生产多个产品,这多个产品之间,必须有内在联系。同一个工厂中的产品都属于同一个产品簇!!不能把不同产品簇中的产品混合到一个抽象工厂的实现类中。
缺点:当产品等级发生变化时(增加产品等级),要修改以前所有的工厂代码的修改,违反了开闭原则
总结就是:

  • 当产品簇比较固定时,考虑使用抽象工厂。
  • 当产品簇经常变动时,不建议使用抽象工厂。

    四、原型

    原型模式解释:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
    用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

    1. public class negtive {
    2. /*==============服务端=======================*/
    3. static class Resume{
    4. private String name;
    5. private Integer age;
    6. public String getName() {
    7. return name;
    8. }
    9. public void setName(String name) {
    10. this.name = name;
    11. }
    12. public Integer getAge() {
    13. return age;
    14. }
    15. public void setAge(Integer age) {
    16. this.age = age;
    17. }
    18. @Override
    19. public String toString() {
    20. return "Resume{" +
    21. "name='" + name + '\'' +
    22. ", age=" + age +
    23. '}';
    24. }
    25. }
    26. /*==============客户端=========================*/
    27. public static void main(String[] args) {
    28. Resume resume01= new Resume();
    29. resume01.setName("ling001");
    30. resume01.setAge(20);
    31. System.out.println(resume01);
    32. Resume resume02= new Resume();
    33. resume02.setName("ling002");
    34. resume02.setAge(23);
    35. System.out.println(resume02);
    36. }
    37. }

    复制多份简历需要一个个去new。Java就提供了这种复制粘贴的办法,不过他有自己的名字—Clone。

    1. public class postvie_01 {
    2. /*==============服务端=======================*/
    3. static class Resume implements Cloneable{
    4. private String name;
    5. private Integer age;
    6. public String getName() {
    7. return name;
    8. }
    9. public void setName(String name) {
    10. this.name = name;
    11. }
    12. public Integer getAge() {
    13. return age;
    14. }
    15. public void setAge(Integer age) {
    16. this.age = age;
    17. }
    18. @Override
    19. public String toString() {
    20. return "Resume{" +
    21. "name='" + name + '\'' +
    22. ", age=" + age +
    23. '}';
    24. }
    25. @Override
    26. protected Object clone() throws CloneNotSupportedException {
    27. return super.clone();
    28. }
    29. }
    30. /*==============客户端=========================*/
    31. public static void main(String[] args) throws CloneNotSupportedException {
    32. Resume resume01= new Resume();
    33. resume01.setName("ling001");
    34. resume01.setAge(20);
    35. Resume resume02= (Resume) resume01.clone();
    36. resume02.setName("li666");
    37. System.out.println(resume01);
    38. System.out.println(resume02);
    39. System.out.println(resume01.equals(resume02));
    40. }
    41. }
    42. //克隆对象,得到一个新对象
    43. //思考1:clone方法,会不会引起构造器的调用?不会!那么clone方法是如何实现克隆对象的效果呢?
    44. //clone方法是直接赋值内存中的二进制。效率更高!
    45. //思考2:既然clone方法没有引起构造器的调用,那么克隆出的对象和原先的对象地址是否一致?不一致!
    46. //最终是两个不同空间中的对象。

    不需要new,只需要服务端先实现一个Cloneable,并重写clone方法即可。而且作用堪比new一个新的对象,因为克隆和被克隆的对象并不是同一个,equals的时候得到的是false的。
    这时候,新需求来了,为简历增加一个工作经历的内容。

    1. public class negtive_02 {
    2. /*==============服务端=======================*/
    3. static class Resume implements Cloneable{
    4. private String name;
    5. private Integer age;
    6. private Date workExperience;
    7. public Date getWorkExperience() {
    8. return workExperience;
    9. }
    10. public void setWorkExperience(Date workExperience) {
    11. this.workExperience = workExperience;
    12. }
    13. public String getName() {
    14. return name;
    15. }
    16. public void setName(String name) {
    17. this.name = name;
    18. }
    19. public Integer getAge() {
    20. return age;
    21. }
    22. public void setAge(Integer age) {
    23. this.age = age;
    24. }
    25. @Override
    26. public String toString() {
    27. return "Resume{" +
    28. "name='" + name + '\'' +
    29. ", age=" + age +
    30. ", workExperience=" + workExperience +
    31. '}';
    32. }
    33. @Override
    34. protected Object clone() throws CloneNotSupportedException {
    35. return super.clone();
    36. }
    37. }
    38. /*==============客户端=========================*/
    39. public static void main(String[] args) throws CloneNotSupportedException{
    40. Resume resume01= new Resume();
    41. Date date = new Date();
    42. resume01.setName("ling001");
    43. resume01.setAge(20);
    44. resume01.setWorkExperience(date);
    45. Resume resume02= (Resume) resume01.clone();
    46. resume02.getWorkExperience().setTime(0);
    47. System.out.println(resume02);
    48. System.out.println(resume01);
    49. System.out.println(resume01.equals(resume02));
    50. }
    51. }
    52. /*输出如下:
    53. Resume{name='ling001', age=20, workExperience=Thu Jan 01 08:00:00 CST 1970}
    54. Resume{name='ling001', age=20, workExperience=Thu Jan 01 08:00:00 CST 1970}
    55. false
    56. */

    这是一个关于深拷贝、浅拷贝的问题。

    深拷贝与浅拷贝

    浅拷贝(浅复制):clone得到的对象a其实只是对被clone对象b的引用,即对象a是指向b对象上的。
    设计模式学习笔记 - 图12
    深拷贝(深复制):clone得到的对象a是对被clone对象b的复制,即对象a和b是两个不同的对象,a只复制了b的内容。
    设计模式学习笔记 - 图13
    Java中,对一对象的clone中深拷贝对象的基本类型字段,浅拷贝引用类型字段。
    这时候要将浅拷贝改为深拷贝。
    只需要重写对象的clone方法即可。] ```java

    1. @Override
    2. public Object clone() throws CloneNotSupportedException {
    3. Resume clone = (Resume) super.clone();
    4. Date cloneDate = (Date) clone.getWorkExperience().clone();
    5. clone.setWorkExperience(cloneDate);
    6. return clone;
    7. }

/输出如下: Resume{name=’ling001’, age=20, workExperience=Thu Jan 01 08:00:00 CST 1970} Resume{name=’ling001’, age=20, workExperience=Fri May 21 20:43:31 CST 2021} false /

  1. 其实就是对**浅拷贝**的字段再进行**深拷贝**。<br />以上面用到的Date引用类型对象为例:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/21546143/1621602071270-ed1305de-76a7-42cd-903b-0aa7d2bf3a24.png#clientId=u81a7129c-2981-4&from=paste&height=156&id=u7d8236b5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=240&originWidth=795&originalType=binary&size=31503&status=done&style=none&taskId=u9171f666-cd23-4b09-941c-06203583e70&width=516.5)<br />可以看到Date是实现了Cloneable接口的,即表示Date也是可以进行clone(克隆)的。只需要将浅拷贝的Date再使用clone方法进行一次深拷贝,再赋值给clone的对象即可。具体参照上面重写的clone方法。<br />总结:<br />这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
  2. <a name="jzAUB"></a>
  3. ### 五、建造者
  4. 建造者模式解释:建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
  5. ```java
  6. //明确实体类
  7. public class Computer {
  8. private String cpu;
  9. private String gpu;
  10. private String Hd;
  11. private String RAM;
  12. public String getCpu() {
  13. return cpu;
  14. }
  15. public void setCpu(String cpu) {
  16. this.cpu = cpu;
  17. }
  18. public String getGpu() {
  19. return gpu;
  20. }
  21. public void setGpu(String gpu) {
  22. this.gpu = gpu;
  23. }
  24. public String getHd() {
  25. return Hd;
  26. }
  27. public void setHd(String hd) {
  28. Hd = hd;
  29. }
  30. public String getRAM() {
  31. return RAM;
  32. }
  33. public void setRAM(String RAM) {
  34. this.RAM = RAM;
  35. }
  36. @Override
  37. public String toString() {
  38. return "Computer{" +
  39. "cpu='" + cpu + '\'' +
  40. ", gpu='" + gpu + '\'' +
  41. ", Hd='" + Hd + '\'' +
  42. ", RAM='" + RAM + '\'' +
  43. '}';
  44. }
  45. }
  1. //反例1
  2. public class negtive_01 {
  3. public static void main(String[] args) {
  4. Computer computer_01 = new Computer();
  5. computer_01.setCpu("R5-3600");
  6. computer_01.setGpu("RTX3060Ti");
  7. computer_01.setHd("SSD1TB");
  8. computer_01.setRAM("32GB");
  9. Computer computer_02 = new Computer();
  10. computer_02.setCpu("i5-10400");
  11. computer_02.setGpu("RTX3060");
  12. computer_02.setHd("SSD500GB");
  13. computer_02.setRAM("16GB");
  14. System.out.println(computer_02);
  15. System.out.println(computer_01);
  16. }
  17. }

缺陷:建造复杂对象的时候,客户端程序猿要炸,造成客户端代码臃肿,且违反迪米特法则

  1. //反例2
  2. public class negtive_02 {
  3. /*=============服务端==========================*/
  4. static class HighComputerBuilder {
  5. private Computer computer = new Computer();
  6. public Computer build() {
  7. computer.setCpu("R5-3600");
  8. computer.setGpu("RTX3060Ti");
  9. computer.setHd("SSD1TB");
  10. computer.setRAM("32GB");
  11. return computer;
  12. }
  13. }
  14. static class High_02ComputerBuilder {
  15. private Computer computer = new Computer();
  16. public Computer build() {
  17. computer.setCpu("i5-10400");
  18. computer.setGpu("RTX3060");
  19. computer.setHd("SSD500GB");
  20. computer.setRAM("16GB");
  21. return computer;
  22. }
  23. }
  24. /*=====================客户端===============================*/
  25. public static void main(String[] args) {
  26. HighComputerBuilder builder_01 = new HighComputerBuilder();
  27. Computer computer_01 =builder_01.build();
  28. High_02ComputerBuilder builder_02 = new High_02ComputerBuilder();
  29. Computer computer_02 =builder_02.build();
  30. System.out.println(computer_01);
  31. System.out.println(computer_02);
  32. }
  33. }

创造了建造者类,用于创建复杂对象。
UML类图如下:
设计模式学习笔记 - 图14
缺陷:建造者遗漏部分建造步骤编译也会通过,会造成建造出来的对象不符合要求。比如,漏执行建造setHd时,使得值为null,这显然是不应该的,后续对象属性被调用时,可能会抛出空指针NullPointerException异常,会造成程序崩溃。

  1. //反例3
  2. public class negtive_03 {
  3. /*=============服务端==========================*/
  4. interface ComputerBuilder{
  5. Computer build();
  6. void setCpu();
  7. void setGpu();
  8. void setHd();
  9. void setRAM();
  10. }
  11. static class HighComputerBuilder implements ComputerBuilder{
  12. private Computer computer = new Computer();
  13. @Override
  14. public Computer build() {
  15. return computer;
  16. }
  17. @Override
  18. public void setCpu() {
  19. computer.setCpu("R5-3600");
  20. }
  21. @Override
  22. public void setGpu() {
  23. computer.setGpu("RTX3060");
  24. }
  25. @Override
  26. public void setHd() {
  27. computer.setHd("SSD1TB");
  28. }
  29. @Override
  30. public void setRAM() {
  31. computer.setRAM("32GB");
  32. }
  33. }
  34. static class High_02ComputerBuilder implements ComputerBuilder{
  35. private Computer computer = new Computer();
  36. @Override
  37. public Computer build() {
  38. return computer;
  39. }
  40. @Override
  41. public void setCpu() {
  42. computer.setCpu("i5-10400");
  43. }
  44. @Override
  45. public void setGpu() {
  46. computer.setGpu("RTX3060");
  47. }
  48. @Override
  49. public void setHd() {
  50. computer.setHd("SSD500GB");
  51. }
  52. @Override
  53. public void setRAM() {
  54. computer.setRAM("16GB");
  55. }
  56. }
  57. /*==============客户端=====================================*/
  58. public static void main(String[] args) {
  59. HighComputerBuilder builder_01 = new HighComputerBuilder();
  60. builder_01.setCpu();
  61. builder_01.setGpu();
  62. builder_01.setHd();
  63. builder_01.setRAM();
  64. Computer computer_01 =builder_01.build();
  65. High_02ComputerBuilder builder_02 = new High_02ComputerBuilder();
  66. builder_02.setCpu();
  67. builder_02.setGpu();
  68. builder_02.setHd();
  69. builder_02.setRAM();
  70. Computer computer_02 =builder_02.build();
  71. System.out.println(computer_01);
  72. System.out.println(computer_02);
  73. }
  74. }

创造了建造者接口,创建者不会再遗漏步骤。
UML类图如下:
设计模式学习笔记 - 图15
缺陷:

  • 每一个builder都要自己去调用setXXX方法进行创建,造成代码重复。
  • 需要客户端自己执行创建步骤,建造复杂对象的时候,容易造成客户端代码臃肿,且违反迪米特法则。而且客户端会出现遗漏步骤的情况。又回到了原点的感觉。

    1. //正例,建造者模式
    2. public class postive {
    3. /*=============服务端==========================*/
    4. interface ComputerBuilder{
    5. Computer getComputer();
    6. void setCpu();
    7. void setGpu();
    8. void setHd();
    9. void setRAM();
    10. }
    11. static class HighComputerBuilder implements ComputerBuilder {
    12. private Computer computer = new Computer();
    13. @Override
    14. public Computer getComputer() {
    15. return computer;
    16. }
    17. @Override
    18. public void setCpu() {
    19. computer.setCpu("R5-3600");
    20. }
    21. @Override
    22. public void setGpu() {
    23. computer.setGpu("RTX3060Ti");
    24. }
    25. @Override
    26. public void setHd() {
    27. computer.setHd("SSD1TB");
    28. }
    29. @Override
    30. public void setRAM() {
    31. computer.setRAM("32GB");
    32. }
    33. }
    34. static class High_02ComputerBuilder implements ComputerBuilder {
    35. private Computer computer = new Computer();
    36. @Override
    37. public Computer getComputer() {
    38. return computer;
    39. }
    40. @Override
    41. public void setCpu() {
    42. computer.setCpu("i5-10400");
    43. }
    44. @Override
    45. public void setGpu() {
    46. computer.setGpu("RTX3060");
    47. }
    48. @Override
    49. public void setHd() {
    50. computer.setHd("SSD500GB");
    51. }
    52. @Override
    53. public void setRAM() {
    54. computer.setRAM("16GB");
    55. }
    56. }
    57. //指挥者
    58. static class Director {
    59. public Computer build(ComputerBuilder builder){
    60. builder.setCpu();
    61. builder.setGpu();
    62. builder.setRAM();
    63. builder.setHd();
    64. return builder.getComputer();
    65. }
    66. }
    67. /*==============客户端=====================================*/
    68. public static void main(String[] args) {
    69. Director director = new Director();
    70. Computer computer_01 =director.build(new HighComputerBuilder());
    71. Computer computer_02 =director.build(new High_02ComputerBuilder());
    72. System.out.println(computer_01);
    73. System.out.println(computer_02);
    74. }
    75. }

    UML类图如下:
    image.png
    此时在需要增加一个不同配置的A_Computer类型计算机,只需要编写一个A_Builder类实现ComputerBuilder接口,再传给指挥者Director进行创建即可得到一个A_Computer类型的Computer对象。符合开闭原则
    总结一下建造者模式的优点:

  • 创建对象的过程保持稳定。(通过ComputerBuilder接口保持稳定)

  • 创建过程只需要编写一次(通过实现ComputerBuilder接口的类保持稳定)
  • 保持扩展性,需要创建新类型的对象时,只需要创建新的Builder,再使用指挥者Director进行调用进行创建即可。
  • 增加指挥者Director保证步骤稳定执行,客户端不需要知道创建对象的具体步骤,符合迪米特法则

    建造者模式和工厂模式的区别

  • 工厂模式注重new一个对象就可以,是否得到了这一对象,更多地关注new的结果。

  • 建造者模式注重保证new的对象稳定可用,保证不出现细节缺漏,更多关注new的细节、过程。

    结构型

    一、装饰器

    解释:在不改变现有对象结构的情况下,为现有对象添加新功能。 ```java / 星巴克是卖咖啡的, 一开始饮料种类比较少,所以使用继承,看不出什么问题。 每种咖啡,都有描述,都有价格。 这是所有咖啡的共性,既然是共性,就要上提到父类Beverage中。 /

abstract class Beverage {

  1. private String description;
  2. public Beverage(String description) {
  3. super();
  4. this.description = description;
  5. }
  6. public abstract double cost();

}

class DarkRoast extends Beverage {

  1. public DarkRoast(String descriptoin) {
  2. super(descriptoin);
  3. }
  4. public double cost() {
  5. return 10;
  6. }

}

class Decaf extends Beverage {

  1. public Decaf(String description) {
  2. super(description);
  3. }
  4. @Override
  5. public double cost() {
  6. return 8;
  7. }

}

class Espresso extends Beverage {

  1. public Espresso(String description) {
  2. super(description);
  3. }
  4. @Override
  5. public double cost() {
  6. return 12;
  7. }

}

class HouseBlend extends Beverage {

  1. public HouseBlend(String description) {
  2. super(description);
  3. }
  4. @Override
  5. public double cost() {
  6. return 15;
  7. }

}

public class Test {

}

  1. 星巴克,业务壮大了,为了把同业抛开,它加了新的花样: 4种调料:<br />蒸奶(steamed Milk)、豆浆(Soy)、摩卡(Mocha)、打起奶泡(Whip)<br />针对于DrakRoast咖啡,可以有以下几种变化:<br />DrakRoast<br />DrakRoastWithSteamedMilk <br />DrakRoastWithSoy<br />DrakRoastWithMocha<br />DrakRoastWithWhip<br />DrakRoastWithSteamedMilkAndSoy<br />.....<br />类,爆炸了!! 程序员,疯了!!
  2. ![uml.jpg](https://cdn.nlark.com/yuque/0/2021/jpeg/21546143/1621693156499-964e82fe-ff98-4c51-b7fe-3e218d36ac34.jpeg#clientId=u1208f76b-9237-4&from=drop&height=388&id=u063f884b&margin=%5Bobject%20Object%5D&name=uml.jpg&originHeight=586&originWidth=675&originalType=binary&size=62095&status=done&style=none&taskId=u95af65c3-e64c-4a68-9a2c-8bde483f9f2&width=447)
  3. ```java
  4. //C package
  5. // 为了解决b包的问题,尝试这样来解决:
  6. // 优点,没有类爆炸!
  7. class Beverage {
  8. private String description;
  9. private boolean steamedMilk;
  10. private boolean soy;
  11. private boolean mocha;
  12. private boolean whip;
  13. public Beverage(String description) {
  14. super();
  15. this.description = description;
  16. }
  17. public double cost() {
  18. //咖啡总价格的计算
  19. double cost = 0;
  20. if(steamedMilk) {
  21. cost += 0.2;
  22. }
  23. if(soy) {
  24. cost += 0.2;
  25. }
  26. if(mocha) {
  27. cost += 0.3;
  28. }
  29. if(whip) {
  30. cost += 0.1;
  31. }
  32. return cost;
  33. }
  34. public boolean isSteamedMilk() {
  35. return steamedMilk;
  36. }
  37. public void setSteamedMilk(boolean steamedMilk) {
  38. this.steamedMilk = steamedMilk;
  39. }
  40. public boolean isSoy() {
  41. return soy;
  42. }
  43. public void setSoy(boolean soy) {
  44. this.soy = soy;
  45. }
  46. public boolean isMocha() {
  47. return mocha;
  48. }
  49. public void setMocha(boolean mocha) {
  50. this.mocha = mocha;
  51. }
  52. public boolean isWhip() {
  53. return whip;
  54. }
  55. public void setWhip(boolean whip) {
  56. this.whip = whip;
  57. }
  58. }
  59. class DarkRoast extends Beverage {
  60. public DarkRoast(String descriptoin) {
  61. super(descriptoin);
  62. }
  63. public double cost() {
  64. return super.cost() + 10;
  65. }
  66. }
  67. class Decaf extends Beverage {
  68. public Decaf(String description) {
  69. super(description);
  70. }
  71. @Override
  72. public double cost() {
  73. return super.cost() + 8;
  74. }
  75. }
  76. class Espresso extends Beverage {
  77. public Espresso(String description) {
  78. super(description);
  79. }
  80. @Override
  81. public double cost() {
  82. return super.cost() + 12;
  83. }
  84. }
  85. class HouseBlend extends Beverage {
  86. public HouseBlend(String description) {
  87. super(description);
  88. }
  89. @Override
  90. public double cost() {
  91. return super.cost() + 15;
  92. }
  93. }
  94. // =========================================================
  95. class Tea extends Beverage {
  96. public Tea(String description) {
  97. super(description);
  98. }
  99. public double cost() {
  100. return super.cost() + 9;
  101. }
  102. }
  103. public class Test {
  104. public static void main(String[] args) {
  105. DarkRoast dr = new DarkRoast("焦炒咖啡");
  106. dr.setMocha(true);
  107. dr.setSoy(true);
  108. System.out.println(dr.cost());
  109. HouseBlend hb = new HouseBlend("混合咖啡");
  110. hb.setWhip(true);
  111. System.out.println(hb.cost());
  112. Tea tea = new Tea("凉茶");
  113. tea.setSoy(true);
  114. System.out.println(tea.cost());
  115. }
  116. }
  117. /*优点:
  118. 1.没有类爆炸
  119. 2.加入新的饮料,茶,不会带来什么影响,符合开闭原则
  120. 缺点:
  121. 1.现在多了一个调料:枸杞。此时必要改写父类的Cost方法,破坏了开闭原则。
  122. */
  1. // 为了解决c包的问题,
  2. abstract class Beverage {
  3. private String description;
  4. public Beverage(String description) {
  5. super();
  6. this.description = description;
  7. }
  8. public abstract double cost();
  9. public String getDescription() {
  10. return description;
  11. }
  12. }
  13. class DarkRoast extends Beverage {
  14. public DarkRoast(String descriptoin) {
  15. super(descriptoin);
  16. }
  17. public double cost() {
  18. return 10;
  19. }
  20. }
  21. class Decaf extends Beverage {
  22. public Decaf(String description) {
  23. super(description);
  24. }
  25. @Override
  26. public double cost() {
  27. return 8;
  28. }
  29. }
  30. class Espresso extends Beverage {
  31. public Espresso(String description) {
  32. super(description);
  33. }
  34. @Override
  35. public double cost() {
  36. return 12;
  37. }
  38. }
  39. class HouseBlend extends Beverage {
  40. public HouseBlend(String description) {
  41. super(description);
  42. }
  43. @Override
  44. public double cost() {
  45. return 15;
  46. }
  47. }
  48. abstract class CondimentDecorator extends Beverage {
  49. public CondimentDecorator() {
  50. //默认调用无参构造器,但父类没有,所以必须加参数
  51. super("调料");
  52. }
  53. public abstract double cost();
  54. }
  55. class Milk extends CondimentDecorator {
  56. private Beverage beverage;
  57. public Milk(Beverage beverage) {
  58. this.beverage = beverage;
  59. }
  60. @Override
  61. public double cost() {
  62. return beverage.cost() + 0.2;
  63. }
  64. @Override
  65. public String getDescription() {
  66. return beverage.getDescription() + " 牛奶";
  67. }
  68. }
  69. class Mocha extends CondimentDecorator{
  70. private Beverage beverage;
  71. public Mocha(Beverage beverage) {
  72. this.beverage = beverage;
  73. }
  74. @Override
  75. public double cost() {
  76. return beverage.cost() + 0.3;
  77. }
  78. @Override
  79. public String getDescription() {
  80. return beverage.getDescription() + " 摩卡";
  81. }
  82. }
  83. class Soy extends CondimentDecorator {
  84. private Beverage beverage;
  85. public Soy(Beverage beverage) {
  86. this.beverage = beverage;
  87. }
  88. @Override
  89. public double cost() {
  90. return beverage.cost() + 0.2;
  91. }
  92. @Override
  93. public String getDescription() {
  94. return beverage.getDescription() + " 豆浆";
  95. }
  96. }
  97. class Whip extends CondimentDecorator {
  98. private Beverage beverage;
  99. public Whip(Beverage beverage) {
  100. this.beverage = beverage;
  101. }
  102. @Override
  103. public double cost() {
  104. return beverage.cost() + 0.1;
  105. }
  106. @Override
  107. public String getDescription() {
  108. return beverage.getDescription() + " 奶泡";
  109. }
  110. }
  111. // =========================================================
  112. class GouQi extends CondimentDecorator {
  113. private Beverage beverage;
  114. public GouQi(Beverage beverage) {
  115. super();
  116. this.beverage = beverage;
  117. }
  118. @Override
  119. public String getDescription() {
  120. return beverage.getDescription() + " 枸杞";
  121. }
  122. @Override
  123. public double cost() {
  124. return beverage.cost() + 0.4;
  125. }
  126. }
  127. public class Test {
  128. public static void main(String[] args) throws IOException {
  129. Beverage b = new DarkRoast("焦炒咖啡");
  130. Beverage b2 = new Milk(b);
  131. Beverage b3 = new Soy(b2);
  132. Beverage b4 = new Whip(b3);
  133. Beverage b5 = new Milk(b4);
  134. Beverage b6 = new GouQi(b5);
  135. System.out.println(b6.getDescription() + ":" + b6.cost());
  136. }
  137. }
  138. /*
  139. 优点:
  140. 1.加一个新的饮料,不会违反开闭原则。加一个新的调料,也不会违反开闭原则
  141. 缺点:
  142. 1.类还是有些多,但是目前最优的方案
  143. */

装饰器设计模式图解.png

Java中,java.io就是用了装饰器模式进行API设计:

  1. import java.io.FileReader;
  2. import java.io.FilterReader;
  3. import java.io.IOException;
  4. import java.io.Reader;
  5. // 根据已有功能,做出更强大的功能!
  6. class MyBufferedReader extends FilterReader {
  7. //要先继承再关联
  8. private Reader in;
  9. protected MyBufferedReader(Reader in) {
  10. super(in);
  11. this.in = in;
  12. }
  13. public String readLine() throws IOException {
  14. StringBuilder sb = new StringBuilder("");
  15. int n = 0;
  16. while((n = in.read()) != -1) {
  17. if(n == '\r' ) {
  18. continue;
  19. }
  20. if(n == '\n' ) {
  21. break;
  22. }
  23. sb.append((char)n);
  24. }
  25. // 流程能走到这里,n有几种可能? 一个是-1, 是\n
  26. return n == -1 ? null : sb.toString();
  27. }
  28. @Override
  29. public void close() throws IOException {
  30. in.close();
  31. }
  32. }
  33. public class Test {
  34. public static void main(String[] args) throws Exception {
  35. Reader in = new FileReader("D:\\1.txt");
  36. MyBufferedReader mbr = new MyBufferedReader(in);
  37. String line = null;
  38. while((line = mbr.readLine()) != null) {
  39. System.out.println(line);
  40. }
  41. mbr.close();
  42. }
  43. }

io uml图.jpg

二、适配器

解释:适配器模式(Adapter),将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
现在中国球员去NBA发展,可是初来乍到,会有语言不同的情况,现有设计类如下:

  1. public interface Foreign_Player {
  2. void Speak_Foreign_Language();
  3. String getName();
  4. }
  5. public class CHN_player implements Foreign_Player {
  6. private String name;
  7. public CHN_player(String name) {
  8. this.name =name;
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. @Override
  14. public void Speak_Foreign_Language() {
  15. System.out.println("用中文说战术。。。。");
  16. }
  17. }
  1. public interface Native_Player {
  2. void speak_English();
  3. String getName();
  4. }
  5. public class NBA_player implements Native_Player {
  6. private String name;
  7. public NBA_player(String name) {
  8. this.name =name;
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. @Override
  14. public void speak_English() {
  15. System.out.println(name+"Talk about Tactics in English。。。。");
  16. }
  17. }

现在需要让所有球员用英文传达战术,中国球员现学肯定来不及了(不能现改中国球员Foreign_Player接口违反开闭原则),那就需要一个翻译人员,适配器模式就派上用场了。

  1. public class Adapter_Translator implements Native_Player {
  2. private Foreign_Player foreign_player;
  3. public Adapter_Translator(Foreign_Player foreign_player) {
  4. this.foreign_player = foreign_player;
  5. }
  6. public String getName() {
  7. return foreign_player.getName()+" 的翻译人员";
  8. }
  9. @Override
  10. public void speak_English() {
  11. System.out.println("翻译人员翻译中文战术成英语给队友-------");
  12. }
  13. }
  1. /*=================客户端====================*/
  2. public class postive {
  3. public static void main(String[] args) {
  4. CHN_player yaoming = new CHN_player("yaoming");
  5. System.out.println(yaoming.getName());
  6. yaoming.Speak_Foreign_Language();
  7. //进行适配
  8. Native_Player translator = new Adapter_Translator(yaoming);
  9. System.out.println(translator.getName());
  10. translator.speak_English();
  11. }
  12. }

UML类图如下:
设计模式学习笔记 - 图19
因为有了翻译人员,中国球员不会因为语言不通的原因导致球员之间无法沟通的问题,这时候,中国球员就好像一个本土球员一样在赛场上打球。
也就是两个实现不同接口的、不存在联系的类,在不修改源代码的前提下,通过适配器,可以实现互通互用,遵守开闭原则。
简单总结一下适配器的编写套路:

  • 继承需要适配成的抽象类,或者实现需要适配的接口。(例子中需要适配成Native_Player,适配器Adapter_Translator就实现Native_Player)
  • 组合原来不适配的抽象类或者接口。(例子中,适配器Adapter_Translator组合了Foreign_Player接口)
    • 这里是体现了七大设计原则中的组合优于继承。

      行为型

      一、策略

      解释:一个类的行为或其算法可以在运行时更改。 ```java / a. Joe上班的公司做了一套相当成功的模拟鸭子游戏:SimUDuck。游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫。 此系统的内部设计使用了标准的OO技术,设计了一个鸭子超类,并让各种鸭子继承此超类 /

abstract class Duck { public void quack() { System.out.println(“呱呱叫”); } public void swim() { System.out.println(“游泳”); } // 因为每一种鸭子的外观都不同,所以display()方法是抽象的 public abstract void display(); }

// 野鸭 class MallardDuck extends Duck { @Override public void display() { System.out.println(“外观是绿头”); } } class RedHeadDuck extends Duck { @Override public void display() { System.out.println(“外观是红头”); } }

public class Test { public static void main(String[] args) { Duck duck = new MallardDuck(); duck.quack(); duck.swim(); duck.display();

  1. System.out.println("=========================================");
  2. duck = new RedHeadDuck();
  3. duck.quack();
  4. duck.swim();
  5. duck.display();
  6. }

}

  1. ![uml.png](https://cdn.nlark.com/yuque/0/2021/png/21546143/1621742065295-fe6ce002-35b7-41aa-b9db-48d260819cd0.png#clientId=u97db5b09-0d73-4&from=drop&height=291&id=u2209620e&margin=%5Bobject%20Object%5D&name=uml.png&originHeight=473&originWidth=648&originalType=binary&size=75949&status=done&style=none&taskId=u33601d14-be10-40c6-8a76-ae22f11e341&width=398)
  2. ```java
  3. /*b.
  4. 现在有个需求:
  5. 主管们决定,此模拟程序需要会飞的鸭子来将竞争者抛在后头。当然,在这个时候,Joe的经理拍胸脯告诉主管们,
  6. Joe只需要一个星期就可以搞定。“毕竟,Joe是一个OO程序员......这有什么困难??”
  7. Joe认为,只需要在Duck类中加上fly()方法,然后所有鸭子都会继承fly()。 “这是我大显身手,展示OO才华的时候了!”
  8. */
  9. abstract class Duck {
  10. public void quack() {
  11. System.out.println("呱呱叫");
  12. }
  13. public void swim() {
  14. System.out.println("游泳");
  15. }
  16. public void fly() {
  17. System.out.println("真的飞起来了!");
  18. }
  19. // 因为每一种鸭子的外观都不同,所以display()方法是抽象的
  20. public abstract void display();
  21. }
  22. // 问题似乎解决了, 但真的是这样吗?
  23. // Joe,没有想到,在众多的Duck子类中,有了一个RubberDuck(橡皮鸭)
  24. class RubberDuck extends Duck {
  25. // 橡皮鸭子不会呱呱叫,所以把quack()的定义覆盖成“吱吱叫”
  26. @Override
  27. public void quack() {
  28. System.out.println("吱吱叫");
  29. }
  30. @Override
  31. public void display() {
  32. System.out.println("外观是橡皮鸭");
  33. }
  34. }
  35. // Joe忽略了一件事,并非Duck的所有子类都会飞。Joe在Duck超类中加上新的行为,
  36. // 会使得某些并不适合该行为的子类也具有该行为。现在可好了!SimUDuck程序中有了
  37. // 一个无生命的会飞的东西。
  38. public class Test {
  39. public static void main(String[] args) {
  40. duck = new RubberDuck();
  41. duck.quack();
  42. duck.swim();
  43. duck.fly(); // 橡皮鸭飞起来了?
  44. duck.display();
  45. }
  46. }
  47. // 我们发现一件事: 一旦设计“维护”时,为了“复用”目的而使用继承,结局并不完美。

uml.png

  1. /*
  2. c.
  3. 针对于b包中的问题,Joe的解决之道是这样的:
  4. 我可以把橡皮鸭类中的fly()方法覆盖掉,就好像覆盖quack()的做法一样
  5. */
  6. class RubberDuck extends Duck {
  7. // 橡皮鸭子不会呱呱叫,所以把quack()的定义覆盖成“吱吱叫”
  8. @Override
  9. public void quack() {
  10. System.out.println("吱吱叫");
  11. }
  12. @Override
  13. public void display() {
  14. System.out.println("外观是橡皮鸭");
  15. }
  16. // 橡皮鸭子不会飞,所以把fly()覆盖为什么事都不做,或者抛出一个UnsupportedOperationException
  17. @Override
  18. public void fly() {
  19. throw new UnsupportedOperationException("you can you up, no can no bb!");
  20. }
  21. }
  22. //这样做,橡皮鸭就不会让人意外地飞起来了,如果客户端非要让一个橡皮鸭飞,只会收到一个运行时异常!
  23. public class Test {
  24. public static void main(String[] args) {
  25. duck = new RubberDuck();
  26. duck.quack();
  27. duck.swim();
  28. duck.fly(); // 就不要赶着鸭子上架子了!
  29. duck.display();
  30. }
  31. }
  32. /*
  33. 可是,如果以后加入木鸭(WoodenDuck),又会如何??
  34. 木头假鸭,不会飞也不会叫......
  35. */
  36. class WoodenDuck extends Duck {
  37. @Override
  38. public void quack() {
  39. // 覆盖,变成什么事都不做
  40. }
  41. @Override
  42. public void display() {
  43. System.out.println("外观是木鸭");
  44. }
  45. @Override
  46. public void fly() {
  47. // 覆盖,变成什么事都不做
  48. }
  49. }
  50. // 利用继承来提供Duck的行为,会导致:
  51. // 1. 很难知道所有鸭子的全部行为。
  52. // 2. 改变会牵一发而动全身,造成其他鸭子不想要的改变。
  53. // 更糟糕的是,每当有新鸭子子类出现,Joe就要被迫检查并可能覆盖fly()或quack()方法...
  54. // 这简直是无穷无尽的噩梦!
  1. //
  2. d.
  3. 针对c包中的问题,我们需要一个更清晰的方法,让“某些”(而不是全部)鸭子类型可飞或可叫。
  4. /*
  5. 通过前面的学习,我们知道:把主要功能定义在父类中,把扩展功能定义在接口中。
  6. 从目前这个状况来看,“飞”和“呱呱叫”成为了鸭子的扩展功能,因为并不是所有的鸭子都会“飞”和“呱呱叫”
  7. 可以把fly()从超类中取出来,放进一个“Flyable接口”中。这么一来,只有会飞的鸭子才实现此接口。
  8. 同样的方式,也可以用来设计一个“Quackable”接口,因为不是所有的鸭子都会叫。
  9. */
  10. abstract class Duck {
  11. public void swim() {
  12. System.out.println("游泳");
  13. }
  14. public abstract void display();
  15. }
  16. interface Flyable {
  17. public void fly();
  18. }
  19. interface Quackable {
  20. public void quack();
  21. }
  22. class MallardDuck extends Duck implements Flyable, Quackable {
  23. @Override
  24. public void quack() {
  25. System.out.println("嘎嘎叫");
  26. }
  27. @Override
  28. public void fly() {
  29. System.out.println("飞起来了");
  30. }
  31. @Override
  32. public void display() {
  33. System.out.println("外观是绿头鸭");
  34. }
  35. }
  36. class RedheadDuck extends Duck implements Flyable, Quackable {
  37. @Override
  38. public void quack() {
  39. System.out.println("嘎嘎叫");
  40. }
  41. @Override
  42. public void fly() {
  43. System.out.println("飞起来了");
  44. }
  45. @Override
  46. public void display() {
  47. System.out.println("外观是红头鸭");
  48. }
  49. }
  50. class RubberDuck extends Duck implements Quackable {
  51. @Override
  52. public void quack() {
  53. System.out.println("嘎嘎叫");
  54. }
  55. @Override
  56. public void display() {
  57. System.out.println("外观是橡皮鸭");
  58. }
  59. }
  60. class WoodenDuck extends Duck {
  61. @Override
  62. public void display() {
  63. System.out.println("外观是木头假鸭");
  64. }
  65. }
  66. public class Test {
  67. public static void main(String[] args) {
  68. }
  69. }
  70. // 我们知道,并非“所有”的子类都具有飞行和呱呱叫的行为,所以继承并不是适当的解决方式。
  71. // 虽然Flyable与Quackable可以解决“一部分”问题(不会再有会飞的橡皮鸭),但是
  72. // 却造成代码无法复用(fly和quack),这只能算是从一个恶梦跳进另一个恶梦。
  73. // 甚至,在会飞的鸭子中,飞行的动作可能还有多种变化.....
  74. // 也许你会说,自jdk1.8开始,接口有默认实现。
  75. // 那么对于48种鸭子的子类,共有12种飞行的方式,又该怎么说? 接口的默认实现选择哪一种飞行方式都不行!