软件开发七大原则
一、开闭原则:面对扩展开放,面对修改关闭
(1)指的是在开发的过程中尽量的去扩展代码,而不是去修改原来的代码,以免影响到之前的逻辑。
(2)强调的是用抽象构建框架,用实现扩展细节。
(3)可以提高软件系统的可复用性及可维护性
(4)例:原有课程类,闲杂要对课程进行打折,应该怎么处理呢?
原有课程类:
package test1; /** * author:songyan * date: 2019/10/6 **/ public interface Course { public String getId(); public String getName(); public double getPrice(); }
package test1; /** * author:songyan * date: 2019/10/6 **/ public class JavaCourse implements Course { private String id; private String name; private double price; @Override public String getId() { return id; } public void setId(String id) { this.id = id; } @Override public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public JavaCourse(String id, String name, double price) { this.id = id; this.name = name; this.price = price; } }
新增打折课程类
package test1; /** * author:songyan * date: 2019/10/6 **/ public class JavaDiscountCourse extends JavaCourse{ public JavaDiscountCourse(String id, String name, double price) { super(id, name, price); } public double getOriginPrice() { return super.getPrice(); } public double getPrice() { return super.getPrice()*0.8; } }
在不修改原来的逻辑的基础上作出对应的修改
测试类:
package test1; /** * author:songyan * date: 2019/10/6 **/ public class Test { public static void main(String[] args) { JavaDiscountCourse cource = new JavaDiscountCourse("001","java",100.0); System.out.println(cource.getPrice()); System.out.println(cource.getOriginPrice()); } }
二、依赖倒置原则:依赖于抽象接口,不要依赖于具体实现。
(1)要求对抽象进行编程,不要对实现进行编程。
(2)降低了客户与实现模块间的耦合。
(3)例:原有转换工具类可以转换word,pdf两种类型的文件,现在该工具类需要扩展使其在原来的基础上还能转换excel文件
如果是按照依赖实现编程:
package test2; /** * des: office文档转pdf类 * author:songyan * date: 2019/10/6 **/ public class Trans { public void transWord(){ System.out.println("转换"); } public void transPDF(){ System.out.println("pdf转换"); } }
客户端:
package test2; /** * author:songyan * date: 2019/10/6 **/ public class Client { public static void main(String[] args) { Trans trans = new Trans(); trans.transWord(); trans.transPDF(); } }
使用这种方式的弊端就在于,如果说要扩展转换工具类的工具类的功能,就需要去修改之前的代码,显然这种做法是非常不安全的,有可能就会影响之前代码。
更好的做法是:
package test2; /** * author:songyan * date: 2019/10/6 **/ public class TransUtil { public void trans(OfficeDocument document){ document.trans(); } }
package test2; /** * author:songyan * date: 2019/10/6 **/ public class WordDocument implements OfficeDocument { @Override public void trans() { System.out.println("word文档转换"); } }
package test2; /** * author:songyan * date: 2019/10/6 **/ public class PDFDocuemnt implements OfficeDocument{ @Override public void trans() { System.out.println("pdf转换"); } }
package test2; /** * author:songyan * date: 2019/10/6 **/ public class Test { public static void main(String[] args) { TransUtil trnas = new TransUtil(); trnas.trans(new WordDocument()); trnas.trans(new PDFDocuemnt()); } }
在转换工具类中是针对处理对象的接口进行处理的,在想要扩展功能的时候只需要添加一个实现类即可,例如:
package test2; /** * author:songyan * date: 2019/10/6 **/ public class ExcelDocument implements OfficeDocument{ @Override public void trans() { System.out.println("excel转换"); } }
package test2; /** * author:songyan * date: 2019/10/6 **/ public class Test { public static void main(String[] args) { TransUtil trnas = new TransUtil(); trnas.trans(new WordDocument()); trnas.trans(new PDFDocuemnt()); trnas.trans(new ExcelDocument()); } }
使用这种方法只需要扩展之前的代码,而不需要修改之前的代码,其实也就是上面说开闭原则。
这个例子其实就是“依赖注入”,那么依赖注入的方式又包括构造器注入,setter方法注入,下面简介一下这两种方式
1)构造器注入
package test2.generator; import test2.OfficeDocument; /** * author:songyan * date: 2019/10/6 **/ public class TransUtil { private OfficeDocument officeDocument; public TransUtil(OfficeDocument officeDocument) { this.officeDocument = officeDocument; } public void trans() { officeDocument.trans(); } }
package test2.generator; import test2.WordDocument; /** * author:songyan * date: 2019/10/6 **/ public class Test { public static void main(String[] args) { TransUtil transUtil = new TransUtil(new WordDocument()); transUtil.trans(); } }
2)setter方式注入
package test2.setter; import test2.OfficeDocument; /** * author:songyan * date: 2019/10/6 **/ public class TransUtil { private OfficeDocument officeDocument; public void setOfficeDocument(OfficeDocument officeDocument) { this.officeDocument = officeDocument; } public void trans(){ officeDocument.trans(); } }
package test2.setter; import test2.WordDocument; /** * author:songyan * date: 2019/10/6 **/ public class Test { public static void main(String[] args) { TransUtil transUtil= new TransUtil(); transUtil.setOfficeDocument(new WordDocument()); transUtil.trans(); } }
以抽象为基准比以细节为基准搭建起来的框架要稳健的多,因此,大家在拿到需求之后要面向抽象接口来编程,先顶层在底层来设计代码结构。
三、单一职责原则:一个类,一个接口,一个方法应该只有一个职能
(1)如果有多个职能,则其中一个职能发生改变之后,就需要修改这个类的功能,就有可能影响到另一个职能。所以我们有必要对他们进行一定程度的拆分。
(2)降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险
(3)例:有一个对nginx操作的工具类如下:
package test3; /** * Nginx工具类 * author:songyan * date: 2019/10/6 **/ public class NginxUtil { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void start(){ System.out.println("启动nginx"); } }
里面有设计nginx基本信息的方法,也有启动nginx的方法,比如说,nginx的启动方法发生了改变,这个时候就需要修改nginxUtil类,那再修改的过程中就有可能对他的其他方法产生影响,因此我们可以对他进行以下划分,将他隔离开:
package test3; /** * author:songyan * date: 2019/10/6 **/ public class NginxInfo { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
package test3; /** * author:songyan * date: 2019/10/6 **/ public class NginxOperator { public void start(){ System.out.println("nginx启动"); } }
接口与此类似。。
方法不符合单一职责而原则的例子:
public void modefied(String name,String fav){ System.out.println("修改名字"); System.out.println("修改爱好"); }
方法符合单一职责而原则的例子:
public void modefiedName(String name){ System.out.println("修改姓名"); } public void modefiedFav(String fav){ System.out.println("修改爱好"); }
其实这种方式乍一看可能感觉不到这样写的好处在哪里,我突然想到我最近的一个任务,是修改之前的同事写的一个系统,里面大多数的逻辑代码都是摞在一起的,一个方法中有几百行代码,做什么的都有,可能有的时候只需要改其中的一个点,但是可能就会影响到其他部分的代码,还有一个缺点就是,里面的代码错综复杂,可能的要找到你想改的地方都很难。
总结一下就是,在写代码的过程中如果尽量的保持单一指责原则就会,提高类的可读性,提高系统的可维护性,降低变更引起的风险。
四、接口隔离原则:使用多个专一的接口,而不使用单一的总接口
(1)一个类对一个类的接口应该建立在最小的接口之上。
(2)尽力单一的接口,而不要建立单一臃肿的接口
(3)尽量的细化接口,接口中的方法尽量少(不是越少越好,要适度)
(4)例:前几天总结的office文档转pdf的方式,其中openoffice,aspose是支持windows,linux两种系统的,但是jacob只支持windows系统,下面的代码就会存在一定的问题。
在下面的代码中,将windows转换的方式,Linux转换的方式放在了一个接口中:
package test4; /** * author:songyan * date: 2019/10/6 **/ public interface Itrans { void windowsTrans(); void linuxTrans(); }
在openoffice,aspose两种方式中,是没有问题的,见下:
package test4; /** * author:songyan * date: 2019/10/6 **/ public class AsposeTrans implements Itrans{ @Override public void windowsTrans() { System.out.println("Aspose在windows的转换"); } @Override public void linuxTrans() { System.out.println("Aspose在linux的转换"); } }
package test4; /** * author:songyan * date: 2019/10/6 **/ public class OpenofficeTrans implements Itrans{ @Override public void windowsTrans() { System.out.println("oppenoffice在windows的转换"); } @Override public void linuxTrans() { System.out.println("oppenoffice在linux的转换"); } }
但是,你会发现在jacob中,他是不支持在Linux的转换的,但是实现这个接口的话必须重写这个接口。。
package test4; /** * author:songyan * date: 2019/10/6 **/ public class JacobTrans implements Itrans{ @Override public void windowsTrans() { System.out.println("Jacob在windows的转换"); } @Override public void linuxTrans() { } }
针对上面的情况可以做以下完善,将接口中的方法拆分到两个接口中:
package test4; /** * author:songyan * date: 2019/10/6 **/ public interface IWindowsTrans { void windowsTrans(); }
package test4; /** * author:songyan * date: 2019/10/6 **/ public interface ILinuxTrans { void linuxTrans(); }
对两种系统都支持的方式可以去实现两种接口
package test4; /** * author:songyan * date: 2019/10/6 **/ public class AsposeTrans implements IWindowsTrans,ILinuxTrans{ @Override public void windowsTrans() { System.out.println("Aspose在windows的转换"); } @Override public void linuxTrans() { System.out.println("Aspose在linux的转换"); } }
对只支持一种系统的方式只需要实现一种接口
package test4; /** * author:songyan * date: 2019/10/6 **/ public class JacobTrans implements IWindowsTrans{ @Override public void windowsTrans() { System.out.println("Jacob在windows的转换"); } }
五、迪米特法则:最少知道原则。
(1)迪米特原则主要强调只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类,而出现在方法体内部的类不属于朋友类。
(2)例:老板让leader查询课程的数量
package test5; import java.util.ArrayList; import java.util.List; /** * author:songyan * date: 2019/10/6 **/ public class Boss { public void askClassNumber(){ List<Clazz> clazzLiat = new ArrayList<Clazz>(); clazzLiat.add(new Clazz()); clazzLiat.add(new Clazz()); clazzLiat.add(new Clazz()); TeamLeader teamLeader = new TeamLeader(); System.out.println(teamLeader.getClassNumber(clazzLiat)); } }
package test5; import java.util.ArrayList; import java.util.List; /** * author:songyan * date: 2019/10/6 **/ public class TeamLeader { public int getClassNumber(List<Clazz> clazzLiat) { return clazzLiat.size(); } }
package test5; /** * author:songyan * date: 2019/10/6 **/ public class Clazz { }
以上代码实现所要求的功能是完全没有问题的,但是,根据迪米特法则,Boss类是没有必要跟Clazz类关联的,所以可以改成以下代码
package test5; import java.util.ArrayList; import java.util.List; /** * author:songyan * date: 2019/10/6 **/ public class Boss { public void askClassNumber(){ TeamLeader teamLeader = new TeamLeader(); System.out.println(teamLeader.getClassNumber()); } }
package test5; import java.util.ArrayList; import java.util.List; /** * author:songyan * date: 2019/10/6 **/ public class TeamLeader { public int getClassNumber() { List<Clazz> clazzLiat = new ArrayList<Clazz>(); clazzLiat.add(new Clazz()); clazzLiat.add(new Clazz()); clazzLiat.add(new Clazz()); return clazzLiat.size(); } }
package test5; /** * author:songyan * date: 2019/10/6 **/ public class Clazz { }
package test5; /** * author:songyan * date: 2019/10/6 **/ public class Test { public static void main(String[] args) { Boss boss = new Boss(); boss.askClassNumber(); } }
简单说就是boss只需要去问leader课程数量有多少,具体的统计过程需要leader自己来完成,最终把结果给BOSS即可。
六、里氏替换原则:父类可以使用的地方也可以适用子类
(1)例:在开闭原则的例子中,在打折类中使用了getPrice获取了打折后的价格,使用getOriginPrice获取打折之前的价格,这里其实是违背了里氏替换原则的,根据里氏替换原则,JavaCourse可以通过getPrice获取课程的价格(原价),那么其子类应该也是可以通过该方法获取课程原价的,但是在重写的时候逻辑却换成了获取折后价。
那么正确的写法应该如下:
package test6; /** * author:songyan * date: 2019/10/7 **/ public interface ICourse { public String getId(); public String getName(); public double getPrice(); }
package test6; /** * author:songyan * date: 2019/10/7 **/ public class JavaCourse implements ICourse{ private String id; private String name; private double price; public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public JavaCourse(String id, String name, double price) { this.id = id; this.name = name; this.price = price; } }
package test6; /** * author:songyan * date: 2019/10/7 **/ public class DiscountJavaCourse extends JavaCourse{ public double getPrice(){ return super.getPrice(); } public double getDiscountPrice(){ return super.getPrice()*0.8; } public DiscountJavaCourse(String id, String name, double price) { super(id, name, price); } }
七、合成复用原则:尽量使用组合,聚合的方式而不是继承的方式实现复用
(1)合成服用原则具体指的是在创建新的对象的时候使用已有的对象。
(2)使用的方式有两种:组合/聚合,继承。
(3)在新对象与“已有的对象”两者的关系是“is-a”时,使用继承关系;两者的关系是"has-a"时,使用组合/聚合的方式。
(4)在使用合成服用原则时,注意不要滥用继承关系
(5)区分“组合”,“聚合”,“继承”
1)组合(整体与部分,同生命周期)
新旧对象是“has-a”的关系,是一种“强”的拥有关系,原有的对象是新对象的一部分,并且两个对象的生命周期是一样的。
例:人与四肢的关系。人有四肢,四肢是人体的一部分,人没了,四肢也就不存在了。
2)聚合(整体与部分,不同生命周期)
新旧对象是"has-a"的关系,是一种“弱”的拥有关系,原有的对象不是新对象的一部分,两个对象的生命周期可以不相同。
例:人与人群的关系。人群里面有人,当人群散了,人照样可以存在。
3)继承
例:学生与人的关系。学生是人,输入人类的一种。
新旧对象是“is-a”的关系。