0%

定义

Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)

开闭原则是java世界的最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。

定义实质

开闭原则定义已经非常明确的告诉我们,软件实体应该对扩展开放,对修改关闭。其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

软件实体通常包括以下几个部分:

1.项目或软件产品中按照一定的逻辑规则划分的模块。

2.抽象和类。

3.方法。

如何使用开闭原则

开闭原则是一个非常虚的原则,前面五个原则是对开闭原则的具体解释,但是开闭原则并不局限于这么多,它“虚”得没有边界,就像“好好学习,天天向上”的口号一样,告诉我们要好好学习,但是学什么,怎么学并没有告诉我们,需要去体会和掌握。那么我们如何使用开闭原则呢?

1.抽象约束

抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:

  • 1.通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法
  • 2.参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
  • 3.抽象层尽量保持稳定,一旦确定即不允许修改。

2.元数据(metadata)控制模块行为

什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。

3.封装变化

对变化的封装包含两层含义:1.将相同的变化封装到一个接口或抽象类中;2.将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

定义

迪米特原则(Law of Demeter, LoD)也称为最少知识原则(Least Knowledge Principle, LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有做少的了解。

通俗的讲,一个类应该对自己需要耦合或调用的类知道的最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。

进一步解释

1.只和朋友交流

迪米特法则还有一个英文释义是:Only talk to your immediate friends(只与直接的朋友通信。)每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。

举例

老师命令班长清点一下班级里所有女生的数量

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//老师类
public class Teacher {
//老师对学生发布命令,清一下女生
public void commond(GroupLeader groupLeader){
List listGirls = new ArrayList();
//初始化女生
for(int i=0;i<20;i++){
listGirls.add(new Girl());
}
//告诉体育委员开始执行清查任务
groupLeader.countGirls(listGirls);
}
}
1
2
3
4
5
6
7
//班长类
public class GroupLeader{
//清查女生数量
public void countGirls(List<Girl> listGirls){
System.out.println("女生数量是:"+ listGirls.size());
}
}
1
2
3
//女生类
public class Girl{
}
1
2
3
4
5
6
7
8
//场景类
public class Client{
public static void main(String[] args){
Teacher teacher = new Teacher();
//老师发布命令
teacher.commond(new GroupLeader());
}
}

运行结果:

1
女生数量是:20

仔细思考,这个程序是有问题的,老师的朋友类是班长,班长的朋友类是女生类,女生不属于老师类的朋友类,因为朋友类的定义是:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内的类不属于朋友类。而迪米特法则告诉我们,一个类只和朋友类交流,但是刚才的commond方法和Girl类有了交流,这样破坏了Teacher类的健壮性。

修改后的程序,把Girl类从Teacher类中进行抽取,修改如下:

1
2
3
4
5
public class Teacher{
public void commond(GroupLeader groupLeader){
groupLeader.countGirls();
}
}
1
2
3
4
5
6
7
8
9
public class GroupLeader{
private List<Girl> listGirls;
public GroupLeader(List<Girl> _listGirls){
this.listGirls = _listGirls;
}
public void countGirls(){
System.out.print("女生数量是:"+this.listGirls.size());
}
}
1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args){
List<Girl> listGirls = new ArrayList<Girl>();
for(int i=0;i<20;i++){
listGirls.add(new Girl());
}
Teacher teacher = new Teacher();
teacher.commond(new GroupLeader(listGirls));
}
}

2.朋友间也是有距离的

迪米特法则定义即使是朋友,也不能无话不说,无所不知。

一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散。因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以再减少public方法和属性,是否可以修改为private/protected等访问权限,是否可以加上final关键字等。

迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、protected等访问权限。

举例

场景:模拟软件安装的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//向导类
public class Wizard{
private Random rand = new Random(System.currentTimeMillis());
//第一步
public int first(){
System.out.println("执行第一个方法...");
return rand.nextInt(100);
}
//第二步
public int second(){
System.out.println("执行第二个方法...");
return rand.nextInt(100);
}
//第三步
public int third(){
System.out.println("执行第三个方法...");
return rand.nextInt(100);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//软件安装类
public class InstallSoftware{
public void installWizard(Wizard wizard){
int first = wizard.first();
if(first>50){
int second = wizard.second();
if(second>50){
int third = wizard.third();
if(third>50){
wizard.first();
}
}
}
}
}
1
2
3
4
5
6
7
//场景类
public class Client{
public static void main(String[] args){
InstallSoftware invoker = new InstallSoftware();
invoker.installWizard(new Wizard());
}
}

程序虽然简单,但仔细思考一下,Wizard类将太多的方法暴露给了InstallSoftware类,两者之间的朋友关系太过亲密了,耦合关系变得异常牢固,如果业务有变更,更改Wizard的同时,必须考虑InstallSoftware是否也需要做修改,从而扩大了变更的风险。因此我们需要进行重构。重构之后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//向导类
public class Wizard{
private Random rand = new Random(System.currentTimeMillis());
//第一步
private int first(){
System.out.println("执行第一个方法...");
return rand.nextInt(100);
}
//第二步
private int second(){
System.out.println("执行第二个方法...");
return rand.nextInt(100);
}
//第三步
private int third(){
System.out.println("执行第三个方法...");
return rand.nextInt(100);
}
//软件安装过程
public void installWizard(){
int first = this.first();
if(first>50){
int second = wizard.second();
if(second>50){
int third = wizard.third();
if(third>50){
wizard.first();
}
}
}
}
}

将三个步骤的访问权限修改为private,同时把installWizard方法从InstallSoftware中移动到Wizard中。Wizard只对外暴露一个installWizard方法,这体现了类的高内聚特性。

修改后的InstallSoftware类

1
2
3
4
5
6
//软件安装类
public class InstallSoftware{
public void installWizard(Wizard wizard){
wizard.installWizard();
}
}

3.如何衡量一个方法是否应该出现在当前类中?

在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:

如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

一个原则

如果一个项目中,我和我需要调用的类之间超过了两次,就要考虑重构了。

迪米特法则要求类间解耦,但解耦是有限度的,在实际项目中需要适度的考虑这个原则,别为了套用原则而做项目。

接口隔离原则

接口隔离原则其定义有两种:

定义

Clients should not be forced to depend upon interfaces that they don’t use.
(客户端不应该依赖它不需要的接口。)

The dependency of one class to another one should depend on the smallest possible interface.
(类间的依赖关系应该建立在最小的接口上。)

定义可以通俗的被翻译为:

建立单一接口,不要建立臃肿庞大的接口。
或者:
接口尽量细化,同时接口中的方法尽量少。

与单一职责原则的不同

定义听起来和单一职责原则所做的事情是一样的,都是给接口瘦身,减少接口中过多的方法。但实质上,与单一职责还是不一样的。两者的审视角度不一样。

单一职责强调的是类和接口职责要单一,强调的是职责,而某些情况下,相同职责下的方法可能很多,例如一个接口的职责包含了10个方法,这十个方法提供给多个模块使用,不同模块下根据各自的权限来访问,这在单一职责的角度来看是允许的,但按照接口隔离的角度来看,可能就不合理了,因为它要求“尽量使用多个专门的接口”。专门的接口指的是什么?就是指提供给每个模块的单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。

对接口规范的约束

1.接口要尽量小

但前提要求是要满足单一职责原则

2.接口要高内聚

3.定制服务

例如一个图书管理系统的查询接口

IBookSearcher
+ void searchByAuthor()
+ void searchByTitle()
+ void searchByPublisher()
+ void searchByCatagory()
+ void complexSearch(Map map)

按照模块进行接口拆分后,分成了两个接口

ISimpleBookSearcher
+ void searchByAuthor()
+ void searchByTitle()
+ void searchByPublisher()
+ void searchByCatagory()

IComplexBookSearcher
+ void complexSearch(Map map)

4.接口设计是有限度的

接口的设计颗粒度越小,系统越灵活,但是,灵活的同时带来了结构的复杂化,开发难度增加,可维护性降低,
所以接口设计一定要注意适度,这个“度”通常是根据经验和常识来判断,没有一个固化或可测量的标准。

依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP) 它的原始定义是这样的:

定义

High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.

翻译过来,包括三层含义

1.高层模块不应该依赖低层模块,两者都应该依赖其抽象;

2.抽象不应该依赖细节;

3.细节应该依赖抽象。

在Java语言中的表现就是:

1.模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;

接口或抽象类不依赖于实现类;

实现类依赖接口或抽象类。

更加精简的定义就是:

面向接口编程 — OOD(Object-Oriented Design, 面向对象设计)的精髓之一.

举例

中国象棋

每个棋子都拥有“移动(move)”的动作,但每个棋子的这个方法又有不同,比如“马走日,象走田”就是体现了移动的不同。

1
2
3
public interface IChess{
public void move();
}
1
2
3
public interface IPlayer{
public void handle(IChess chess);
}
1
2
3
4
5
public class Ma implements IChess{
public void move(){
System.out.println("马走日");
}
}
1
2
3
4
5
public class Xiang implements IChess{
public void move(){
System.out.println("象走田");
}
}
1
2
3
4
5
6
public class RedPlayer implements IPlayer{
public void handle(IChess chess){
System.out.println("红方移动");
chess.move();
}
}
1
2
3
4
5
6
public class BlackPlayer implements IPlayer{
public void handle(IChess chess){
System.out.println("黑方移动");
chess.move();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Clint{
public static void main(String args[]){
IChess ma = new Ma();
IChess xiang = new Xiang();

IPlayer redPlayer = new RedPlayer();
IPlayer blackPlayer = new BlackPlayer();

redPlayer.handle(ma);
blackPlayer.handle(xiang);
}
}
1
2
3
4
5
6
运行结果:

红方移动
马走日
黑方移动
象走田

究竟何为依赖倒置

首先看依赖二字,依赖体现共通的逻辑,对接口方法的实现就是体现了子类依赖其接口或抽象类。

那么何为“倒置”呢?“倒置”是相对“正置”而言的,人类正常的思维方式都是正置的,怎么理解呢?就是我们接触到的都是具体实现类,比如马走日,就是指我们接触到的是马所行走的方式,而倒置就是指我们根据系统设计的角度,来找出事物间的抽象模型,抽取出抽象间的依赖,来代替传统思维中对事物间依赖关系的描述。比如“司机开车”就是抽象的依赖关系,其具体的实现包括“卡车司机开卡车”,“公交车司机开小轿车”等。

依赖倒置原则的三个实现:

1.构造函数传递依赖对象

1
2
3
4
public interface IDriver {
//司机的驾驶方法
public void drive();
}
1
2
3
4
5
6
7
8
9
10
11
public class Driver implements IDriver{
private ICar car
//构造函数注入
public Driver(ICar _car){
this.car = _car;
}
//司机的驾驶方法
public void drive(){
this.car.run();
}
}

2.Setter方法传递依赖对象

1
2
3
4
5
6
public interface IDriver{
//车辆型号
public void setCar(ICar car);
//司机的驾驶方法
public void drive();
}
1
2
3
4
5
6
7
8
9
10
public class Driver implements IDriver{
private ICar car;
public void setCar(ICar car){
this.car = car;
}
//司机的驾驶方法
public void drive(){
this.car.run();
}
}

3.接口声明依赖对象

上面对依赖倒置的举例就是接口声明的方式,该方法也叫做接口注入

建议遵循的规则

1.每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备

2.变量的表面类型尽量是接口或者是抽象类

3.任何类都不应该从具体类派生

4.尽量不要复写基类的方法

5.结合里氏替换原则使用

单一职责原则

单一职责原则的英文名称是Single Responsibility Principle,简称SRP.

定义

单一职责的定义是:

There should never be more than one reason for a class to change.

应该有且仅有一个原因引起类的变更。

举例

例如IUserInfo接口定义如下:

IUserInfo
+void setUserID(String userID)
+String getUserID()
+void setPassword(String password)
+String getPassword()
+void setUserName(String userName)
+String getUserName()
+boolean changePassword(String oldPassword)
+boolean deleteUser()
+void mapUser()
+boolean addOrg(int orgID)
+boolean addRole(int roleID)

这个接口按照单一职责的原则进行拆分,将用户信息抽取成一个BO(Business Object,业务对象),将用户行为抽取成一个Biz(Business Logic,业务逻辑)

IUserBO
+void setUserID(String userID)
+String getUserID()
+void setPassword(String password)
+String getPassword()
+void setUserName(String userName)
+String getUserName()
IUserBiz
+boolean changePassword(String oldPassword)
+boolean deleteUser()
+void mapUser()
+boolean addOrg(int orgID)
+boolean addRole(int roleID)

IUserInfo接口继承IUserBO和IUserBiz

分清职责后的代码示例:

1
2
3
4
5
6
7
8
9
......
IUserInfo userInfo = new UserInfo();
//我要赋值了,我就认为它是一个纯粹的BO
IUserBO userBO = (IUserBO)userInfo;
userBO.setPassword("abc");
//我要执行动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz)userInfo;
userBiz.deleteUser();
......

注意点:

单一职责原则最难划分的就是职责。一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎样细化?细化后是否都要有一个接口或类?这些都需要从实际的项目去考虑。

对于接口,我们在设计的时候一定要做到单一,但是对于实现类酒需要多方面考虑了。生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。本来一个类可以实现的,硬要拆成两个类,然后再使用聚合或组合的方式藕合在一起,人为地制造了系统的复杂性。所以原则是死的,人是活的,这句话很有道理。

单一职责适用于接口,类,同时也适用于方法,什么意思呢?一个方法尽可能只做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法点颗粒度很粗,比如下面这个方法:

1
2
//一个方法承担多个职责
void changeUser(IUserBO userBO,String..changeOptions)

相比较而言,比较好的写法应该是这样的:

1
2
3
void changeUserName(String newUserName)
void changeHomeAddress(String newHomeAddress)
void changeOfficeTel(String telNumber)

对于单一职责原则,笔者的建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

里氏替换原则

里氏替换原则 英文名称Liskov Subtitution Principle, LSP

定义

传说中最正宗的定义:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型.

传说中第二种比较通俗的定义:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

所有引用基类的地方必须能透明地使用其子类的对象。

更通俗的一种定义:

只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就能适应。

举例

子类必须完全实现父类的方法

cs游戏中的枪支类图:
cs游戏枪支类图

代码清单:

枪支的抽象类

1
2
3
4
public abstract class AbstractGun {
//枪用来干什么的?杀敌!
public abstract void shoot();
}

手枪、步枪、机枪的实现类:

1
2
3
4
5
6
7
public class Handgun extends AbstractGun {
//手枪的特点是携带方便,射程短
@Override
public void shoot() {
System.out.println("手枪射击...");
}
}
1
2
3
4
5
6
7
public class Rifle extends AbstractGun {
//步枪的特点是射程远,威力大
@Override
public void shoot() {
System.out.println("步枪射击...");
}
}
1
2
3
4
5
6
public class MachineGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("机枪射击...");
}
}

士兵的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Soldier {
//定义士兵的枪支
private AbstractGun gun;
//给士兵一支枪
public void setGun(AbstractGun _gun){
this.gun = _gun;
}
public void killEnemy(){
System.out.println("士兵开始杀敌人...");
gun.shoot();
}
}

场景类:

1
2
3
4
5
6
7
8
9
public class Client {
public static void main(String[] args) {
//产生三毛这个士兵
Soldier sanMao = new Soldier();
//给三毛一支枪
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}

运行结果如下:

1
2
士兵开始杀敌人...
步枪射击...

在这个程序中,我们给三毛这个士兵一把步枪,然后就开始杀敌了。如果要使用机枪,当然也可以,直接把sanMao.setGun(new Rifle())修改为sanMao.setGun(new MachineGun())即可,在编写程序时Solider士兵类根本不用知道是哪个型号的枪(子类)被传入。

注意 在类中调用其他类时,务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。

如果我们要加入一把玩具枪,该如何处理呢?

第一种处理方式 ToyGun继承AbstractGun
继承方式拓展玩具枪

但是玩具枪毕竟是玩具,不能当作真枪来杀敌人,所以这样拓展并不妥当,更好的处理方式应该是如下图:

委托的方式来拓展玩具枪

ToyGun脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbastractGun建立关联委托关系。例如,可以在AbstractToy中声明将声音、形状都委托给AbstractGun处理,仿真枪嘛,形状和声音都要和真实的枪一样了,然后两个基类下的子类自由延展,互不影响。

注意 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。

子类可以有自己的个性

子类当然可以有自己的方法和属性了,这里再次强调,是因为里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任。

以刚才对枪支为例,步枪有几个比较“响亮”的型号,比如AK47、AUG狙击步枪等,把这两个型号的枪引入后的Rifle子类图如下:

步枪扩展

代码清单:

1
2
3
4
5
6
7
8
9
public class AUG extends Rifle{
//狙击枪都携带一个精准的望远镜
public void zoomOut(){
System.out.println("通过望远镜观察敌人...");
}
public void shoot(){
System.out.println("AUG射击...");
}
}

狙击手代码:

1
2
3
4
5
6
7
8
public class Snipper {
public void killEnemy(AUG aug){
//首先看看敌人的情况,别杀死敌人,自己也被人干掉
aug.zoomOut();
//开始射击
aug.shoot();
}
}

场景类:

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
//产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle(new AUG());
sanMao.killEnemy();
}
}

运行结果:

1
2
通过望远镜观察敌人...
AUG射击...

那么我们能不能传递Rifle父类给sanMao呢?如下所示:

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
//产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle((AUG)(new Rifle()));
sanMao.killEnemy();
}
}

显示是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家常说的向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

覆盖或实现父类的方法时输入参数可以被放大

即子类方法的参数应该大于父类方法的参数,即子类方法的参数应该至少与父类相同或者或者是其父类

复写或实现父类的方法时输出结果可以被缩小

即子类方法的返回值应该小于父类方法的返回值,即子类的返回值应该至多与父类的返回值相同或者是其子类

以上两点 归根结底一句话:重载或实现 子类的方法输入范围>=父类,返回范围<=父类L

Unix管道的发明人、Unix传统的奠基人之一Doug Mcllroy在[Mcllroy78]中曾经说过:

  • (i)让每个程序就做好一件事。如果有新任务,就重新开始,不要往原程序中加入新功能而搞得复杂。
  • (ii)假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是未知的。输出中不要有无关的信息干扰。避免使用严格的分栏式和二进制格式输入。不要坚持使用交互式输入。
  • (iii)经可能早地将设计和编译的软件投入试用,哪怕是操作系统也不例外,理想情况下,应该是在几星期内。对拙劣的代码别犹豫,扔掉重写。
  • (iv)优先使用工具而不是拙劣的帮助来减轻编程任务的负担。工欲善其事,必先利其器。

后来他这样总结道(引自《Unix的四分之一世纪》):

Unix哲学是这样的:一个程序制作一件事,并做好。程序要能协作。程序要能处理文本流,因为这是最通用的接口。

Rob Pike,最伟大的C语言大师之一,在《Notes on C Programming》 中从另一个稍微不同的角度阐述了Unix的哲学[Pike]:

  • 原则1:你无法判定程序会在什么地方耗费运行时间。瓶颈经常出现在想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。
  • 原则2:估量。在你没对代码进行估量,特别是没有找到最耗时的那部分之前,别去优化速度。
  • 原则3:花哨的算法在n很小时通常很慢,而n通畅很小。花哨算法的常熟复杂度很大。除非你确定n总是很大,否则不要用花哨算法(即使n很大,也优先考虑原则2)
  • 原则4:花哨的算法比简单算法更容易出bug、更难实现。尽量使用简单的算法配合简单的数据结构。
  • 原则5:数据压倒一切。如果已经选择了正确的数据结构并且把一切都组织得井井有条,正确的算法也有不言自明。编程的核心是数据结构,而不是算法。
  • 原则6:没有原则6.

Ken Thompson —Unix最初版本的设计者和实现者,禅宗偈语般地对Pike的原则4作了强调:

拿不准就穷举。

Unix哲学中更多的内容不是这些先哲们口头表述出来的,而是由他们所作的一切和Unix本身所作出的榜样体现出来的.从整体上来说,可以概括为以下几点:

  • 1.模块原则:使用简洁的接口拼合借口简单的部件。
  • 2.清晰原则:清晰声誉机巧。
  • 3.组合原则:设计时考虑拼接组合。
  • 4.分离原则:策略同机制分离,接口同引擎分离。
  • 5.简洁原则:设计要简洁,复杂度能低则低。
  • 6.吝啬原则:除非确无它法,不要编写庞大的程序。
  • 7.透明性原则:设计要可见,以便审查和调试。
  • 8.健壮原则:健壮源于透明与简洁。
  • 9.表示原则:把知识叠入数据以求逻辑质朴而健壮。
  • 10.通俗原则:接口设计避免标新立异。
  • 11.缄默原则:如果一个程序没什么好说的,就沉默。
  • 12.补救原则:出现异常时,马上退出并给出足够错误信息。
  • 13.经济原则:宁花机器一分钟,不花程序员一秒。
  • 14.生成原则:避免手工hack,尽量编写程序去生成程序。
  • 15.优化原则:雕琢前先要有原则,跑之前先学会走。
  • 16.多样原则:绝不相信所谓“不二法门”的断言。
  • 17.扩展原则:设计着眼未来,未来总比预想来得快。

正如Brian Kernighan曾经说过的:“计算机编程的本质就是控制复杂度”

汇编语言、编译语言、流程图、过程化编程、结构化编程、所谓的人工智能、第四代编程语言、面向对象、以及软件开发的方法论,不计其数的解决之道被抛售者吹得神乎其神。但实际上这些用处都不大,原因恰恰在于它们“成功”地将程序的复杂度提升到了人脑几乎不能处理的地步。就像Fredbrooks的一句名言[Brooks]:没有万能药

永远不要去吃力地解读一段晦涩的代码三次。第一次也许侥幸成功,但如果发现必须重新解读一遍--离第一次太久了、具体细节无从回想--那么你该注释代码了,这样第三次就相对不会那么痛苦了。
--Henry Spencer

“错综复杂的美妙事务”听来自相矛盾。Unix程序员相互比的是谁能够做到“简洁而漂亮”并以此为荣,这一点虽然只是隐含在这些规则之中,但还是很值得公开提出来强调一下。
--Doug Mcllroy

最小立异原则的另一面是避免表象相似而实际略有不同。这回极端危险,因为表象相似往往导致人们产生错误的假定。所以最好不要让不同事务有明显区别,而不要看起来几乎一模一样。
--Henry Spencer

由于略微不同的一些原因,Donald Knuth(程序设计领域中屈指可数的经典著作之一《计算机程序设计艺术》的作者)广为传播普及了这样的观点:“过早优化是万恶之源”。他是对的。
完整的句子是这样的:“97%的时间里,我们不应该考虑蝇头小利的效率提升:过早优化是万恶之源”。Knuth自称这一观点来自C.A.R.Hoare。

在Unix世界里,有一个非常明确的悠久传统(例证之一是Rob Pike以上的评论,另一个是Ken Thompson关于穷举法的格言):先制作原型,再精雕细琢。优化之前先确保能用。或者:先能走,再学跑。“极限编程”宗师Kent Beck从另一种不同的文化将这一点有效地扩展为:先求运行,再求正确,最后求快

我最有成效的一天就是扔掉了1000行代码。 --Ken Thompson

所有的Unix哲学浓缩为一条铁律,那就是各地编程大师们奉为圭臬的“KISS”原则:

K.I.S.S.

Keep It Simple,Stupid!

  • 只要可行,一切都应该做成与来源和目标无关的过滤器。
  • 数据流应尽可能文本化(这样可以使用标准工具来查看和过滤)。
  • 数据库部署和应用协议应尽可能文本化(让人可以阅读和编辑)。
  • 复杂的前端(用户界面)和后端应该泾渭分明。
  • 如果可能,用C编写前,先用解释性语言搭建原型。
  • 当且仅当只用一门语言编程会提高程序复杂度时,混用语言编程才比单一语言编程来得好。
  • 宽收严发(对接收的东西要包容,对输出的东西要严格)。
  • 过滤时,不需要丢弃的信息决不丢。
  • 小就是美。在确保完成任务的基础上,程序功能尽可能少。

Hexo 果然比Jeklly 漂亮许多,使用了NexT的模版,以后可以愉快的写blog了~