设计模式——软件API设计最佳实践指南小结

Posted CrazyMo_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式——软件API设计最佳实践指南小结相关的知识,希望对你有一定的参考价值。

引言

优秀的API 能够让阅读者和调用者赏心悦目,反之则,以下是借鉴 Google Cloud API guide,[6] Microsoft API design best practices 提取的一些关于API 设计的指导原则,不**宜生搬硬套灵活运用才是良策,**仅供参考。

一、面向对象的设计原则

1、开闭原则(★★★★★)

开闭原则(The Open-Closed Principle ,OCP),在进行面向对象设计中,设计类或其他程序单位时,应该遵循:

  • 对扩展开放(open)
  • 对修改关闭(closed) 的设计原则。

即在不修改原有实体的基础扩展功能,抽象化是开闭的核心思想。根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。

2、依赖倒转原则(★★★★★)

依赖倒置原则(Dependency Inversion Principle ,DIP)将依赖关系倒置为,依赖接口,面向接口(抽象)编程,而不是针对具体类编程,主要体现在:

  • 上层模块不宜依赖于下层,二者都应该依赖于抽象

  • 父类不能依赖子类,两者都应该依赖同一抽象。

  • 抽象不能依赖于具体

  • 针对接口编程,不要针对实现编程

  • 依赖于抽象,即任何变量都不应该持有一个指向具体类的指针或引用;任何类都不应该从具体类派生。

  • 设计接口而非设计实现,使用继承避免对类的直接绑定

抽象类/接口: 倾向于较少的变化;抽象是关键点,它易于修改和扩展;不要强制修改那些抽象接口/类,而有些类不可能变化,在可以直接使用具体类的情况下,不需要插入抽象层,如:字符串类

  • 避免传递依赖,使用继承和抽象类来有效地消除传递依赖

依赖倒置原则可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险,

3、里氏替换原则(★★★★)

里氏替换原则(Liskov Substitution Principle ,LSP)所有引用基类的地方必须能透明地使用其派生类的对象且满足以下2个条件的OO设计才可被认为是真正满足了LSP原则:

  • 不应该在代码中出现通过if/else等条件语句,对派生类类型进行判断

  • 派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

在一个软件系统中,所有使用到基类的地方都可以使用子类替代,主要针对继承的设计原则

  • 子类必须能够替换其父类
  • 子类可以扩展父类的功能,但不能改变父类原有的功能
  • 子类可以实现父类的抽象方法,但不能完全覆盖父类的非抽象方法
  • 子类可以增加自己的独有方法
  • 子类重载父类的方法时,形参要比父类更宽松,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松
  • 子类实现父类抽象方法时,返回值要比父类更严格。
  • 当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等。

里式替换原则可以作为是否应该使用继承的基本依据,如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,即不宜粗暴地只是根据两者之间是否有相同之处来决定是否使用继承。

只有当以下的条件全部被满足时,才应当使用继承关系。

  • 派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分"Has-A"和"Is-A"。只有"Is-A"关系才符合继承关系,"Has-A"关系应当用聚合来描述。

  • 永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。

  • 派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。

  • 只有在分类学角度上有意义时,才可以使用继承。

4、合成复用原则(★★★★)

组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)尽量使用组合/聚合,在软件系统中类与类优先建立组合、聚合、关联关系,尽量少使用继承。即在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新对象通过向这些对象的委派达到复用已有功能的目的。通常在面向对象设计中,有两种基本的办法可以实现复用:

  • 第一种是通过组合/聚合
  • 第二种就是通过继承。
组合继承
优点1、新对象存取子对象的唯一方法是通过子对象的接口。
2、这种复用是黑箱复用,因为子对象的内部细节是新对象所看不见的。
3、这种复用更好地支持封装性。且相互依赖性比较小。
4、每一个新的类可以将焦点集中在一个任务上。
5、这种复用可以在运行时间内动态进行,新对象可以动态的引用与子对象类型相同的对象。
6、作为复用手段可以应用到几乎任何环境中去。
新的实现较为容易,因为基类的大部分功能可以通过继承的关系自动进入派生类。 修改和扩展继承而来的实现较为容易。
缺点系统中会有较多的对象需要管理。继承复用破坏封装性,因为继承将基类的实现细节暴露给派生类。由于基类的内部细节常常是对于派生类透明的,所以这种复用是透明的复用,又称“白箱”复用。 如果基类发生改变,那么派生类的实现也不得不发生改变。

5、单一职责原则(★★★★)

一个类的职责要单一,同时避免将相同功能分散到不同的类,也避免一个类承担过多的职责,高内聚,低耦合。类设计的主要工作就是“发现职责”并分离。比如工厂模式分离了对象的创建和使用,两个功能在不同的类中被实现。如果一个类需要改变,改变它的理由永远只有一个。如果存在多个改变它的理由,就需要重新设计该类。其核心含意是:只能让一个类/接口/方法有且仅有一个职责

注意这里的职责的意思是不是只能有一个接口,而是它表现的功能集是完备不可约。

6、迪米特法则(★★★)

迪米特原则(最少知道原则)(Law of Demeter ,LoD)一个软件实体对其他实体的引用越少越好,或者说两个实体类尽量减少不必要的直接交互,而是通过第三方进行交互。对于面向OOD表现在:

  • 一个软件实体应当尽可能少地与其他实体发生相互作用。

  • 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位:

    • 当前对象本身(this)
    • 以参量形式传入到当前对象方法中的对象
    • 当前对象的实例变量直接引用的对象
    • 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
    • 当前对象所创建的对象

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”(非密切单位)。迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。但不希望类直接建立直接的接触。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度。

7、接口隔离原则(★★)

接口分隔原则(Interface Segregation Principle ,ISP)不能宜强迫用户去依赖那些他们不使用的接口。因为使用多个专门的接口比使用单一的总接口总要好,具体表现为:

  • 接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口。

  • 接口的依赖(继承)原则:如果一个接口a继承另一个接口b,则接口a相当于继承了接口b的方法,那么继承了接口b后的接口a也应该遵循上述原则:不应该包含用户不使用的方法。 反之,则说明接口a被b给污染了,应该重新设计它们的关系。

接口分隔原则主要是约束接口,针对抽象、整体框架。使用多个接口替代一个整体接口,一个类对另外一个类的依赖性建立在最小的接口上。

★代表重要性

二、类的内部设计原则

1、 成员数据私有

这是最重要的,绝对不要破坏封装性。有时需要编写一个访问器方法或更改器方法, 但是最好还是尽量保持实例域的私有性。很多惨痛的经验告诉我们, 数据的表示形式很可能会改变, 但它们的使用方式却不会经常发生变。当数据保持私有时, 它们的表示形式的变化不会对类的使用者产生影响, 即使出现 bug 也易于检测。

2、 数据初始化

Java 不对局部变量进行初始化, 但是会对象的实例域进行初始化。最好不要依赖于系统的默认值, 而是应该显式地初始化所有的数据, 具体的初始化方式可以是提供默认值, 也可以是在所有构造器中设置默认值。

3、合理封装

不要在类中使用过多的基本类型,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。例如, 用一个称为Address 的新的类替换一个 Customer 类中以下的实例域:

private String street;
private String city;
private String state;
private int zip;

这样可以很容易处理地址的变化, 例如, 需要增加对国际地址的处理。

4、不是所有的域都需要独立的域访问器和域更改器

或许需要获得或设置雇员的薪金,而一旦构造了雇员对象, 就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域, 例如, 在 Address 类中, 存放州缩写的数组。

5、将职责过多的类进行分解

这样说似乎有点含糊不清, 究竟多少算是“ 过多” ? 每个人的看法不同。但是,如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解(但另一方面,也不要走极端。设计 10 个类,每个类只有一个方法,显然有些矫枉过正了)。下面是一个反面的设计示例

public class CardDeck // bad design
{
    private int value;
    private int[] suit;
    public CardDeck() { . . . }
    public void shuffle0 { ... }
    public int getTopValueO { . . . }
    public int getTopSuitO { . . . }
    public void drawO {... }
}

实际上,这个类实现了两个独立的概念:一副牌(含有 shuffle 方法和 draw方法)和一
张牌(含有查看面值和花色的方法)。另外, 引入一个表示单张牌的 Card 类。现在有两个类,
每个类完成自己的职责:

public class CardDeck
{
private Card[] cards;
public CardDeckO { . . }
public void shuffle() { . . . }
public Card getTopO { . . . }
public void draw() { . . . }
}
public class Card
{
private int value;
private int suit;
    
public Card(int aValue, int aSuit) { . . . }
public int getValueO { . . . }
public int getSuitO { . . . }
}

6、类名和方法名要能够体现它们的职责

与变量应该有一个能够反映其含义的名字一样, 类也应该如此(在标准类库中, 也存在着一些含义不明确的例子,如:Date 类实际上是一个用于描述时间的类)。 命名类名的良好习惯是采用一个名词(Order )、 前面有形容词修饰的名词( RushOrder ) 或动名词(有“ -ing” 后缀)修饰名词(例如, BillingAddress)。对于方法来说,习惯是访问 器方法用小写 get 开头 ( getSalary ), 更改器方法用小写的 set 开头(setSalary )

7、优先使用不可变的类

LocalDate 类以及 java.time 包中的其他类是不可变的—没有方法能修改对象的状态。类似 plusDays 的方法并不是更改对象,而是返回状态已修改的新对象。 更改对象的问题在于, 如果多个线程试图同时更新一个对象,就会发生并发更改。其结 果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。 因此, 要尽可能让类是不可变的, 这是一个很好的想法。对于表示值的类, 如一个字符串或一个时间点,这尤其容易。计算会生成新值, 而不是更新原来的值。 当然,并不是所有类都应当是不可变的。如果员工加薪时让 raiseSalary 方法返回一个新的 Employee 对象, 这会很奇怪。

以上是关于设计模式——软件API设计最佳实践指南小结的主要内容,如果未能解决你的问题,请参考以下文章

API设计指南 一个接口文档模板的最佳实践

API设计指南-「干货」一个接口文档模板的最佳实践

20个设计数据库的最佳实践指南

关于RestFul API 介绍与实践

软件需求分架构设计与建模最佳实践

22条API设计的最佳实践