软件开发中的原则
在软件开发中,前人对软件系统的设计和开发总结了一些原则和模式, 不管用什么语言做开发,都将对我们系统设计和开发提供指导意义。本文主要将总结这些常见的原则,和具体阐述意义。
面向对象设计原则 SOLID
面向对象设计OOD(Object-Oriented Design)
| 缩写 | 英文名称 | 中文名称 |
|---|---|---|
| SRP | Single Responsibility Principle | 单一职责原则 |
| OCP | Open Close Principle | 开闭原则 |
| LSP | Liskov Substitution Principle | 里氏替换原则 |
| LoD | Law of Demeter ( Least Knowledge Principle) | 迪米特法则(最少知道原则) |
| ISP | Interface Segregation Principle | 接口分离原则 |
| DIP | Dependency Inversion Principle | 依赖倒置原则 |
- 单一功能原则(Single Responsibility Principle)
每一个类应该专注于做一件事情。
- 开闭原则(Open Close Principle)
面向扩展开放,面向修改关闭。
- 里氏替换原则(Liskov Substitution Principle)
超类存在的地方,子类是可以替换的。
- 接口隔离原则(Interface Segregation Principle)
应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。
- 依赖反转原则(Dependence Inversion Principle)
实现尽量依赖抽象,不依赖具体实现。
- 迪米特法则 得墨忒耳定律(Law Of Demeter)
又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。
- 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。
合成复用原则是指:尽量使用合成 / 聚合的方式,而不是使用继承。
追求高内聚highly cohesive 和松耦合 loosely couple 的解决方案是面向对象设计基本核心原则。
SOLID细则
单一职责原则(Single Responsibility Principle)
因为:
可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性,提高系统的可维护性;变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
所以:
从大局上看Android中的Paint和Canvas等类都遵守单一职责原则,Paint和Canvas各司其职。
Single-Responsibility Principle, 一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合,高内聚在面向对象原则的引申,将职责定义为引起变化的原因,以提高内聚性减少引起变化的原因。
定义
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。(Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class.),即又定义有且仅有一个原因使类变更。
原则分析
- 一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
- 类的职责主要包括两个方面: 数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。
- 单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
优点
- 降低类的复杂性,类的职责清晰明确。比如数据职责和行为职责清晰明确。
- 提高类的可读性和维护性,
- 变更引起的风险减低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
注意: 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否合理,但是“职责”和“变化原因”都是没有具体标准的,一个类到底要负责那些职责? 这些职责怎么细化? 细化后是否都要有一个接口或类? 这些都需从实际的情况考虑。因项目而异,因环境而异。
例子
SpringMVC 中Entity,DAO,Service,Controller, Util等的分离。
开闭原则(Open Close Principle)
因为:
开放封闭原则主要体现在对扩展开放、对修改封闭,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。软件需求总是变化的,世界上没有一个软件的是不变的,因此对软件设计人员来说,必须在不需要对原有系统进行修改的情况下,实现灵活的系统扩展。
所以:
可以通过Template Method模式和Strategy模式进行重构,实现对修改封闭,对扩展开放的设计思路。 封装变化,是实现开放封闭原则的重要手段,对于经常发生变化的状态,一般将其封装为一个抽象,拒绝滥用抽象,只将经常变化的部分进行抽象。
Open - ClosedPrinciple ,OCP, 对扩展开放,对修改关闭(设计模式的核心原则)
定义
一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭. 意思是,在一个系统或者模块中,对于扩展是开放的,对于修改是关闭的,一个 好的系统是在不修改源代码的情况下,可以扩展你的功能. 而实现开闭原则的关键就是抽象化.
原则分析
- 当软件实体因需求要变化时, 尽量通过扩展已有软件实体,可以提供新的行为,以满足对软件的新的需求,而不是修改已有的代码,使变化中的软件有一定的适应性和灵活性 。已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。
- 实现开闭原则的关键就是抽象化 :在"开-闭"原则中,不允许修改的是抽象的类或者接口,允许扩展的是具体的实现类,抽象类和接口在"开-闭"原则中扮演着极其重要的角色..即要预知可能变化的需求.又预见所有可能已知的扩展..所以在这里"抽象化"是关键!
- 可变性的封闭原则:找到系统的可变因素,将它封装起来. 这是对"开-闭"原则最好的实现. 不要把你的可变因素放在多个类中,或者散落在程序的各个角落. 你应该将可变的因素,封套起来..并且切忌不要把所用的可变因素封套在一起. 最好的解决办法是,分块封套你的可变因素!避免超大类,超长类,超长方法的出现!!给你的程序增加艺术气息,将程序艺术化是我们的目标!
例子
设计模式中模板方法模式和观察者模式都是开闭原则的极好体现。
里氏替换原则(Liskov Substitution Principle)
因为:
里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
所以:
使用里氏替换原则时需要注意,子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
从大局看Java的多态就属于这个原则。
Liskov Substitution Principle ,LSP: 任何基类可以出现的地方,子类也可以出现;这一思想表现为对继承机制的约束规范,只有子类能够替换其基类时,才能够保证系统在运行期内识别子类,这是保证继承复用的基础。
定义
第一种定义方式相对严格: 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
第二种更容易理解的定义方式: 所有引用基类(父类)的地方必须能透明地使用其子类的对象。即子类能够必须能够替换基类能够从出现的地方。子类也能在基类 的基础上新增行为。 (里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士、麻省理工学院教授BarbaraLiskov和卡内基.梅隆大学Jeannette Wing教授于1994年提出。其原文如下: Let q(x) be a property provableabout objects x of type T. Then q(y) should be true for objects y of type Swhere S is a subtype of T. )
原则分析
- 讲的是基类和子类的关系,只有这种关系存在时,里氏代换原则才存在。正方形是长方形是理解里氏代换原则的经典例子。
- 里氏代换原则可以通俗表述为: 在软件中如果能够使用基类对象,那么一定能够使用其子类对象。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。
- 里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
接口隔离原则(Interface Segregation Principle)
因为:
提供尽可能小的单独接口,而不要提供大的总接口。暴露行为让后面的实现类知道的越少越好。譬如类ProgramMonkey通过接口CodeInterface依赖类CodeC,类ProgramMaster通过接口CodeInterface依赖类CodeAndroid,如果接口CodeInterface对于类ProgramMonkey和类CodeC来说不是最小接口,则类CodeC和类CodeAndroid必须去实现他们不需要的方法。将臃肿的接口CodeInterface拆分为独立的几个接口,类ProgramMonkey和类ProgramMaster分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
所以:
建立单一接口,不要建立庞大的接口,尽量细化接口,接口中的方法尽量少。也就是要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的约定,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
从大局来说Java的接口可以实现多继承就是接口隔离原则的基础保障。
(Interface Segregation Principle,ISL): 客户端不应该依赖那些它不需要的接口。(这个法则与迪米特法则是相通的)
定义
客户端不应该依赖那些它不需要的接口。
另一种定义方法: 一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。 注意,在该定义中的接口指的是所定义的方法。例如外面调用某个类的public方法。这个方法对外就是接口。
原则分析:
- 接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。
- 一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。
- 接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。
- 使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。
- 可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。
依赖倒置原则(Dependence Inversion Principle)
因为:
具体依赖抽象,上层依赖下层。假设B是较A低的模块,但B需要使用到A的功能,这个时候,B不应当直接使用A中的具体类;而应当由B定义一抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口;这样就达到了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能造成循环依赖。
所以:
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。
从大局看Java的多态就属于这个原则。
Dependency-Inversion Principle 要依赖抽象,而不要依赖具体的实现, 具体而言就是高层模块不依赖于底层模块,二者共同依赖于抽象。抽象不依赖于具体,具体依赖于抽象。
定义
高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。简单的说,依赖倒置原则要求客户端依赖于抽象耦合。原则表述:
抽象不应当依赖于细节;细节应当依赖于抽象;
要针对接口编程,不针对实现编程。
原则分析
- 如果说开闭原则是面向对象设计的目标,依赖倒转原则是到达面向设计"开闭"原则的手段..如果要达到最好的"开闭"原则,就要尽量的遵守依赖倒转原则. 可以说依赖倒转原则是对"抽象化"的最好规范! 我个人感觉,依赖倒转原则也是里氏代换原则的补充..你理解了里氏代换原则,再来理解依赖倒转原则应该是很容易的。
- 依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中。
- 类之间的耦合: 零耦合关系,具体耦合关系,抽象耦合关系。依赖倒转原则要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒转原则的关键。
例子
理解这个依赖倒置,首先我们需要明白依赖在面向对象设计的概念:
依赖关系(Dependency): 是一种使用关系,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系。(假设A类的变化引起了B类的变化,则说名B类依赖于A类。)大多数情况下,依赖关系体现在某个类的方法使用另一个类的对象作为参数。在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。
例子: 某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。

由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类MainClass都需要修改源代码,以便使用新的类,但违背了开闭原则。现使用依赖倒转原则对其进行重构。

- 当然根据具体的情况,也可以将AbstractSource注入到AbstractStransformer,依赖注入的方式有以下三种:
/**
* 依赖注入是依赖AbstractSource抽象注入的,而不是具体
* DatabaseSource
*
*/
abstract class AbstractStransformer {
private AbstractSource source;
/**
* 构造注入(Constructor Injection): 通过构造函数注入实例变量。
*/
public void AbstractStransformer(AbstractSource source){
this.source = source;
}
/**
* 设值注入(Setter Injection): 通过Setter方法注入实例变量。
* @param source : the sourceto set
*/
public void setSource(AbstractSource source) {
this.source = source;
}
/**
* 接口注入(Interface Injection): 通过接口方法注入实例变量。
* @param source
*/
public void transform(AbstractSource source ) {
source.getSource();
System.out.println("Stransforming ...");
}
}
迪米特法则(Law Of Demeter)
因为:
类与类之间的关系越密切,耦合度也就越来越大,只有尽量降低类与类之间的耦合才符合设计模式;对于被依赖的类来说,无论逻辑多复杂都要尽量封装在类的内部;每个对象都会与其他对象有耦合关系,我们称出现成员变量、方法参数、方法返回值中的类为直接的耦合依赖,而出现在局部变量中的类则不是直接耦合依赖,也就是说,不是直接耦合依赖的类最好不要作为局部变量的形式出现在类的内部。
所以:
一个对象对另一个对象知道的越少越好,即一个软件实体应当尽可能少的与其他实体发生相互作用,在一个类里能少用多少其他类就少用多少,尤其是局部变量的依赖类,能省略尽量省略。同时如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一方法的话,可以通过第三者转发这个调用。
从大局来说Android App开发中的多Fragment与依赖的Activity间交互通信遵守了这一法则。
Law of Demeter,LoD: 系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度
定义
又叫最少知识原则(Least Knowledge Principle或简写为LKP)几种形式定义:
- 不要和“陌生人”说话。英文定义为: Don't talk to strangers.
- 只与你的直接朋友通信。英文定义为: Talk only to your immediate friends.
- 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
简单地说,也就是,一个对象应当对其它对象有尽可能少的了解。一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public方法,我就调用这么多,其他的一概不关心。
法则分析
- 朋友类:
在迪米特法则中,对于一个对象,其朋友包括以下几类:
- 当前对象本身(this);
- 以参数形式传入到当前对象方法中的对象;
- 当前对象的成员对象;
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
- 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
- 狭义法则和广义法则:
在狭义的迪米特法则中,如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
狭义的迪米特法则: 可以降低类之间的耦合,但是会在系统中增加大量的小方法并散落在系统的各个角落,它可以使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联,但是也会造成系统的不同模块之间的通信效率降低,使得系统的不同模块之间不容易协调。
广义的迪米特法则: 指对对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制。信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用和修改,同时可以促进软件的复用,由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显。
- 迪米特法则的主要用途: 在于控制信息的过载。
- 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
- 在类的设计上,只要有可能,一个类型应当设计成不变类;
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
例子
外观模式Facade(结构型)
迪米特法则与设计模式Facade模式、Mediator模式
系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度,因为在你的系统中,扩展的时候,你可能需要修改这些类,而类与类之间的关系,决定了修改的复杂度,相互作用越多,则修改难度就越大,反之,如果相互作用的越小,则修改起来的难度就越小..例如A类依赖B类,则B类依赖C类,当你在修改A类的时候,你要考虑B类是否会受到影响,而B类的影响是否又会影响到C类. 如果此时C类再依赖D类的话,呵呵,我想这样的修改有的受了。
组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
因为:
其实整个设计模式就是在讲如何类与类之间的组合/聚合。在一个新的对象里面通过关联关系(包括组合关系和聚合关系)使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用其已有功能的目的。也就是,要尽量使用类的合成复用,尽量不要使用继承。
如果为了复用,便使用继承的方式将两个不相干的类联系在一起,违反里氏代换原则,哪是生搬硬套,忽略了继承了缺点。继承复用破坏数据封装性,将基类的实现细节全部暴露给了派生类,基类的内部细节常常对派生类是透明的,白箱复用;虽然简单,但不安全,不能在程序的运行过程中随便改变;基类的实现发生了改变,派生类的实现也不得不改变;从基类继承而来的派生类是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。
所以:
组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
(Composite/Aggregate ReusePrinciple ,CARP): 要尽量使用对象组合,而不是继承关系达到软件复用的目的
定义
经常又叫做合成复用原则(Composite ReusePrinciple或CRP),尽量使用对象组合,而不是继承来达到复用的目的。
就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。简而言之,要尽量使用合成/聚合,尽量不要使用继承。
原则分析:
- 在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承。
- 继承复用: 实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。(“白箱”复用)
- 组合/聚合复用: 耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。(“黑箱”复用)
- 组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
- 此原则和里氏代换原则氏相辅相成的,两者都是具体实现"开-闭"原则的规范。违反这一原则,就无法实现"开-闭"原则,首先我们要明白合成和聚合的概念:
注意: 聚合和组合的区别是什么?
合成(组合): 表示一个整体与部分的关系,指一个依托整体而存在的关系(整体与部分不可以分开);比如眼睛和嘴对于头来说就是组合关系,没有了头就没有眼睛和嘴,它们是不可分割的。在UML中,组合关系用带实心菱形的直线表示。
聚合:聚合是比合成关系的一种更强的依赖关系,也表示整体与部分的关系(整体与部分可以分开);比如螺丝和汽车玩具的关系,螺丝脱离玩具依然可以用在其它设备之上。在UML中,聚合关系用带空心菱形的直线表示。
11大原则:
面向对象的基本原则(solid)是五个,但是在经常被提到的除了这五个之外还有
迪米特法则和合成复用原则等, 所以在常见的文章中有表示写六大或七大原则的。除此之外还有一些其它相关书籍和互联网上出现的原则;
1. 单一职责原则(Single Responsibility Principle)
不应该有超过一个理由去修改类,因为一个类只能有一个职责功能,如果你将多于一个功能增加到一个类中,相当于在两个功能之间引入了紧耦合。
因为可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性,提高系统的可维护性;变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
所以从大局上看Android中的Paint和Canvas等类都遵守单一职责原则,Paint和Canvas各司其职。
2. 开闭原则(Open Close Principle)
类,方法或函数应该对扩展开放(新功能)和对修改关闭。这又是一个漂亮的面向对象的设计原则,防止对已经测试过的代码尝试修改。
因为开放封闭原则主要体现在对扩展开放、对修改封闭,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。软件需求总是变化的,世界上没有一个软件的是不变的,因此对软件设计人员来说,必须在不需要对原有系统进行修改的情况下,实现灵活的系统扩展。
所以可以通过Template Method模式和Strategy模式进行重构,实现对修改封闭,对扩展开放的设计思路。 封装变化,是实现开放封闭原则的重要手段,对于经常发生变化的状态,一般将其封装为一个抽象,拒绝滥用抽象,只将经常变化的部分进行抽象。
3. 里氏替换原则(Liskov Substitution Principle)
根据里氏替换原则,子类型必须是可替代超类型,即方法或函数,它使用超类的类型必须能够与子类的对象时一样没有任何问题,如果一个类比子类有更多的功能,而子类可能不支持某些功能,这没有违反LSP。为了遵循LSP的设计原理,派生类或子类必须是增强功能不是减少它。
因为里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
所以使用里氏替换原则时需要注意,子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
从大局看Java的多态就属于这个原则。
4. 接口隔离原则(Interface Segregation Principle)
接口隔离原则要求:客户端不应该实现它不使用的接口,。当一个接口包含一个以上的功能而客户端只需要一个功能时容易出现这种情况,因为一旦你释放你的接口,你就不能改变它。
因为提供尽可能小的单独接口,而不要提供大的总接口。暴露行为让后面的实现类知道的越少越好。譬如类ProgramMonkey通过接口CodeInterface依赖类CodeC,类ProgramMaster通过接口CodeInterface依赖类CodeAndroid,如果接口CodeInterface对于类ProgramMonkey和类CodeC来说不是最小接口,则类CodeC和类CodeAndroid必须去实现他们不需要的方法。将臃肿的接口CodeInterface拆分为独立的几个接口,类ProgramMonkey和类ProgramMaster分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
所以建立单一接口,不要建立庞大的接口,尽量细化接口,接口中的方法尽量少。也就是要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的约定,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
从大局来说Java的接口可以实现多继承就是接口隔离原则的基础保障。
5. 依赖倒置原则(Dependence Inversion Principle)
不要主动要求依赖,因为它已经由框架提供给你。比如Spring框架等,这样的设计原则妙处在于,它是由DI注入框架的注入匹配需要的类,这样更容易进行测试维护,因为创建对象的代码都集中在框架,而客户端代码是不参与的。
因为具体依赖抽象,上层依赖下层。假设B是较A低的模块,但B需要使用到A的功能,这个时候,B不应当直接使用A中的具体类;而应当由B定义一抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口;这样就达到了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能造成循环依赖。
所以采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。
从大局看Java的多态就属于这个原则。
控制反转(IoC)与依赖注入(DI)
https://blog.51cto.com/zhangjunhd/126530?source=drt
依赖->依赖倒置->控制反转->依赖注入
依赖注入(原来是对象里面用到的对象直接创建,现在是用的时候在传对象,感觉也没啥高级的) 目的:为了解耦、保证代码的健壮、灵活、可维护
- 构造函数注入
- set方法注入
- 接口方式注入
JSR-330 (JAVA 依赖注入的标准)
6. 迪米特法则(Law Of Demeter) 最少知道原则
因为类与类之间的关系越密切,耦合度也就越来越大,只有尽量降低类与类之间的耦合才符合设计模式;对于被依赖的类来说,无论逻辑多复杂都要尽量封装在类的内部;每个对象都会与其他对象有耦合关系,我们称出现成员变量、方法参数、方法返回值中的类为直接的耦合依赖,而出现在局部变量中的类则不是直接耦合依赖,也就是说,不是直接耦合依赖的类最好不要作为局部变量的形式出现在类的内部。
所以一个对象对另一个对象知道的越少越好,即一个软件实体应当尽可能少的与其他实体发生相互作用,在一个类里能少用多少其他类就少用多少,尤其是局部变量的依赖类,能省略尽量省略。同时如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一方法的话,可以通过第三者转发这个调用。
从大局来说Android App开发中的多Fragment与依赖的Activity间交互通信遵守了这一法则。
7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
组合胜过于继承
如果可能的话组合composition总是胜过继承。组合比继承拥有更多的灵活性。组合允许在运行时设置属性,并通过使用接口来实现,我们可以使用多态在类运行时改变类的行为,从而提供更好的接口实现。
因为其实整个设计模式就是在讲如何类与类之间的组合/聚合。在一个新的对象里面通过关联关系(包括组合关系和聚合关系)使用一些已有的对象,使之成为新对象的一部分,新对象通过委派调用已有对象的方法达到复用其已有功能的目的。也就是,要尽量使用类的合成复用,尽量不要使用继承。如果为了复用,便使用继承的方式将两个不相干的类联系在一起,违反里氏代换原则,哪是生搬硬套,忽略了继承了缺点。继承复用破坏数据封装性,将基类的实现细节全部暴露给了派生类,基类的内部细节常常对派生类是透明的,白箱复用;虽然简单,但不安全,不能在程序的运行过程中随便改变;基类的实现发生了改变,派生类的实现也不得不改变;从基类继承而来的派生类是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。
所以组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
8. DRY (不要重复你自己)
这是指不写重复的代码,取而代之是使用抽象共性的东西。如果超过一次使用硬编码,那么就要考虑将其公开为final修饰的不变量;如果你在两个以上地方有相同的代码块,那么就要考虑使其成为一个单独的方法。SOLID设计原理的好处是在维护。值得注意的是重复的不是指代码,而是对于功能而言的。
9. 封装变化
在软件领域唯一不变的就是“变”,所以封装那些你估计在未来可能被改变的代码。这种设计的好处是容易测试和易于维护。如果你是进行Java编码,那么就要使变量和方法变成私有。有几个Java设计模式采用封装,工厂设计模式封装的是对象创建代码,并提供了在不改变现有的代码情况下推出新产品的灵活性。
10. 面向接口而不是实现编程
面向接口编程而不是面向实现子类,这有灵活性的好处,特别是同样接口有不同实现子类时。
11. 委托原则
不要自己做所有的事情,可以委托给相应的类去完成。
问题
面向对象设计其他原则
封装变化
少用继承 多用组合
针对接口编程 不针对实现编程
为交互对象之间的松耦合设计而努力
类应该对扩展开发 对修改封闭(开闭OCP原则)
依赖抽象,不要依赖于具体类(依赖倒置DIP原则)
密友原则: 只和朋友交谈(最少知识原则,迪米特法则)
说明: 一个对象应当对其他对象有尽可能少的了解,将方法调用保持在界限内,只调用属于以下范围的方法: 该对象本身(本地方法)对象的组件 被当作方法参数传进来的对象 此方法创建或实例化的任何对象
别找我(调用我) 我会找你(调用你)(好莱坞原则)
一个类只有一个引起它变化的原因(单一职责SRP原则)
你能解释一下里氏替换原则吗?
严格定义: 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象用o1替换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
通俗表述: 所有引用基类(父类)的地方必须能透明地使用其子类的对象。也就是说子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。 子类中可以增加自己特有的方法。 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
什么情况下会违反迪米特法则? 为什么会有这个问题?
迪米特法则建议“只和朋友说话,不要陌生人说话”,以此来减少类之间的耦合。
给我一个符合开闭原则的例子?
开闭原则要求你的代码对扩展开放,对修改关闭。这个意思就是说,如果你想增加一个新的功能,你可以很容易的在不改变已测试过的代码的前提下增加新的代码。有好几个设计模式是基于开闭原则的,如策略模式,如果你需要一个新的策略,只需要实现接口,增加配置,不需要改变核心逻辑。一个正在工作的例子是 Collections.sort() 方法,这就是基于策略模式,遵循开闭原则的,你不需为新的对象修改 sort() 方法,你需要做的仅仅是实现你自己的 Comparator 接口。
什么时候使用享元模式(蝇量模式)?
享元模式通过共享对象来避免创建太多的对象。为了使用享元模式,你需要确保你的对象是不可变的,这样你才能安全的共享。JDK 中 String 池、Integer 池以及 Long 池都是很好的使用了享元模式的例子。
低内聚高耦合
要理解“低内聚高耦合”,首先需要明确两个核心概念的定义,再通过具体案例拆解其问题——低内聚指模块/功能内部职责混乱、关联性弱(“一件事没做好,还管了一堆闲事”);高耦合指模块/功能之间依赖过强,一个模块的修改会直接影响多个其他模块(“牵一发而动全身”)。
一、先明确:内聚与耦合的核心区别
| 维度 | 内聚(模块内部) | 耦合(模块之间) |
|---|---|---|
| 关注焦点 | 模块自身职责的“集中度” | 模块之间依赖的“紧密程度” |
| 理想状态 | 高内聚(一个模块只做一件事) | 低耦合(模块间依赖少、可独立) |
| 问题状态 | 低内聚(模块职责混乱、杂糅) | 高耦合(模块间绑定紧、难修改) |
二、低内聚高耦合:具体案例(以“电商订单处理”为例)
假设某电商系统早期设计时,为了“图方便”将多个不相关功能塞进一个模块,同时模块间直接硬依赖,形成典型的“低内聚高耦合”结构,具体如下:
1. 案例背景:一个混乱的“订单大模块”
开发团队将“订单创建、库存扣减、支付处理、物流通知、用户积分更新”全部写在一个名为 OrderService 的类(模块)中,代码结构简化如下:
// 低内聚的OrderService:一个模块干了5件不相关的事
public class OrderService {
// 1. 创建订单
public void createOrder(Order order) {
// 订单数据存入数据库
}
// 2. 扣减库存(本应是库存模块的职责)
public void reduceStock(Order order) {
// 直接操作库存数据库表
}
// 3. 处理支付(本应是支付模块的职责)
public void handlePayment(Order order) {
// 直接调用支付宝API(硬编码依赖)
}
// 4. 发送物流通知(本应是通知模块的职责)
public void sendLogisticsNotice(Order order) {
// 直接调用短信接口(硬编码依赖)
}
// 5. 更新用户积分(本应是用户模块的职责)
public void updateUserPoints(Order order) {
// 直接操作用户积分表
}
// 核心流程:创建订单时,一次性调用所有方法
public void processOrder(Order order) {
createOrder(order);
reduceStock(order);
handlePayment(order);
sendLogisticsNotice(order);
updateUserPoints(order);
}
}
2. 案例拆解:低内聚的问题(模块内部职责混乱)
OrderService 的核心职责本应是“订单相关逻辑”(如创建订单、查询订单),但实际包含了库存、支付、通知、用户积分4个完全独立的职责,属于典型的“低内聚”,具体问题如下:
- 职责边界模糊:新人接手时,无法快速定位“库存扣减”的代码(因为它不在
StockService,而在OrderService); - 复用性差:如果其他场景需要“单独扣减库存”(如用户取消订单后恢复库存),无法直接复用
reduceStock方法(因为它属于OrderService,依赖订单对象); - 维护成本高:修改“积分规则”(如原本消费1元加1分,改为消费2元加1分)时,需要修改
OrderService——一个与“积分”无关的模块,容易误改其他订单逻辑。
3. 案例拆解:高耦合的问题(模块之间硬依赖)
不仅OrderService内部混乱,它还与其他系统(库存数据库、支付宝API、短信接口)直接“硬耦合”,具体问题如下:
- 依赖不可替换:如果公司从“支付宝”切换为“微信支付”,需要修改
OrderService中的handlePayment方法(硬编码了支付宝API地址),甚至可能影响processOrder的核心流程; - 故障传导性强:若短信接口临时故障,
sendLogisticsNotice方法报错,会直接导致整个processOrder流程中断——用户无法创建订单(即使库存、支付都正常); - 无法独立测试:测试“订单创建”功能时,必须同时启动库存数据库、支付宝测试环境、短信测试接口(因为
OrderService直接依赖它们),否则测试无法运行。
三、反例:高内聚低耦合的优化方案
为解决上述问题,需按“单一职责”拆分模块,并通过“接口”降低依赖,优化后结构如下:
| 模块(类) | 核心职责(高内聚) | 与其他模块的交互(低耦合) |
|---|---|---|
OrderService | 仅处理订单逻辑(创建、查询、取消) | 依赖StockService、PaymentService等接口(而非具体实现) |
StockService | 仅处理库存逻辑(扣减、恢复) | 提供reduceStock接口,不依赖其他模块 |
PaymentService | 仅处理支付逻辑(支付宝、微信支付) | 提供processPayment接口,内部适配不同支付渠道 |
NoticeService | 仅处理通知逻辑(短信、推送) | 提供sendNotice接口,内部适配不同通知渠道 |
UserPointService | 仅处理积分逻辑(增加、扣减) | 提供updatePoints接口,独立维护积分规则 |
优化后优势:
- 修改“支付渠道”时,仅需修改
PaymentService,不影响OrderService; - 短信接口故障时,
NoticeService返回“通知待发送”,OrderService可正常创建订单(后续重试通知即可); - 测试“订单创建”时,可通过“模拟接口”(如模拟
StockService返回“库存充足”),无需依赖真实库存数据库。
四、低内聚高耦合的常见场景总结
除了上述电商案例,以下场景也常出现类似问题:
- Excel报表工具:一个Excel模板中,既包含“数据统计逻辑”(如计算销售额),又包含“格式美化逻辑”(如单元格颜色、字体),还包含“数据导出逻辑”——修改字体颜色时需调整统计代码,低内聚;
- 手机APP的“我的页面”:一个页面同时处理“用户信息展示”“订单列表加载”“优惠券查询”“消息通知”——修改“优惠券样式”时,可能影响“用户信息加载”,高耦合;
- 后端接口:一个接口(如
/user/all)同时返回“用户基本信息”“用户订单列表”“用户收货地址”——前端仅需“用户姓名”时,也会加载大量无用数据,且修改“订单列表字段”需调整该接口,低内聚高耦合。
核心结论
低内聚高耦合的本质是“设计时未明确职责边界”,导致模块“既不专注,又不独立”。这种设计在短期开发时可能快速上线,但随着系统迭代,会出现“改一处崩一片”“复用难、测试难”的问题,是系统长期维护的“隐形炸弹”。
怎样进行代码重构来改善低内聚高耦合的问题?
改善低内聚高耦合的代码重构,核心是通过拆分职责(提升内聚)和解除强依赖(降低耦合),让代码模块“职责单一、独立可维护”。以下是具体的重构步骤和实践方法,结合案例说明:
一、重构核心原则
在动手前需明确两个指导原则:
- 高内聚:一个模块(类/函数)只做一件事,内部逻辑紧密相关(比如“订单模块”只处理订单的创建、查询,不碰库存)。
- 低耦合:模块之间通过“接口”或“约定”通信,不直接依赖具体实现(比如订单模块调用“库存接口”,而非直接操作库存数据库)。
二、具体重构步骤(以“低内聚的订单模块”为例)
以下以之前提到的OrderService为例,演示如何从“低内聚高耦合”重构为“高内聚低耦合”。
步骤1:拆分职责,提升内聚(解决“低内聚”)
问题:OrderService包含订单、库存、支付、通知、积分5个不相关职责。
方案:按“单一职责”拆分为独立模块,每个模块只负责自己的核心逻辑。
// 1. 订单模块:只处理订单本身的逻辑
public class OrderService {
// 依赖其他模块的接口(暂不实现,见步骤2)
private StockService stockService;
private PaymentService paymentService;
private NoticeService noticeService;
private UserPointService userPointService;
// 核心职责:创建订单(仅处理订单数据)
public Order createOrder(OrderDTO orderDTO) {
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setStatus(OrderStatus.CREATED);
// 保存订单到数据库(仅订单相关操作)
return orderRepository.save(order);
}
// 流程编排:调用其他模块完成整个下单流程(但不自己实现具体逻辑)
public void processOrder(OrderDTO orderDTO) {
Order order = createOrder(orderDTO);
// 调用拆分后的模块(通过接口,而非硬编码)
stockService.reduceStock(order.getProductId(), orderDTO.getQuantity());
paymentService.processPayment(order.getId(), orderDTO.getAmount());
noticeService.sendDeliveryNotice(order.getUserId(), order.getId());
userPointService.addPoints(order.getUserId(), calculatePoints(orderDTO.getAmount()));
}
}
// 2. 库存模块:只处理库存逻辑
public class StockService {
public void reduceStock(Long productId, int quantity) {
// 仅处理库存扣减逻辑
}
}
// 3. 支付模块:只处理支付逻辑
public class PaymentService {
public void processPayment(Long orderId, BigDecimal amount) {
// 仅处理支付逻辑(后续优化见步骤2)
}
}
// 4. 通知模块:只处理通知逻辑
public class NoticeService {
public void sendDeliveryNotice(Long userId, Long orderId) {
// 仅处理通知逻辑
}
}
// 5. 积分模块:只处理积分逻辑
public class UserPointService {
public void addPoints(Long userId, int points) {
// 仅处理积分逻辑
}
}
效果:每个模块职责清晰,修改库存逻辑只需改StockService,不会影响订单核心逻辑。
步骤2:解除强依赖,降低耦合(解决“高耦合”)
问题:原代码直接在OrderService中硬编码支付宝API、短信接口等,导致依赖不可替换。
方案:通过“接口抽象”“依赖注入”“策略模式”等方式,让模块依赖“抽象”而非“具体实现”。
方法1:用接口隔离具体实现
// 定义支付接口(抽象)
public interface PaymentProvider {
void pay(Long orderId, BigDecimal amount);
}
// 支付宝实现(具体)
public class AlipayProvider implements PaymentProvider {
@Override
public void pay(Long orderId, BigDecimal amount) {
// 调用支付宝API
}
}
// 微信支付实现(具体)
public class WechatPayProvider implements PaymentProvider {
@Override
public void pay(Long orderId, BigDecimal amount) {
// 调用微信支付API
}
// 修改支付模块,依赖接口而非具体实现
public class PaymentService {
// 通过接口引用,不关心具体是支付宝还是微信
private PaymentProvider paymentProvider;
// 构造函数注入(由外部决定用哪种支付方式)
public PaymentService(PaymentProvider paymentProvider) {
this.paymentProvider = paymentProvider;
}
public void processPayment(Long orderId, BigDecimal amount) {
paymentProvider.pay(orderId, amount); // 调用接口方法
}
}
方法2:用依赖注入(DI)管理依赖
通过Spring等框架,由外部容器管理模块之间的依赖,而非模块自己创建依赖:
// OrderService不再自己创建StockService,而是由框架注入
public class OrderService {
// 声明依赖(接口)
private final StockService stockService;
private final PaymentService paymentService;
// 构造函数注入(依赖由外部传入)
public OrderService(StockService stockService, PaymentService paymentService) {
this.stockService = stockService;
this.paymentService = paymentService;
}
}
效果:切换支付方式时,只需修改PaymentService的注入参数(如从AlipayProvider换成WechatPayProvider),OrderService完全不用改。
步骤3:消除冗余代码,提取公共逻辑
问题:低内聚代码中常出现重复逻辑(如多个模块都有“日期格式化”“参数校验”代码)。
方案:将重复逻辑提取为工具类或公共模块。
// 提取公共工具类(如日期处理)
public class DateUtils {
public static String format(Date date, String pattern) {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
return sdf.format(date);
}
}
// 所有模块统一调用工具类,而非各自实现
public class OrderService {
public void createOrder(OrderDTO orderDTO) {
String createTime = DateUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
// ... 其他逻辑
}
}
public class StockService {
public void reduceStock(...) {
String operateTime = DateUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
// ... 其他逻辑
}
}
步骤4:通过“中间层”解耦跨模块依赖
问题:如果模块A直接调用模块B的方法,当B修改时A可能受影响(如B的方法名变了)。
方案:引入“中间层”(如事件总线、消息队列),让模块通过“事件”通信,而非直接调用。
// 1. 定义订单创建事件
public class OrderCreatedEvent {
private Long orderId;
private Long userId;
// 其他订单相关字段
}
// 2. 订单模块发布事件(不关心谁处理)
public class OrderService {
private EventBus eventBus; // 事件总线(中间层)
public void createOrder(OrderDTO orderDTO) {
Order order = saveOrder(orderDTO);
// 发布事件,不直接调用库存、支付等模块
eventBus.publish(new OrderCreatedEvent(order.getId(), order.getUserId()));
}
}
// 3. 库存模块订阅事件(主动接收通知)
public class StockService {
@Subscribe // 订阅事件
public void handleOrderCreated(OrderCreatedEvent event) {
reduceStock(event.getOrderId()); // 处理库存扣减
}
}
// 4. 支付模块订阅事件
public class PaymentService {
@Subscribe
public void handleOrderCreated(OrderCreatedEvent event) {
processPayment(event.getOrderId()); // 处理支付
}
}
效果:订单模块完全不知道库存、支付模块的存在,即使库存模块改了方法名,只要能处理OrderCreatedEvent,订单模块就不受影响。
三、重构后的检查清单
重构后可通过以下标准验证是否改善:
- 内聚检查:每个模块的方法是否都围绕一个核心职责?(如
StockService的所有方法都和库存相关) - 耦合检查:修改一个模块时,是否需要同时修改其他模块?(理想状态:修改
PaymentService不影响OrderService) - 复用性检查:某个模块的功能(如
UserPointService的积分计算)能否直接复用到其他场景(如签到送积分)? - 测试性检查:能否单独测试一个模块(如用Mock模拟
StockService,测试OrderService的流程)?
四、重构注意事项
- 小步迭代:不要试图一次性重构整个系统,先从最混乱的模块开始(如频繁出bug的部分),每次重构后跑通测试再提交。
- 保留测试:重构前确保有完善的单元测试,避免重构时引入新bug(“没有测试的重构都是耍流氓”)。
- 避免过度设计:拆分模块不是越多越好,过度拆分可能导致代码碎片化(如一个简单功能拆成10个类)。
通过以上步骤,代码会逐渐从“混乱的一团”变得“职责清晰、依赖松散”,后续维护和迭代的效率会显著提升。核心思路是:让每个模块“专注自己的事”,并“友好地和其他模块合作”。