《设计模式详解》

  • 3、软件设计原则
    • 3.1 开闭原则
      • 示例
    • 3.2 里式代换原则
      • 反例
      • 改进反例
    • 3.3 依赖倒转原则
      • 反例
      • 改进反例
    • 3.4 接口隔离原则
      • 反例
      • 改进反例
    • 3.5 迪米特法则
      • 示例
    • 3.6 合成复用原则
      • 继承复用示例
      • 合成复用示例

完整的笔记目录:《设计模式详解》笔记目录,欢迎指点!

3、软件设计原则

在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据 6 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

  • 开闭原则:对拓展开放,对修改封闭。
  • 里式代换原则:任何基类可以出现的地方,子类一定可以出现,反之不一定。
  • 依赖倒转原则:高层模块不应该依赖低层模块,两者都应该依赖其抽象。
  • 接口隔离原则:客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。
  • 迪米特法则:只和你的直接朋友交谈,不跟 “陌生人” 说话(Talk only to your immediate friends and not to strangers)。
  • 合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

3.1 开闭原则

开闭原则对扩展开放,对修改关闭

在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。这是为了使程序的扩展性好,易于维护和升级。

想要达到这样的效果,我们需要使用接口和抽象类。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

示例

【例】搜狗输入法 的皮肤设计。

分析:搜狗输入法 的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,可以为其定义一个抽象类(AbstractSkin),而每个具体的皮肤(DefaultSpecificSkin 和 HeimaSpecificSkin)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。

注,这张图有点小问题,SouGouInput 对于 AbstractSkin 应该是依赖关系,图中画成了实现关系

代码实现:

/*** 抽象皮肤类*/
public abstract class AbstractSkin {public abstract  void display();
}
/*** 默认皮肤类*/
public class DefaultSkin extends AbstractSkin {@Overridepublic void display() {System.out.println("默认皮肤");}
}
/*** 黑马皮肤类*/
public class HeimaSkin extends AbstractSkin {@Overridepublic void display() {System.out.println("黑马皮肤");}
}
/*** @Description: 搜狗输入法*/
@Data
public class SougouInput {private AbstractSkin skin;public void display() {skin.display();}
}
/*** 测试类*/
public class Client {public static void main(String[] args) {// 1,创建搜狗输入法对象SougouInput input = new SougouInput();// 2,创建皮肤对象// DefaultSkin skin = new DefaultSkin();// 修改皮肤不需要修改其他代码,只需要新增一个皮肤类,设置皮肤为该类即可,符合开闭原则HeimaSkin skin = new HeimaSkin();// 3,将皮肤设置到输入法中input.setSkin(skin);// 4,显示皮肤input.display();}
}

3.2 里式代换原则

里氏代换原则是面向对象设计的基本原则之一。

里氏代换原则任何基类可以出现的地方,子类一定可以出现,反之不一定

子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

如果必须重写,更适合在父类中定义成抽象方法

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

反例

【例】正方形不是长方形。

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。

长方形类 Rectangle:

/*** @Description: 长方形类*/
@Data
public class Rectangle {private double length;private double width;
}

正方形类 Square:由于正方形的长和宽相同,所以在方法 setLength 和 setWidth 中,对长度和宽度赋相同值。

public class Square extends Rectangle {public void setWidth(double width) {super.setLength(width);super.setWidth(width);}public void setLength(double length) {super.setLength(length);super.setWidth(length);}
}

RectangleDemo 类是我们的软件系统中的一个组件,它有一个 resize 方法依赖基类 Rectangle,resize 方法是 RectandleDemo 类中的一个方法,用来实现宽度逐渐增长的效果。

public class RectangleDemo {public static void main(String[] args) {// 创建长方形对象Rectangle r = new Rectangle();r.setLength(20);r.setWidth(10);resize(r);printLengthAndWidth(r);System.out.println("=====================");// 创建正方形对象Square s = new Square();s.setLength(10);resize(s);printLengthAndWidth(s);}// 拓宽方法public static void resize(Rectangle rectangle) {while (rectangle.getWidth() <= rectangle.getLength()) {rectangle.setWidth(rectangle.getWidth() + 1);}}// 打印长方形的长和宽public static void printLengthAndWidth(Rectangle rectangle) {System.out.println(rectangle.getLength());System.out.println(rectangle.getWidth());}}

运行这段代码会发现:

  • 假如我们把一个普通长方形作为参数传入 resize 方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期

  • 假如我们再把一个正方形作为参数传入 resize 方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。

    所以,普通的长方形是适合这段代码的,正方形不适合。

  • 我们得出结论:在 resize 方法中,Rectangle 类型的参数不能被 Square 类型的参数所代替,如果进行了替换就得不到预期结果。

    因此,Square 类和 Rectangle 类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

改进反例

如何改进呢?此时我们需要重新设计他们之间的关系。

抽象出来一个四边形接口(Quadrilateral),让 Rectangle 类和 Square 类实现 Quadrilateral 接口

注意,上图画的有问题,RectangleDemo 对于 Quadrilateral 和 Rectangle 是依赖关系,应当是虚线

四边形接口 Quadrilateral:

public interface Quadrilateral {double getLength();double getWidth();
}

长方形类 Rectangle:

@Setter
public class Rectangle implements Quadrilateral {private double length;private double width;@Overridepublic double getLength() {return length;}@Overridepublic double getWidth() {return width;}
}

正方形类 Square:

@Data
public class Square implements Quadrilateral {private double side;@Overridepublic double getLength() {return side;}@Overridepublic double getWidth() {return side;}
}

测试类:

public class RectangleDemo {public static void main(String[] args) {// 创建长方形对象Rectangle r = new Rectangle();r.setLength(20);r.setWidth(10);// 调用方法进行扩宽操作resize(r);printLengthAndWidth(r);}// 扩宽的方法public static void resize(Rectangle rectangle) {// 判断宽如果比长小,进行扩宽的操作while(rectangle.getWidth() <= rectangle.getLength()) {rectangle.setWidth(rectangle.getWidth() + 1);}}// 打印长和宽public static void printLengthAndWidth(Quadrilateral quadrilateral) {System.out.println(quadrilateral.getLength());System.out.println(quadrilateral.getWidth());}
}

3.3 依赖倒转原则

依赖倒转原则高层模块不应该依赖低层模块,两者都应该依赖其抽象

抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

反例

【例】组装电脑

现要组装一台电脑,需要配件 cpu、硬盘、内存条,只有这些配置都有了,计算机才能正常的运行。

cpu 有很多选择,如 Intel、AMD 等,硬盘可以选择希捷、西数等,内存条可以选择金士顿、海盗船等。

代码如下:

希捷硬盘类(XiJieHardDisk):

public class XiJieHardDisk implements HardDisk {public void save(String data) {System.out.println("使用希捷硬盘存储数据" + data);}public String get() {System.out.println("使用希捷希捷硬盘取数据");return "数据";}
}

Intel 处理器(IntelCpu):

public class IntelCpu implements Cpu {public void run() {System.out.println("使用Intel处理器");}
}

金士顿内存条(KingstonMemory)

public class KingstonMemory implements Memory {public void save() {System.out.println("使用金士顿作为内存条");}
}

电脑(Computer):

@Data
public class Computer {private XiJieHardDisk hardDisk;private IntelCpu cpu;private KingstonMemory memory;public void run() {System.out.println("计算机工作");cpu.run();memory.save();String data = hardDisk.get();System.out.println("从硬盘中获取的数据为:" + data);}
}

**测试类(TestComputer):**测试类用来组装电脑。

public class TestComputer {public static void main(String[] args) {Computer computer = new Computer();computer.setHardDisk(new XiJieHardDisk());computer.setCpu(new IntelCpu());computer.setMemory(new KingstonMemory());computer.run();}
}

上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的 cpu 只能是 Intel 的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。

以上代码不满足依赖倒转原则,因为 Computer 类依赖了各个组件的具体实现。

改进反例

根据依赖倒转原则进行改进,需要让 Computer 类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。

将硬盘、Cpu、内存抽取出接口:

/*** 硬盘接口*/
public interface HardDisk {public void save(String data);public String get();
}/*** Cpu接口*/
public interface Cpu {public void run();
}/*** 内存条接口*/
public interface Memory {public void save();
}

具体型号的配件实现其接口:

/*** 希捷硬盘*/
public class XiJieHardDisk implements HardDisk {public void save(String data) {System.out.println("使用希捷硬盘存储数据为:" + data);}public String get() {System.out.println("使用希捷希捷硬盘取数据");return "数据";}
}/*** Intel cpu*/
public class IntelCpu implements Cpu {public void run() {System.out.println("使用Intel处理器");}
}/*** 金士顿内存条*/
public class KingstonMemory implements Memory {public void save() {System.out.println("使用金士顿内存条");}
}

组装电脑的时候,利用接口来实现:

/*** Computer*/
@Data
public class Computer {private HardDisk hardDisk;private Cpu cpu;private Memory memory;//运行计算机public void run() {System.out.println("运行计算机");String data = hardDisk.get();System.out.println("从硬盘上获取的数据是:" + data);cpu.run();memory.save();}
}

在使用的时候,如果需要组装不同的组件,就不需要去修改 Computer 类,只需要创建新的组件对象并赋给计算机对象

public class ComputerDemo {public static void main(String[] args) {// 创建计算机的组件对象HardDisk hardDisk = new XiJieHardDisk();Cpu cpu = new IntelCpu();Memory memory = new KingstonMemory();// 创建计算机对象Computer c = new Computer();// 组装计算机c.setCpu(cpu);c.setHardDisk(hardDisk);c.setMemory(memory);// 运行计算机c.run();}
}

面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。

以上代码符合依赖倒转原则,大大降低了程序与实现细节的耦合度。


多数情况下,以上三个设计原则会同时出现:

开闭原则是目标,里式代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致。

3.4 接口隔离原则

接口隔离原则客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。

反例

【例】安全门案例

我们需要创建一个黑马品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:

代码如下:

/*** 安全门接口*/
public interface SafetyDoor {// 防盗void antiTheft();// 防火void fireProof();// 防水void waterProof();
}
/*** 黑马品牌的安全门*/
public class HeimaSafetyDoor implements SafetyDoor {public void antiTheft() {System.out.println("防盗");}public void fireProof() {System.out.println("防火");}public void waterProof() {System.out.println("防水");}
}
public class Client {public static void main(String[] args) {HeimaSafetyDoor door = new HeimaSafetyDoor();door.antiTheft();door.fireProof();door.waterProof();}
}

上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。

现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现 SafetyDoor 接口就违背了接口隔离原则

改进反例

将各个功能抽离成单一接口:

/*** 防盗接口*/
public interface AntiTheft {void antiTheft();
}/*** 防火接口*/
public interface Fireproof {void fireproof();
}/*** 防水接口*/
public interface Waterproof {void waterproof();
}

当前有个黑马防盗门,它可以实现防盗、防火、防水功能:

/*** HeiMaSafetyDoor*/
public class HeiMaSafetyDoor implements AntiTheft, Fireproof, Waterproof {public void antiTheft() {System.out.println("防盗");}public void fireproof() {System.out.println("防火");}public void waterproof() {System.out.println("防水");}
}

如果此时还需要新增一个传智防盗门,它只有防盗、防火功能:

/*** 传智安全门*/
public class ItcastSafetyDoor implements AntiTheft, Fireproof {public void antiTheft() {System.out.println("防盗");}public void fireproof() {System.out.println("防火");}
}

测试类:

public class Client {public static void main(String[] args) {// 创建黑马安全门对象HeimaSafetyDoor door = new HeimaSafetyDoor();// 调用功能door.antiTheft();door.fireProof();door.waterProof();// 创建传智安全门对象ItcastSafetyDoor door1 = new ItcastSafetyDoor();//调用功能door1.antiTheft();door1.fireproof();}
}

以上代码实现了接口隔离原则,没有强迫客户端去依赖它不使用的方法。

3.5 迪米特法则

迪米特法则又叫最少知识原则。

迪米特法则只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。

其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。

其目的是:降低类之间的耦合度,提高模块的相对独立性。

迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

示例

【例】明星与经纪人的关系实例

明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。

这里经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。

/** * 明星类 */
@Getter
public class Star {    private String name;    public Star(String name) {        this.name = name;    }
}
/*** 粉丝类*/
@Getter
public class Fans {private String name;public Fans(String name) {this.name = name;}
}
/*** 媒体公司类*/
@Getter
public class Company {private String name;public Company(String name) {this.name = name;}
}
/*** 经纪人类*/
@Getter
public class Agent {private Star star;private Fans fans;private Company company;// 和粉丝见面的方法public void meeting() {System.out.println(star.getName() + "和粉丝" + fans.getName() + "见面");}// 和媒体公司洽谈的方法public void business() {System.out.println(star.getName() + "和" + company.getName() + "洽谈");}
}
public class Client {public static void main(String[] args) {// 创建经纪人类Agent agent = new Agent();// 创建明星对象Star star = new Star("林青霞");agent.setStar(star);// 创建粉丝对象Fans fans = new Fans("李四");agent.setFans(fans);// 创建媒体公司对象Company company = new Company("黑马媒体公司");agent.setCompany(company);// 经纪人组织: 和粉丝见面agent.meeting();// 经纪人组织: 和媒体公司洽谈业务agent.business();}
}

3.6 合成复用原则

合成复用原则尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现

通常类的复用分为:继承复用和合成复用两种。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

继承复用示例

【例】汽车分类管理程序

汽车按 “动力源” 划分可分为汽油汽车、电动汽车等;按 “颜色” 划分可分为白色汽车、黑色汽车和红色汽车等。

如果同时考虑这两种分类,其组合就很多。类图如下:

从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。

合成复用示例

我们试着将继承复用改为聚合复用,类图如下:

《设计模式详解》软件设计原则相关推荐

  1. 设计模式-03(软件设计原则)

    软件设计原则                                                                                               ...

  2. 设计模式之七大软件设计原则

    一.开闭原则 定义:一个软件实体如类.模块和函数应该对扩展开放,对修改关闭.用抽象构建框架,用实现扩展细节. 开闭原则中原有"开",是指对于组件功能的扩展是开放的,是允许对其进行功 ...

  3. 软件设计模式——软件设计原则

    摘要 设计模式(Design Pattern)是一套被反复使用.多数人知晓的.无数工程师实践的代码设计经验的总结,它是面向对象思想的高度提炼和模板化,使用设计模式是为了让代码具有更高的可重用性,更好的 ...

  4. UML图及软件设计原则详解

    1.UML图 统一建模语言(Unified Modeling Language,UML)是用设计软件的可视化建模语言.它的特点是简单.统一.图形化.能表达软件设计中的动态与静态信息. UML从目标系统 ...

  5. java 设计模式:软件设计原则、面向对象理论、23 种设计模式

    文章目录 软件设计原则 1.单一职责原则(Single Responsibility Principle) 2.开闭原则(Open Closed Principle) 3.里氏代换原则(Liskov ...

  6. 【设计模式系列24】GoF23种设计模式总结及软件设计7大原则

    设计模式总结及软件设计七大原则 设计模式系列总览 前言 软件设计7大原则 开闭原则(Open-Closed Principle,OCP) 里氏替换原则(Liskov Substitution Prin ...

  7. 设计模式01 UML图,软件设计原则,创建型模式

    概述 "设计模式"最初并不是出现在软件设计中,而是被用于建筑领域的设计中. 1995年,由 Erich Gamma.Richard Helm.Ralph Johnson 和 Joh ...

  8. 从零开始学习Java设计模式 | 软件设计原则篇:开闭原则

    从本讲开始,咱们就要开始学习第一章中的第三部分内容,即软件设计原则了. 在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件 ...

  9. 设计模式学习笔记1——概述 UML图 软件设计原则

    目录 1.设计模式概述 1.1.软件设计模式产生背景 1.2.软件设计模式概念 1.3.学习设计模式的必要性 1.4.设计模式分类 1.4.1.创建型模式 1.4.2.结构型模式 1.4.3.行为型模 ...

最新文章

  1. flask 配置文件
  2. 取成本中心-生产订单
  3. java制作文本框中的表格输入List数据
  4. hibernate_day03_MySQL数据库-表与表之间的多对多关系-实例
  5. vue --- Vue中的路由跳转问题
  6. Linux设备驱动入门----globalmem字符设备驱动
  7. ERROR: Cannot unpack file C:\Users\admin\AppData\Local\Temp\pip-unpack-yo8pmupp\simple.htm (download
  8. 中矿新生赛 H 璐神看岛屿【BFS/DFS求联通块/连通块区域在边界则此连通块无效】...
  9. python多个对象嵌套会有问题吗_Python列表嵌套常见坑点及解决方案
  10. Take it easy
  11. ANSYS网格划分---单元类型选择及步骤
  12. 迭代法动态生成谢尔宾斯基三角形
  13. Java Foreach拉姆达表达式
  14. MySQL插入emoji表情错误的2种解决方案,Incorrect string value: '\xF0\x9F\x98\x84'
  15. ARCore从零到一 (3) 更换AR模型
  16. C#VB.NET 合并PDF页面
  17. Kafka学习整理三(borker(0.9.0及0.10.0)配置)
  18. [转]阿里云的这群疯子
  19. STM32单片机烧录失败汇总
  20. 辰颐物语系统(开发、奖励规则)

热门文章

  1. 为什么有的人手机通知栏显示的是4G+而有的是HD?
  2. The 6 richest people in the world
  3. 排序算法(三)--冒泡排序法
  4. Java - package和import
  5. sql azure 语法_Azure SQL –弹性作业代理
  6. ffmpeg 视频合并
  7. Mac 显示sudo: pip: command not found
  8. 严格单调递增与非严格之间的转换
  9. Clos Network
  10. 《C++ Primer Plus》14.2 私有继承 学习笔记