软件设计是从需求规格说明书开始,根据需求分析阶段确定的功能设计软件系统的整体结构、划分功能模块、确定每个模块的实现算法以及编写具体的代码,形成软件最终的软件产品.软件设计是把许多事物和问题抽象出来。将问题或事物分解并且模块化使得解决问题变得容易,同时分解的越细模块数量也就越多,这带来的副作用就是使得设计者需要考虑更多的模块之间耦合度的情况。
软件设计包括软件的结构设计,数据设计,接口设计和过程设计.软件是对真实世界的抽象,是对某种思想的总结和呈现.思想和对真实世界的看法是不断变化的,因此软件是会不断变化的.软件的设计为了让这些变化变得更加容易,成本更低而总结的一套方法.
2 设计原则
软件设计的原则主要是解耦各个模块之间的关系,这里需要明确一点,软件设计出来是给人看的,解耦是指软件在开发阶段的时候对于其他模块的依赖,不是在运行阶段彻底分离.如果在运行阶段也彻底分离那就说明这个模块在此系统中没有任何作用可以去掉了.
一个优秀的设计可以应对软件的变化,可以方便的对现有软件进行扩展,可以很容易的对现有程序进行修改,可以很容易理解软件各个模块之间的关系.将程序修改成本降到最低.为此业界各个大神总结了一套软件设计的原则.
2.1职责单一原则
只因为一种需求变化引起本模块的修改.如果有多重需求的变化都会引起此模块的变化,那么就应该考虑将模块进行分离,重新设计.职责单一原则的好处1,降低类的复杂度,实现什么功能很明确.2,提高可读性.3,提高可维护性,因为复杂度降低和可读性提高可维护性自然就上来了.4,变更引起的风险降低,这是单一原则最大的好处.一个接口修改只对相应的实现类有影响,与其他的接口无影响,这个是对项目有非常大的帮助.
我们看一个例子,现在手机都有拍照和播放音乐的功能,我们程序模拟手机这两功能进行设计.类图如下:
这个设计看起来没有什么问题,功能能够很好的完成.那么问题来了拍照和播放音乐在系统中有直接关系吗?很明显没有.那就说明,Mobile类的修改会有两种不同的需求变化引起.这样设计就不符合职责单一原则.修改如下:
这样修改之后手机只关心功能的接口,不在关心具体的实现,接口实现只针对具体的摸一个功能来完成.而且只有一种变化会引起实现类的修改.
职责单一原则也是比较有争议的原则,职责的定义很难有一个明确的标准.因此在项目中很难看到单一原则的影子,在实际的项目环境中,由于开发工作量大,人员技术水平以及项目紧迫程度都会导致大量的设计违背这一原则.而且原本一个类可以实现的功能可能会被拆分到多个类中实现,然后在采用聚合等方式合并到一起.这样也增加了开发工作量.所以导致了很多人放弃了此原则.
职责单一原则不仅仅是在接口和类中,同样也实用于方法.一个方法应该尽可能只完成一件单一的事情.俗话说,我简单我快乐就是这样的.比如某些方法喜欢根据参数来进行switch case然后做不同的事情,这样代码其实是非常糟糕的代码.一般的做法是将具体的case中的内容单独提取成为一个方法,然后进行调用.有判断的地方可以考虑策略等模式进行处理.
2.2里氏替换原则
基类可以出现的地方都可以用子类进行替换,而不会引起任何不适应的问题.里氏替换原则是继承复用的基石,只有当派生类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为.里氏代换原则是对“开-闭”原则的补充.实现“开-闭”原则的关键步骤就是抽象化.覆盖或实现父类的方法时,子类返回值类型可以是父类返回值类型的子类.
举个例子:现在流行的log4j日志系统,就可有很多种输出方式,可以发送邮件,可以写数据库,可以写文本.其简单设计模型如下:
客户端WriteLog类调用只依赖于抽象类Log4j.同时对于子类实例的创建,这里有很多种方式,比如工厂方法,IoC注入等等,当然也可以在Client类中直接创建,但这种方式不是很好.本例中采用的是属性注入的方式.具体代码如下:
public class Log4jDemo {
public static void main(String[] args) {
WriteLog writeLog =new WriteLog(new WriteTextLog());
writeLog.writeLog("日志");
}
}
class WriteLog
{
private Log4j log4j;
public WriteLog(Log4j log4j)
{
this.log4j=log4j;
}
public void writeLog(String log)
{
log4j.writeLog(log);
}
}
abstract class Log4j
{
public abstract void writeLog(String logInfo);
}
class WriteDatabaseLog extends Log4j
{
private String connectString;
public String getConnectString() {
return connectString;
}
public WriteDatabaseLog setConnectString(String connectString) {
this.connectString = connectString;
return this;
}
@Override
public void writeLog(String logInfo) {
System.out.println("写数据库日志");
System.out.println(logInfo);
}
}
class WriteTextLog extends Log4j
{
private String filePath;
public String getFilePath() {
return filePath;
}
public WriteTextLog setFilePath(String filePath) {
this.filePath = filePath;
return this;
}
@Override
public void writeLog(String logInfo) {
System.out.println("写文本日志");
System.out.println(logInfo);
}
}
class SendToEmail extends Log4j
{
private String emailAddr;
public String getEmailAddr() {
return emailAddr;
}
public SendToEmail setEmailAddr(String emailAddr) {
this.emailAddr = emailAddr;
return this;
}
@Override
public void writeLog(String logInfo) {
System.out.println("发送邮件");
System.out.println(logInfo);
}
}
在本例中WriteLog类中申明了Log4j抽象类,但具体依赖的对象是通过注入方式进来的.注意这里建议只使用抽象类或者接口来做.否则就回违背其他原则(比如依赖倒转原则),而且在WriteLog类中注入的类是Log4j的子类中的任意一个均可以.
里氏替换原则只能正向使用,在这里其实很好理解,子类可以有自己的独有的外观和行为.上面的例子中父类并没有定义自己的外观,仅仅定义了一个行为.每个子类中都有自己的一个独有外观.所以这里大家也应该能够看明白,子类与子类之间并没有直接联系,因此子类和子类直接无法直接替换.这也是在定义接口时必须使用父类或者接口的原因.
在里氏替换原则中还有一个问题需要考虑,那就是子类是否完整整覆盖了父类业务.如果我们子类是对日志进行分析统计,那么这个时候写日志其实就不合适了.
我们下面我们将上面的例子修改一下增加一个日志分析类,类图下:
增加的类用于分析日志中错误出现的次数,代码如下:
class AnalysisLog extends Log4j
{
@Override
public void writeLog(String logInfo) {
int count = logInfo.indexOf("错误");
System.out.println("有" + count + "条错误");
}
}
首先可以肯定这个代码是可以正常运行的,也能够得到正确的结果,但是要明白抽象类定义writeLog的方法是用于记录日志,而不是分析统计日志.因此这里的实现放在writeLog中并不合适.那么这就说明AnalysisLog类继承的Log4j类在这里并没有将业务完整覆盖.对于这种问题最简单的处理方式就是直接脱离原来的继承关系,但是脱离之后AnalysisLog类分析日志的时候还是需要日志信息,那么怎么办呢?可以增加一个抽象类,来完成日志信息获取.修改后的类图如下:
修改后的类图是在Analysis与Log4j之间增加了一个抽象类AbsAnalysisLog,将接收日志信息的功能委托出来,然后AnalysisLog和其他的写日志的类各自发展.
继承关系有一个需要注意的是:子类在继承父类之后子类的前置条件应该比父类更加宽松,举个例子:
class Father
{
public Log4j factory(Log4j log4j)
{
System.out.println("Execute father");
return log4j;
}
}
class Son extends Father
{
public Log4j factory(SendToEmail sendToEmail)
{
System.out.println("Execute son");
return sendToEmail;
}
}
class main
{
public static void main(String[] args) {
Father f=new Son();
f.factory(new SendToEmail());
System.out.println("====");
Son s=new Son();
s.factory(new SendToEmail());
}
}
输出结果如下:
这里就说明子类在参数方面缩小了父类的参数范围,这种上面的代码在实际的项目中可能会引起很多不需要麻烦.在main函数中的申明是一个Father类指向一个Son的对象.正常情况应该代用Son的factory方法.但这里执行了Father的方法,因此可能引起意想不到的Bug.所以子类中方法的前置条件必须与父类中被覆盖的方法的前置条件相同或者更宽松.上述例子中的代码其实子类和父类的的factory方面没有关系了.按照大多数的设计应该是子类方法中的参数和父类中的方法参数一样或者是父类方法中参数类型的父类.
子类在覆盖父类方法的时候输出结果可以缩小.意思就是方法返回值可以是父类方法返回值的子类.例子:
class Son2 extends Father
{
@Override
public SendToEmail factory(Log4j log4j)
{
System.out.println("execute son2");
return (SendToEmail)log4j;
}
}
class Main
{
public static void main(String[] args) {
Father f2=new Son2();
f2.factory(new SendToEmail());
}
}
执行结果如下:
里氏替换原则是为了提高程序的健壮性和兼容性.在做版本升级的时候可以有更多的选择考虑上下级兼容.增加了子类扩展了功能可以保证原来的功能不受影响.
2.3依赖倒转原则
依赖倒转原则简单来说就是高层模块不应该依赖底层模块,抽象不应该依赖细节,细节应该依赖抽象.依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多.以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多.在实际的开发中抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成.我们任然用上一节中的例子来表示类图如下:
在这里对于写日志WriteLog类来说只需要依赖于Log4j这个接口即可.根本不关心具体怎么写日志.给Log4j的任何一种实现都可以完成WriteLog中所需要的功能.
依赖倒置原则的核心思想是面向接口编程,只对契约负责.我们在举一个例子.很多人找对象都有一堆要求.比如会做饭,声音甜美,皮肤白皙,身材苗条,懂的什么叫做爱.好吧这些其实就是一个契约.符合这个契约的女人很多,给他随便哪一个都可以(不允许一夫多妻,要不然可以来多个).下面是类图:
从上面的类图中可以看到这个Man其实很可怜的,最多只有两个符合他的要求(只有两个类实现契约接口),可选余地很少.对于具体是Girl1还是Girl2对于都符合Requirement的要求.给他Girl1还是Girl2都无所谓.具体实现代码如下:
class Main1
{
public static void main(String[] args) {
Man zhangsan=new Man();
Requirement xiaohua=new Girl1();
xiaohua.setGirlName("小花");
zhangsan.setRequirementGril(xiaohua);
zhangsan.marray();
}
}
class Man
{
private Requirement requirementGril;
public Man setRequirementGril(Requirement requirementGril) {
this.requirementGril = requirementGril;
return this;
}
public void marray()
{
System.out.println("女人"+this.requirementGril.getGirlName()
+"有白皙皮肤:" + this.requirementGril.fairComplexion());
System.out.println("女人"+this.requirementGril.getGirlName()
+"有苗条身材:" + this.requirementGril.slimBody());
System.out.println("女人"+this.requirementGril.getGirlName()
+"有甜美声音:" + this.requirementGril.sweetVoice());
System.out.println("男人与" + requirementGril.getGirlName() +" 结婚");
this.requirementGril.doCook();
this.requirementGril.knownLove();
}
}
interface Requirement {
String getGirlName();
void setGirlName(String girlName);
boolean sweetVoice();
boolean fairComplexion();
boolean slimBody();
void doCook();
void knownLove();
}
abstract class AbsGirl implements Requirement
{
private String girlName;
@Override
public String getGirlName() {
return girlName;
}
@Override
public void setGirlName(String girlName) {
this.girlName = girlName;
}
@Override
public boolean sweetVoice() {
return true;
}
@Override
public boolean fairComplexion() {
return true;
}
@Override
public boolean slimBody() {
return true;
}
}
class Girl1 extends AbsGirl
{
@Override
public void doCook() {
System.out.println(getGirlName()+" 会做饭会炒菜");
}
@Override
public void knownLove() {
System.out.println(getGirlName()+" 懂什么叫做爱");
}
}
class Girl2 extends AbsGirl
{
@Override
public void doCook() {
System.out.println(getGirlName()+" 会川菜");
}
@Override
public void knownLove() {
System.out.println(getGirlName()+"知道什么叫做爱");
}
}
在使用了依赖倒转原则后,如果这个男人想换一个女人结婚,那么很简单,只需在Main1的函数中重新new一个对象即可,修改的范围控制在很小的范围.在实际的开发中这里还可以采用IoC进行注入.将具体的对象的创建放到容器中完成.
实际项目中的重要策略,决定及业务模型正是在这些高层的模块中.也正是这些模型包含着应用的特性.但是,当这些模块依赖于低层模块时,低层模块的修改将会直接影响到高层模块,迫使高层模块也去改变.这种改变是不应该出现.应该是处于高层的模块改变去迫使那些低层的模块发生改变.应该是处于高层的模块优先于低层的模块,改变应该自上而下.无论如何高层的模块也不应依赖于低层的模块.而且,我们想能够复用的是高层的模块.通过类库的形式,我们已经可以复用低层的模块了.当高层的模块依赖于低层的模块时,这些高层模块就很难在不同的环境中复用.但是,当那些高层模块独立于低层模块时,它们就能很简单地被复用了.依赖倒转原则是框架设计中的最核心之处的原则。
2.4 接口隔离原则
接口隔离原则是说应用程序不应该依赖它不需用的接口,类间的依赖关系应该建立在最小的接口上.这个原则有两层第一种解释:是不应该依赖它不需要接口,那依赖什么?依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性;第二中解释:类间的依赖关系应该建立在最小的接口上,它要求是最小的接口,也是要求接口细化,接口纯洁,与第一中解释其实是一样,只是一个事物的两种不同描述.其实总结一下这个原则就一个意思:不要建立大而全的接口.建立多个小接口,小到可能某些接口只有一个方法.这里是不是有点疑惑?和单一职责是不是有点类似?其实这个是有区别的,单一职责强调的职责单一,并没有说接口单一.一个职责可能由多个接口来完成.按照单一职责原则,可以在一个接口中定义所有完成本职责所需的接口,但按照接口隔离的原则则不允许这样定义.我们来一个例子说明.还是接着上面的例子来说明吧.男人想结婚要找对象,因此提出了一堆要求.这些要求一个女人有时候很难满足,因此小三开始登场.好吧我们来分解一下,将一个大而全的要求分解为多个小的要求.为此男人付出的代价就是需要应付多个小三.类图如下:
具体代码如下:
interface IName
{
String getName();
}
interface IDoCook extends IName
{
void doCook();
}
interface IKnownLove extends IName
{
void knownLove();
}
class Gril3 implements IDoCook
{
private String name;
public Gril3(String name)
{
this.name=name;
}
@Override
public void doCook() {
System.out.println(this.name + " 会做饭,会抓住男人的胃,但不懂什么叫做爱");
}
@Override
public String getName() {
return name;
}
}
class Gril4 implements IKnownLove
{
private String name;
public Gril4(String name)
{
this.name=name;
}
@Override
public void knownLove() {
System.out.println(this.name+"非常明白什么叫做爱,但不会做饭");
}
@Override
public String getName() {
return name;
}
}
class BadMan
{
private IKnownLove xiaosan;
private IDoCook wife;
public BadMan setXiaosan(IKnownLove xiaosan) {
this.xiaosan = xiaosan;
return this;
}
public BadMan setWife(IDoCook wife) {
this.wife = wife;
return this;
}
public void marray()
{
System.out.println("男人与"+wife.getName()+ " 结婚" );
wife.doCook();
}
public void thirdGird()
{
System.out.println("男人找小三" + xiaosan.getName() );
xiaosan.knownLove();
}
}
class Main3{
public static void main(String[] args) {
IDoCook wife=new Gril3("貂蝉");
IKnownLove xiaosan=new Gril4("杨玉环");
BadMan badMan=new BadMan();
badMan.setWife(wife);
badMan.setXiaosan(xiaosan);
badMan.marray();
badMan.thirdGird();
}
}
从上面的代码中男人可以完成结婚和找小三的功能.但是男人的欲望在不断膨胀.现在只需要懂什么叫做爱就可以是小三,现在男人增加了一种爱好,比如讨人喜欢.如果按照依赖倒转中提到的例子来做那么你就会发现我们接口已经固定.需要修改接口方能实现.但是如果按照本节中的定义方式来做却只需要增加接口即可完成.
接口隔离核心思想就是接口要足够的小,但是小也不是无限度的小,应该首先满足职责单一原则.接口隔离要求接口足够的小,但我们开发要求接口要高内聚.首先说一下什么是高内聚,高内聚的要求是提高接口,类或者模块的处理能力以减少对外依赖.举个简单例子,战场上司令给军长下达占领A这座城市.然后军长毫不犹豫的回答”是,保证完成任务”.然后军长自己带着部队攻打A这座城市.并且成功占领.这个就是高内聚的表现.在程序中接口隔离原则就是减少能够让外部访问的方法.外部能够访问的方法就表明此类或者接口对外的一种承诺.而这种承诺越少责任就越小,因外部原因而改变的可能性也就越小,因为功能少.
接口设计是有限度的.接口的设计粒度是越小系统越灵活,这是大家都知道的,但副作用是结构的复杂化,开发难度增加,维护性降低,这不是我们所期望看到的,所有接口设计一定要注意适度,适度的“度”怎么来判断的呢?这个度目前没有具体的标准,大部分都是根据经验和常识判断!
接口隔离原则是对接口的定义也同时是对类的定义,接口和类都尽量使用原子接口或原子类来组装,但是这个原子该怎么划分也是设计中的一大难题,在实践中应用时可以根据以下几个规则来衡量:
- 一个接口只服务于一个子模块或者业务逻辑
通过业务逻辑压缩接口中的 public 方法。接口时常去回顾,尽量做让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法.对于已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
- 了解环境,拒绝盲从
每个项目或产品都有特定的环境因素,别看到别人是这样做的你就照抄,千万别这样,环境不同的,接口拆分的标准就不同.深入了解的业务逻辑,才能设计出合理的接口.
接口隔离原则和其他的设计原则都一样需要花费很多精力来设计,需要从多方面考虑,既要保证灵活性也要保证开发的维护方便性,同时还要能够应对用户提出各种”无理”要求的时候程序能够从容面对.
迪米特原则又叫知道最少原则.一个类应该对其他类有最少的了解,通俗的讲一个类对自己需要耦合或者调用的类应该知道的最少,你类内部是怎么复杂,怎么的纠缠不清都和我没关系,那是你的类内部的事情,我就知道你提供的这些接口,然后就调用这些接口.迪米特原则另一解释很有意思:只和直接的朋友交流.我们程序中会有很多的对象,每个对象之间必然会有多种关系,这些关系将对象与对象耦合在一起.只和朋友交流意思就是只与最近的类保持通信.类中又很多种关系组合,聚合,继承,关联,依赖,实现等等.业界将出现在成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友.
举个例子说明吧:
在这个类图中,看起来其实有些别扭.公司知道雇员,这个其实就是违反迪米特法则.我们直接上代码:
class Main4{
public static void main(String[] args) {
Company company=new Company();
for (int i = 0; i < 5; i++) {
Department department=new Department();
department.setDepartName("部门" + i);
for (int j = 0; j < 5; j++) {
Employee employee=new Employee();
employee.setName("姓名"+i);
employee.setNumber("Num:" +i);
department.addEmployee(employee);
}
company.addDept(department);
}
company.show();
}
}
class Company
{
private String companyName;
private List<Department> departments=new ArrayList<>();
public String getCompanyName() {
return companyName;
}
public Company setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}
public void addDept(Department department)
{
departments.add(department);
}
public void show()
{
this.departments.forEach(x->{
System.out.println(x.getDepartName());
x.getEmployees().forEach(e->{
System.out.println(e.getName()+"," +e.getNumber());
});
});
}
}
class Employee
{
private String name;
private String number;
public String getName() {
return name;
}
public Employee setName(String name) {
this.name = name;
return this;
}
public String getNumber() {
return number;
}
public Employee setNumber(String number) {
this.number = number;
return this;
}
}
class Department
{
private String DepartName;
private List<Employee> employees=new ArrayList<>(16);
public void addEmployee(Employee employee)
{
employees.add(employee);
}
public List<Employee> getEmployees()
{
return employees;
}
public String getDepartName() {
return DepartName;
}
public Department setDepartName(String departName) {
DepartName = departName;
return this;
}
public void showEmployee()
{
employees.forEach(x-> System.out.println(x.getName()
+","
+ x.getNumber()));
}
}
这里违反迪米特法则的原因在于Company这个类中出现了Employee类的对象.正常情况是公司只应该有部门,雇员属于部门,那么雇员的信息就应该由部门管理.因此我们修改一下:
class Company
{
private String companyName;
private List<Department> departments=new ArrayList<>();
public String getCompanyName() {
return companyName;
}
public Company setCompanyName(String companyName) {
this.companyName = companyName;
return this;
}
public void addDept(Department department)
{
departments.add(department);
}
public void show()
{
this.departments.forEach(x->{
System.out.println(x.getDepartName());
x.showEmployee();
});
}
}
注意黄色部分的代码.其他部分的代码都不变.这样就符合迪米特法则,公司只负责管理部门,不管理雇员,部门负责管理雇员,那就让部门来负责.这样既减少了公司对于雇员的依赖也让职责更加清晰.
朋友之间也应该有距离.比如一个类中需要调用一个陌生类中的方法,但这个陌生是朋友类中可以访问的类,那么应该让朋友类来负责调用.这个听起来有点拗口.举个例子吧比如一般大一点的公司老板不会认识全部的雇员,但是他要下达一个命令给某个他不知道姓名的雇员,那么怎么办呢?找这个雇员所在的部门的负责人来传达.也就是常见的中介模式.例子如下:
具体代码如下:
class Worker{
private String name;
public Worker(String name) {
this.name=name;
}
public void doWork()
{
System.out.println(name +" execute");
}
}
class Leader {
private Worker worker;
public Leader(Worker worker) {
this.worker = worker;
}
public void command()
{
System.out.println("leader 通知工作者");
worker.doWork();
}
}
class Boos{
private Leader leader;
public Boos(Leader leader) {
this.leader = leader;
}
public void command()
{
System.out.println("boss 下命令");
leader.command();
}
}
class Main5{
public static void main(String[] args) {
Boos boos =new Boos(new Leader(new Worker("张三")));
boos.command();
}
}
迪米特法则使用需要多方面的考虑,容易在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的业务逻辑无关.遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联.但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调.在项目常常出现这种情况,一个方法放在本类中也可以,放在其他类中看起来也没有什么不妥,那么究竟放在哪个类中呢?有一个简单的方法可以帮忙辨别,如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,就放置在本类中.上面公司的例子中,如果将打印雇员的信息放到公司中那么就增加了公司对雇员的关系,因此是不合适的.
这个原则是软件设计中的一个基础原则.开闭原则简单来说就是对扩展开放对修改关闭.什么意思呢,其实很简单,就是你可以通过我扩展,但想修改我没门儿.一个类的修改应该只因为错误而修改(需求变更也属于错误).其他任何情况都不应该修改此类,应该通过扩展的方式来实现.
开闭原则有两种方式一种是多态开闭原则:由于抽象化接口的使用,在这中间实现可以被改变,多种实现可以被创建,并且多态化的替换不同的实现.另一种叫做梅耶开闭原则,提倡实现继承.具体实现可以通过继承方式来重用,但是接口规格不必如此.已存在的实现对于修改是封闭的,但是新的实现不必实现原有的接口.这个原则相对于其他原则遵守的比较好,同时也使用的比较多.其实说简单点就是结合继承和多态的方式来实现类的扩展,而不是去修改原有的类的代码.这个原则是一个基础原则,如果我们能够做到上面的5个原则那么这个原则自然就遵守了.