软件设计原则

软件设计原则可以说是无数前辈在踩过无数坑之后总结出来的提醒后人遵循的一些基本思想、规范、模式。遵循这些原则,有利于我们做出良好的设计,比如达到高内聚低耦合、模块划分清晰、源码可读性可维护性良好的效果。下面将对最广为人知的八大原则进行说明,有些原则还会给出代码示例。

 

6.1  KISS原则

KISS是“keep it simple, stupid”的简称,这种观点认为,一个简单的系统往往比复杂的系统运转得更好,因此,在进行系统设计时应尽量保持简单,避免不必要的复杂性。 

这里强调的是不必要的复杂性,有时候一些复杂性不可避免,但我们无论在产品设计还是架构设计方面,在保持功能完整性的同时,应该尽量做减法,坚守KISS原则。

 

6.2  DRY原则

DRY是“Don't repeat yourself ”的简称,这个原则的目标在于通过抽象的手段来减少重复的模式,避免冗余,包括设计、代码等方面的冗余,强调系统中的每个实体元素都是单一的、无歧义的。DRY原则运用好的话,系统中任何元素的修改都不会导致逻辑上跟它无关的元素的修改,而逻辑上相关的元素即使被修改,也是以统一的可预测的方式被修改。

怎么做到遵守DRY原则呢?本人认为要依靠抽象思维,我们在设计系统和数据结构时,尽量理解透彻系统中所存在的实体及其关系,将共性的内容抽出来成为一个抽象层,具体的实体再从抽象层派生,这里其实就对应了面向对象里面继承的概念,继承使得代码可以被复用。

 

6.3  开闭原则

开闭原则也叫Open-Close原则,大致思想是:软件实体(包括类、模块、函数等)应该对扩展开放,而对修改关闭。以类为例来说明,就是你可以从一个基类派生新的类作为子类,子类可以扩展基类的功能,但不能修改基类的代码。开闭原则也是最基本的原则,这一章中所有其它原则本身都遵守了开闭原则。

现实中,很多软件中的插件,就是开闭原则思想的具体实践。通常允许你通过添加插件的方式来扩展软件的行为,而不允许修改软件的一些基础功能。如果允许你修改软件的基本功能,那么有一天,这个软件具备的功能和软件原来声称的功能大相径庭,想想都觉得是件诡异的事情。

 

6.4  里氏替换原则

里氏替换原则(Liskov substitution)可以表述为:所有出现基类的地方都可以由子类来代替,并且替换后不需要修改其它代码系统还可以继续正常工作。这样看来,里氏替换原则也符合开闭原则,不修改基类,而是将子类替换基类,就能够达到扩展系统功能的目的。

怎么运用这个原则呢?个人认为在设计系统时,要关注系统实体间的关系(比如是否是继承关系)和扩展性,这两个特性结合起来的地方可能就是使用了里氏替换原则。

 

6.5  依赖倒置原则

依赖倒置原则是软件模块解耦的一种方式,其核心思想是:高层模块不应该依赖于底层模块,它们都应该依赖于抽象,也就是抽象不能依赖于细节,细节应该依赖抽象。

前后端分离架构就是一个例子,前端只管使用ajax请求服务端的API,不在乎服务器端API服务是如何实现和部署的,只需要API接口遵守协议(比如HTTP)就好了。

 

6.6  单一职责原则

顾名思义,单一职责原则的核心思想是:一个类,只做一件事情,只有一个理由引发它的改变。这也和Unix的设计哲学之一“Do One Thing and Do It Well”有异曲同工之妙,即只做一件事,并把它做好。

这个原则说起来最简单,而实际上执行起来不容易,稍微不小心就容易违背它。只做一件事,对应到软件设计里边,就是一个模块(类、函数)尽量少做事情,但要做好,这样也符合我们常说的高内聚低耦合的设计要求,所以在单一原则里边,我们需要注意的是模块(类、函数)的边界和职责划分,思考怎样划分才能符合该原则,这里就不举例子了。

 

6.7  接口隔离原则

接口隔离原则是面向对象设计的一个原则,倡导将接口分离,即用户不需要实现他使用不到的接口。比如一个接口定义有两个方法:

interface ShapeInterface {

  public function area();

  public function volume();

} 

而有时候实现类只使用到其中一个方法,但编程语言规范又迫使派生类去实现基类的两个接口。如果遵循接口隔离原则,这时候应该将接口分解为两个接口,然后实现类根据需要可以只实现其中一个接口,如果需要两个接口方法都实现,就去实现两个接口好了。代码如下:

interface ShapeInterface {

    public function area();

}

interface SolidShapeInterface {

    public function volume();

}

class Cuboid implements ShapeInterface,  SolidShapeInterface {

    public function area() { }

    public function volume() {}

} 

原则是一种思想,并不局限于某种编程语言,上面以Java代码为例来说明,其他语言中可以根据各个语言的不同规范采用不同的方式来是接口符合这个原则。

 

6.8  最少知识原则

最少知识原则(Least Knowledge Principle)也叫迪米特法则(Law of Demeter),其来源于1987年荷兰大学的一个叫做Demeter的项目。Craig Larman把Law of Demeter又称作“不要和陌生人说话”,其核心思想是提倡一个模块只知道自己模块内部的东西,对其它模块知之甚少,与其它模块的交互都是通过接口来实现,这个原则可以引导我们完成低耦合的系统设计。

上面说到的是模块,那其实我们可以小到一个类,一个函数,都可以遵循最少知识原则。比如我们尽量将类的成员变量声明为私有的,只能通过类的公共方法来访问该变量,外部不能直接使用该变量。在函数里面,我们尽量使用局部变量,少用全局变量,因为局部变量只在这个函数内有效,全局变量可以共用,使得函数的耦合性变高了。因此,提倡使用局部变量,也是遵循了最少知识原则。

 

6.9  好莱坞明星原则

好莱坞明星原则叫IOC(Inversion of control)原则或者控制反转原则,英文里比较形象的说法就是“Don’t call me, I will call you back”,源于这么一个现象:好莱坞的经纪人们一般不希望你去联系他们,而是他们会在需要的时候来联系你,因为主动权确实是掌握在他们手上,耍大牌不用商量。   

传统的编程方式是,用户代码直接调用底层库函数,而控制反转原则是底层框架反过来调用用户编写的代码,类似基于事件的编程。当事件触发时,底层框架代码被调用,然后再反过来调用用户自定义的事件处理程序。这样来看的话,就是框架提供了程序的引擎和接口定义,用户代码负责接口实现,应用程序的开发模式就掌控在框架开发者的手中。引用台湾著名架构师高焕堂的话,就是好莱坞明星原则保证了强盛,保证了底层框架的龙头地位。    

这里我们举个简单的例子吧,比如我们在进行GUI编程时,通常要为控件编写一个OnCreate函数,但是这个函数我们从没有主动去调用它,而是框架在适当的时机来调用,这就是将控制权交给了底层框架,完成了控制反转。

 

6.10  面向接口原则

面向对象设计模式中有一个模式叫桥接模式(Bridge pattern),提倡基于接口编程,英文里边的说法是:Program to an interface, not an implementation。因为同一个接口可以衍生出各种不同的实现,接口和实现分离使得用户程序可以根据不同的情况选择不同的实现,实现的修改独立于接口的调用者,这样当需要修改实现代码时,接口调用者是无感知的,这样也就降低了软件的耦合度。

下面我们以Java代码为例来看一下如何面向接口编程,首先,我们有一个显示接口,定义如下:

interface displayModule {

  public void display();

}

显示器类,实现displayModule接口

public class Monitor implements displayModule{

   public void display() {  

    System.out.println(“Display through   Monitor”);

   }

}

投影仪类,实现displayModule接口

public class Projector implements displayModule

{

    public void display(){

  System.out.println(“Display through projector”);

    }

}

主机类,聚合了显示接口

public class Computer

{

   // 面向接口编程

   displayModule dm;   

   Public void setDisplayModule

   (displayModule dm)

   {

      this.dm=dm;

    }

   public void display()

   {

       dm.display();

   }

}

程序主函数,类似于一个装配器

public static void main(String args[]) 

{

  Computer cm = new Computer();

  displayModule dm = new Monitor();

  displayModule dm1=new Projector();

  cm.setDisplayModule(dm);

  cm.display();

  cm.setDisplayModule(dm1);

  cm.display();

}

上面的例子中,Computer类聚合了displayModule 接口,然后通过一个方法setDisplayModule来设置具体的接口实现对象,比如显示器或者投影仪。主程序就类似于装配器,程序作者就像装配工人,根据需要装配不同的插件(显示器或者投影仪对象)。如果Computer类聚合的不是接口而是某个实现类,也就是直接聚合某个插件,这样就不需要方法setDisplayModule了,但是失去了灵活性,装配工人也就无法根据需要装配不同的插件了。

 

节选自本人作品:《漫谈中小企业研发技术栈》第六章。

 

欢迎关注公众号:

posted @ 2019-06-30 22:03  顺哥聊数字化  阅读(512)  评论(0编辑  收藏  举报