草庐IT

聊聊OOP中的设计原则以及访问者模式

袋鼠云数栈前端 2023-03-28 原文

一  设计原则 (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类直接依赖了两个低级类, FrontendDeveloperBackendDeveloper, 而此时来了一个新的低层模块,就要修改 高层模块 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 接口, 让FrontendDeveloperBackendDeveloper 去实现它, 我们不需要在 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中的设计原则以及访问者模式的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  3. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  4. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  5. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  6. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  7. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  8. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  9. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  10. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

随机推荐