聊聊OOP中的设计原则以及访问者模式
一 设计原则 (SOLID)
1. S - 单一职责原则(Single Responsibllity Principle)
1.1 定义
一个类或者模块只负责完成一个职责(或功能), 认为“对象应该仅具有一种单一功能”的概念, 如果一个类包含了两个或两个以上业务没有关联的功能,就被认为是职责不够单一,可以差分成多个功能单一的类
1.2 举个栗子
Employee 类里面包含了多个不同的行为, 违背了单一指责原则
通过拆分出 TimeSheetReport 类, 依赖了 Employee 类, 遵循单一指责原则
2. O - 开放关闭原则(Open-Closed Principle)
2.1 定义
软件实体(包括类、模块、功能等)应该对扩展开放,但是对修改关闭, 满足以下两个特性
- 对扩展开放
模块对扩展开放,就意味着需求变化时,可以对模块扩展,使其具有满足那些改变的新行为
- 对修改关闭
模块对修改关闭,表示当需求变化时,应该尽量在不修改源代码的基础上面扩展功能
2.2 举个栗子
在订单中需要根据不同的运输方式计算运输成本
Order
类中计算运输成本,如果后续再增加新的运输方式,就需要修改Order原来的方法getShippingCost() , 违背了OCP
根据多态的思想,可以将 shipping 抽象成一个类, 后续新增运输方式, 无须修改Order 类原有的方法,
只需要在增加一个Shipping的派生类就可以了
3. L - 里氏替换原则(Liskov Substitution Principle)
3.1 定义
使用父类的地方都可以用子类替代,子类能够兼容父类
- 子类方法的参数类型应该比父类方法的参数类型更抽象或者说范围更广
- 子类方法的返回值类型应该比父类方法的返回值类型更具体或者说范围更小
3.2 举个栗子
子类方法的参数类型应该比父类方法的参数类型更抽象或者说范围更广
演示 demo
class Animal {}
class Cat extends Animal {
faviroteFood: string;
constructor(faviroteFood: string) {
super();
this.faviroteFood = faviroteFood;
}
}
class Breeder {
feed(c: Animal) {
console.log("Breeder feed animal");
}
}
class CatCafe extends Breeder {
feed(c: Animal) {
console.log("CatCafe feed animal");
}
}
const animal = new Animal();
const breeder = new Breeder();
breeder.feed(animal);
// 约束子类能够接受父类入参
const catCafe = new CatCafe();
catCafe.feed(animal);
- 子类方法的返回值类型应该比父类方法的返回值类型更具体或者说范围更小
class Animal {}
class Cat extends Animal {
faviroteFood: string;
constructor(faviroteFood: string) {
super();
this.faviroteFood = faviroteFood;
}
}
class Breeder {
buy(): Animal {
return new Animal();
}
}
class CatCafe extends Breeder {
buy(): Cat {
return new Cat("");
}
}
const breeder = new Breeder();
let a: Animal = breeder.buy();
const catCafe = new CatCafe();
a = catCafe.buy();
- 子类不应该强化前置条件
- 子类不应该弱化后置条件
4. I - 接口隔离原则(Interface Segregation Principle)
4.1 定义
客户端不应该依赖它不需要的接口, 一个类对另一个类的依赖应该建立在最小的接口上
4.2 举个栗子
类 A 通过接口 I 依赖类 B,类 C 通过接口 I 依赖类 D,如果接口 I 对于类 A 和类 B 来说不是最小接口,则类 B 和类 D 必须去实现他们不需要的方法
interface I {
m1(): void;
m2(): void;
m3(): void;
m4(): void;
m5(): void;
}
class B implements I {
m1(): void {}
m2(): void {}
m3(): void {}
//实现的多余方法
m4(): void {}
//实现的多余方法
m5(): void {}
}
class A {
m1(i: I): void {
i.m1();
}
m2(i: I): void {
i.m2();
}
m3(i: I): void {
i.m3();
}
}
class D implements I {
m1(): void {}
//实现的多余方法
m2(): void {}
//实现的多余方法
m3(): void {}
m4(): void {}
m5(): void {}
}
class C {
m1(i: I): void {
i.m1();
}
m4(i: I): void {
i.m4();
}
m5(i: I): void {
i.m5();
}
}
将臃肿的接口 I 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依赖关系
interface I {
m1(): void;
}
interface I2 {
m2(): void;
m3(): void;
}
interface I3 {
m4(): void;
m5(): void;
}
class B implements I, I2 {
m1(): void {}
m2(): void {}
m3(): void {}
}
class A {
m1(i: I): void {
i.m1();
}
m2(i: I2): void {
i.m2();
}
m3(i: I2): void {
i.m3();
}
}
class D implements I, I3 {
m1(): void {}
m4(): void {}
m5(): void {}
}
class C {
m1(i: I): void {
i.m1();
}
m4(i: I3): void {
i.m4();
}
m5(i: I3): void {
i.m5();
}
}
4.3 现实中的栗子
以电动自行车为例
普通的电动自行车并没有定位和查看历史行程的功能,但由于实现了接口 ElectricBicycle ,所以必须实现接口中自己不需要的方法。更好的方式是进行拆分
5. D - 依赖倒置原则
5.1 定义
依赖一个抽象的服务接口,而不是去依赖一个具体的服务执行者,从依赖具体实现转向到依赖抽象接口,倒置过来
在软件设计中可以将类分为两个级别:高层模块, 低层模块, 高层模块不应该依赖低层模块,两者都应该依赖其抽象。高层模块指的是调用者,低层模块指的是一些基础操作
依赖倒置基于这个事实:相比于实现细节的多变性,抽象的内容要稳定的多
5.2 举个栗子
SoftwareProject类直接依赖了两个低级类, FrontendDeveloper 和 BackendDeveloper, 而此时来了一个新的低层模块,就要修改 高层模块 SoftwareProject 的依赖
class FrontendDeveloper {
public writeHtmlCode(): void {
// some method
}
}
class BackendDeveloper {
public writeTypeScriptCode(): void {
// some method
}
}
class SoftwareProject {
public frontendDeveloper: FrontendDeveloper;
public backendDeveloper: BackendDeveloper;
constructor() {
this.frontendDeveloper = new FrontendDeveloper();
this.backendDeveloper = new BackendDeveloper();
}
public createProject(): void {
this.frontendDeveloper.writeHtmlCode();
this.backendDeveloper.writeTypeScriptCode();
}
}
可以遵循依赖倒置原则, 由于 FrontendDeveloper 和 BackendDeveloper是相似的类, 可以抽象出一个 develop 接口, 让FrontendDeveloper 和BackendDeveloper 去实现它, 我们不需要在 SoftwareProject类中以单一方式初始化 FrontendDeveloper 和 BackendDeveloper,而是将它们作为一个列表来遍历它们,分别调用每个 develop() 方法
interface Developer {
develop(): void;
}
class FrontendDeveloper implements Developer {
public develop(): void {
this.writeHtmlCode();
}
private writeHtmlCode(): void {
// some method
}
}
class BackendDeveloper implements Developer {
public develop(): void {
this.writeTypeScriptCode();
}
private writeTypeScriptCode(): void {
// some method
}
}
class SoftwareProject {
public developers: Developer[];
public createProject(): void {
this.developers.forEach((developer: Developer) => {
developer.develop();
});
}
}
二 访问者模式 (Visitor Pattern)
1. 意图
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
- Visitor的作用,即
作用于某对象结构中的各元素的操作
,也就是 Visitor 是用于操作对象元素的 它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
也就是说,你可以只修Visitor 本身完成新操作的定义,而不需要修改原本对象, Visitor设计奇妙之处, 就是将对象的操作权移交给了 Visitor
2. 场景
- 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式
- 访问者模式通过在访问者对象中为多个目标类提供相同操作的变体, 让你能在属于不同类的一组对象上执行同一操作
3. 访问者模式结构
- Visitor:访问者接口
- ConcreteVisitor:具体的访问者
- Element: 可以被访问者使用的元素,它必须定义一个 Accept 属性,接收 visitor 对象。这是实现访问者模式的关键
可以看到,要实现操作权转让到 Visitor
,核心是元素必须实现一个 Accept
函数,将这个对象抛给 Visitor
:
class ConcreteElement implements Element {
public accept(visitor: Visitor) {
visitor.visit(this)
}
}
从上面代码可以看出这样一条链路:Element 通过 accept函数接收到 Visitor 对象,并将自己的实例抛给 Visitor 的 visit函数,这样我们就可以在 Visitor 的 visit 方法中拿到对象实例,完成对对象的操作
4 . 实现方式以及伪代码
在本例中, 访问者模式为几何图像层次结构添加了对于 XML 文件导出功能的支持
4.1 在访问者接口中声明一组 “访问” 方法, 分别对应程序中的每个具体元素类
interface Visitor {
visitDot(d: Dot): void;
visitCircle(c: Circle): void;
visitRectangle(r: Rectangle): void;
}
4.2 声明元素接口。 如果程序中已有元素类层次接口, 可在层次结构基类中添加抽象的 “接收” 方法。 该方法必须接受访问者对象作为参数
interface Shape {
accept(v: Visitor): void;
}
4.3 在所有具体元素类中实现接收方法, 元素类只能通过访问者接口与访问者进行交互,不过访问者必须知晓所有的具体元素类, 因为这些类在访问者方法中都被作为参数类型引用
class Dot implements Shape {
public accept(v: Visitor): void {
return v.visitDot(this)
}
}
class Circle implements Shape {
public accept(v: Visitor): void {
return v.visitCircle(this)
}
}
class Rectangle implements Shape {
public accept(v: Visitor): void {
return v.visitRectangle(this)
}
}
4.4 创建一个具体访问者类并实现所有的访问者方法
class XMLExportVisitor implements Visitor {
visitDot(d: Dot): void {
console.log(`导出点(dot)的 ID 和中心坐标`);
}
visitCircle(c: Circle): void {
console.log(`导出圆(circle)的 ID 、中心坐标和半径`);
}
visitRectangle(r: Rectangle): void {
console.log(`导出长方形(rectangle)的 ID 、左上角坐标、宽和长`);
}
}
4.5 客户端必须创建访问者对象并通过 “接收” 方法将其传递给元素
const application = (shapes:Shape[],visitor:Visitor) => {
// ......
for (const shape of allShapes) {
shape.accept(visitor);
}
// ......
}
const allShapes = [
new Dot(),
new Circle(),
new Rectangle()
];
const xmlExportVisitor = new XMLExportVisitor();
application(allShapes, xmlExportVisitor);
4.6 完整代码预览
interface Visitor {
visitDot(d: Dot): void;
visitCircle(c: Circle): void;
visitRectangle(r: Rectangle): void;
}
interface Shape {
accept(v: Visitor): void;
}
class Dot implements Shape {
public accept(v: Visitor): void {
return v.visitDot(this)
}
}
class Circle implements Shape {
public accept(v: Visitor): void {
return v.visitCircle(this)
}
}
class Rectangle implements Shape {
public accept(v: Visitor): void {
return v.visitRectangle(this)
}
}
class XMLExportVisitor implements Visitor {
visitDot(d: Dot): void {
console.log(`导出点(dot)的 ID 和中心坐标`);
}
visitCircle(c: Circle): void {
console.log(`导出圆(circle)的 ID 、中心坐标和半径`);
}
visitRectangle(r: Rectangle): void {
console.log(`导出长方形(rectangle)的 ID 、左上角坐标、宽和长`);
}
}
const allShapes = [
new Dot(),
new Circle(),
new Rectangle()
];
const application = (shapes:Shape[],visitor:Visitor) => {
// ......
for (const shape of allShapes) {
shape.accept(visitor);
// .....
}
const xmlExportVisitor = new XMLExportVisitor();
application(allShapes, xmlExportVisitor);
5. 访问者模式优缺点
优势:
- 开闭原则。 你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改
- 单一职责原则 可将同一行为的不同版本移到同一个类中
不足:
- 每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者
- 在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限
聊聊OOP中的设计原则以及访问者模式的更多相关文章
- 设计模式(含UML、设计原则、各种模式讲解链接)
一.统一建模语言UML UML是一种开放的方法,用于说明.可视化.构建和编写一个正在开发的.面向对象的.软件密集系统的制品的开放方法 UML展现了一系列最佳工程实践,这些最佳实践在对大规模,复杂系统进 ...
- Hbase中rowkey设计原则
1.热点问题 在某一时间段,有大量的数据同时对一个region进行操作 2.原因 对rowkey的设计不合理 对rowkey的划分不合理 3.解决方式 rowkey是hbase的读写唯一标识 最大长度 ...
- 078 Hbase中rowkey设计原则
1.热点问题 在某一时间段,有大量的数据同时对一个region进行操作 2.原因 对rowkey的设计不合理 对rowkey的划分不合理 3.解决方式 rowkey是hbase的读写唯一标识 最大长度 ...
- Java基础学习 -- Java(OOP)程序的设计原则
避免代码复制.解决方案:函数.父类: 封装.尽量private每个类的成员变量,用操作封装数据,减少类与类之间成员变量的直接调用,而是调用method,降低耦合: 可扩展性最大化.尽量使用框架+数据的 ...
- 浅谈Java五大设计原则之代理模式
我们来定义一下 AOP(面向切面编程) 它是面向对象的一种补充或者是一种增强,它在这基础上增加了一些 而外的功能增强. 它可以在原有的行为不改变的前提,在这之前或者之后完成一些而外 的事情. 而AO ...
- Java程序员应当知道的10个面向对象设计原则
面向对象设计原则是OOPS编程的核心, 但我见过的大多数Java程序员热心于像Singleton (单例) . Decorator(装饰器).Observer(观察者) 等设计模式,而没有把足够多的注 ...
- Design Principle vs Design Pattern 设计原则 vs 设计模式
Design Principle vs Design Pattern设计原则 vs 设计模式 来源:https://www.tutorialsteacher.com/articles/differen ...
- .NET 云原生架构师训练营(设计原则与模式)--学习笔记
在复杂系统的架构设计中引入设计原则与模式,能够极大降低复杂系统开发.和维护的成本 目录 几个问题 为什么要学习设计模式 优良架构设计的具体指标 理解复杂系统 面向对象思想(指导复杂系统的分析.设计.实 ...
- C#设计模式之二十二访问者模式(Visitor Pattern)【行为型】
一.引言 今天我们开始讲"行为型"设计模式的第九个模式,该模式是[访问者模式],英文名称是:Visitor Pattern.如果按老规矩,先从名称上来看看这个模式,我根本不能获 ...
随机推荐
- HTML表格CSS美化
效果展示 style.css html{ width: 100%; height: 100%; overflow: hidden;}body{ width: 100%; height: 100%; f ...
- php文件下载服务器代码
事情的起因 额,平板想下载电脑上的pdf文件,我开启了web服务,局域网下的ipad访问该文件web路径会直接打开该pdf,而不是下载.于是本小白就折腾了一下. 源代码 <?php forceD ...
- spring-bean依赖注入-02(通过p命名空间注入)
上一篇博客讲述了为什么使用spring依赖注入,怎么注入,详见 spring-bean依赖注入-01(等你来点击) 废话不多说,开始使用p命名空间进行set注入 使用另外一种注入方式是这样的(具体实现 ...
- Codeforces Round #710 (Div. 3) Editorial 1506A - Strange Table
题目链接 https://codeforces.com/contest/1506/problem/A 原题 1506A - Strange Table Example input 5 1 1 1 2 ...
- Java语言学习day12--7月11日
###16随机点名器代码实现 * A: 随机点名器案例代码 /* 随机点名器,集合改进 (学生的姓名和年龄) 现实中有学生这个事物,使用定义类的形式,描述学生事物 属性: 姓名,年龄 姓名存储了数组, ...
- VMware虚拟机无法安装Win11解决方法 (暂时全网最全方案)
目录 1.现象 1.蓝屏重启 2.如下图示,无法启动 2.解决方案 2.1 Hyper-V方案 2.2 禁用 Device Guard(系统:win11) 2.3 升级虚拟机VMware pro的版本 ...
- 国产化之 .NET Core 操作达梦数据库DM8的两种方式
背景 某个项目需要实现基础软件全部国产化,其中操作系统指定银河麒麟,数据库使用达梦V8,CPU平台的范围包括x64.龙芯.飞腾.鲲鹏等.考虑到这些基础产品对.NET的支持,最终选择了.NET Core ...
- Typora使用手册(基础)
Typora使用手册 第一步,你首先得拥有一个Typora,可通过(https://typoraio.cn/)该网址下载. 第二步,安装并打开. 第三步,让我们开始认识并设置自己的Typora吧~ 什 ...
- Atlassian应对CVE-2022-22963,CVE-2022-22965的常见问题
CVE-2022-22965 常见问题解答 基本信息 已发现 Spring Framework 中的关键远程代码执行漏洞 CVE-2022-22965.根据 Spring 的安全公告,此漏洞会影响在 ...
- Springboot 整合 MyBatisPlus[详细过程]
Springboot 整合 MyBatisPlus[详细过程] 提要 这里已经将Springboot环境创建好 这里只是整合MyBatis过程 引入Maven依赖 添加MyBatisPlus启动依赖, ...