架构设计原则
6大设计原则
Single Responsibility Principle : 单一职责原则
Liskov Substitution Principle : 里氏替换原则
Dependence Inversion Principle :依赖倒置原则
Interface Segregation Principle : 接口隔离原则
Law of Demeter : 迪米特法则
Open Closed Principle : 开闭原则
软件开发之所以会有这些原则,就是因为复杂多变且不可预料的需求。并不是说在实际项目开发中对这六大原则中的每一条都遵循到极致,而是说在项目开发的过程中,根据项目的实际需求尽量的去遵守这些原则。当然要做到这些肯定是不容易的,能真正做到并且做好的恐怕也只能是有经验之人。
问题场景:
模块独立性指每个模块只完成系统要求的独立子功能,并且与其他模块的联系最少且接口简单,两个定性的度量标准――耦合性和内聚性。
耦合性也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。
耦合性分类(低―高)
1 无直接耦合:
2 数据耦合: 指两个模块之间有调用关系,传递的是简单的数据值,相当于高级语言的值传递;
3 标记耦合: 指两个模块之间传递的是数据结构,如高级语言中的数组名、记录名、文件名等这些名字即标记,其实传递的是这个数据结构的地址;
4 控制耦合: 指一个模块调用另一个模块时,传递的是控制变量(如开关、标志等),被调模块通过该控制变量的值有选择地执行块内某一功能;
5 公共耦合: 指通过一个公共数据环境相互作用的那些模块间的耦合。公共耦合的复杂程序随耦合模块的个数增加而增加。
6 内容耦合: 这是最高程度的耦合,也是最差的耦合。当一个模块直接使用另一个模块的内部数据,或通过非正常入口而转入另一个模块内部。
内聚性又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。
内聚性分类(低―高)
1 偶然内聚: 指一个模块内的各处理元素之间没有任何联系。
2 逻辑内聚: 指模块内执行几个逻辑上相似的功能,通过参数确定该模块完成哪一个功能。
3 时间内聚: 把需要同时执行的动作组合在一起形成的模块为时间内聚模块。
4 通信内聚: 指模块内所有处理元素都在同一个数据结构上操作(有时称之为信息内聚),或者指各处理使用相同的输入数据或者产生相同的输出数据。
5 顺序内聚: 指一个模块中各个处理元素都密切相关于同一功能且必须顺序执行,前一功能元素输出就是下一功能元素的输入。
6 功能内聚: 这是最强的内聚,指模块内所有元素共同完成一个功能,缺一不可。与其他模块的耦合是最弱的。
耦合性与内聚性是模块独立性的两个定性标准,将软件系统划分模块时,尽量做到高内聚低耦合,提高模块的独立性,为设计高质量的软件结构奠定基础。
某位大师说过: 计算机科学中的所有的问题, 都可以通过增加一个间阶层来解决.
这位大师好像是 Dennis Debruler .
间阶层的价值:
允许逻辑共享
分开解释意图和实现:你可以选择每个类和函数的名字,这给了你一个解释自己意图的机会。
隔离变化:通过使用子类来隔离同一对象修改对另一处引用带来变化的风险
封装条件逻辑:使用多态机制将条件逻辑转化为消息机制,这往往能降低代码的重复,增加清晰度并提高弹性.
但是:
过多的间接层会导致代码的层次太深
使代码难以阅读.因此要权衡加入间接层的利弊
1、GOF在书中说:设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述;
设计模式就是不断反省,将软件开发经验抽象积累成解决问题的预案。
2、Dennis DeBruler曾说过的一句话:计算机科学是一门相信所有问题都可以通过多一个间接层(indirection)来解决的科学。
技术问题的解决思路是类似的:添加间接层。如:J2EE的分层技术、重构等。
间接层的好处:共享逻辑(重用)、分离意图和实现(提高灵活性)、 隔离变化(封装)、解耦等。
但是要记住,间接层应用过于泛滥,则会过犹不及,它会导致简单问题复杂化、跳跃阅读、难以理解等问题,适度的使用间接层,设计模式就是很好的范例。
不过在具体操作上,23种模式中间接层的应用可大可小,无比灵活,注意其应用场景。
3、设计模式中广泛遵循了两条设计原则:面向接口编程,而不是实现;优先使用组合,而不是继承。
单一职责原则(SRP)
Single Responsibility Principle(简称SRP):单一职责原则
1.单一职责的定义:应该有且只有一个原因引起类的变更。换句话说就是一个接口只做一件事,即一个职责一个接口。但是困难的是划分职责时并没有一个标准,最终都是需要从实际的项目去考虑。我们在设计的时候,尽量单一,然后对于其实现类就要多方面的考虑。不能死套单一职责原则,否则会增加很多类,给维护带来不便。
2. 接口单一职责实例:
下面是一个User类的类图:
仔细去考虑这个类的设计,我们就可以看到问题所在。用户的属性属性操作和其他的行为没有分开。正确的做法是我们应该把属性抽取成为一个业务对象(Business Object,简称BO),而把行为抽取成为一个业务逻辑(Business Logic,简称BL)。
依赖单一职责原则设计出的类,类图如下:
3.单一职责同样适用于方法
有这样一个方法,传递可变长参数去修改一个用户的信息。
public boolean changeUserinfo(User user,String ...changeOptions){
//修改密码,可变参数传递要修改的密码,编写相应的实现代码
//修改地址,可变参数传递要修改的地址,编写相应的实现代码
}
像这样的写是绝对不允许的,相信大家也不会这样写。这样的方法就是典型的违背了单一职责原则。正确的写法如下:
public boolean changeUserpassword(User user,String psw){
//代码实现
}
public boolean changeUseraddress(User user,String psw){
//代码实现
}
4.总结:接口一定做到单一职责,类的设计尽量做到只有一个原因可以引起它的改变。
开闭原则(OCP)
Open Closed Principle : 开闭原则
1. 定义:开闭原则是Java里最基础的设计原则。具体的定义是:一个软件实体,比如类,模块,函数应该对扩展开放,对修改关闭。说的通熟易懂一些就是一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现改变。
2. 举一个实例的例子来说明一下开闭原则的具体做法。
① 一个简单的图书销售系统。
②现在业务需求有改变,所有书均打七折。有三个方法可以解决这个问题:
第一种方法:修改接口。增加一个方法getOffPrice专门进行打折处理。
第二种方法:修改实现类,在实现类里修改getPrice方法。
第三种方法:重新扩展一个类继承NovelBook,重新复写getPrice方法。
根据开放扩展关闭修改我原则我们应该选择第三种解决方法。具体代码试下如下:
import java.util.ArrayList;
import java.util.List;
interface Book{
int getPrice();
String getAuthor();
String getName();
int getCount();
}
class NovelBook implements Book{
private int price;
private String author;
private String name;
private int count;
public NovelBook(int price, String author, String name, int count) {
super();
this.price = price;
this.author = author;
this.name = name;
this.count = count;
}
@Override
public int getPrice() {
return this.price;
}
@Override
public String getAuthor() {
return this.author;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getCount() {
return this.count;
}
}
public class BookStore {
private static List<Book> books = new ArrayList<Book>();
static{
books.add(new NovelBook(30,"Author1","Java",100));
books.add(new NovelBook(40,"Author2","PHP",400));
books.add(new NovelBook(10,"Author3","JS",20));
books.add(new NovelBook(20,"Author4","Ajax",4));
}
public static void main(String[] args){
System.out.println(" 图书信息");
for(Book book : books){
System.out.println(" 书籍名称:"+book.getName()+" 书籍作者:"+book.getAuthor()+" 书籍价格:"+book.getPrice()+" 书籍库存:"+book.getCount());
}
}
}
import java.util.ArrayList;
import java.util.List;
interface Book{
int getPrice();
String getAuthor();
String getName();
int getCount();
}
class NovelBook implements Book{
private int price;
private String author;
private String name;
private int count;
public NovelBook(int price, String author, String name, int count) {
super();
this.price = price;
this.author = author;
this.name = name;
this.count = count;
}
@Override
public int getPrice() {
return this.price;
}
@Override
public String getAuthor() {
return this.author;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getCount() {
return this.count;
}
}
class OffNovelBook extends NovelBook{
public OffNovelBook(int price, String author, String name, int count) {
super(price, author, name, count);
}
public int getPrice() {
int price = super.getPrice();
price = (int) (price * 0.6);
return price;
}
}
public class BookStore {
private static List<Book> books = new ArrayList<Book>();
static{
books.add(new OffNovelBook(30,"Author1","Java",100));
books.add(new OffNovelBook(40,"Author2","PHP",400));
books.add(new OffNovelBook(10,"Author3","JS",20));
books.add(new OffNovelBook(20,"Author4","Ajax",4));
}
public static void main(String[] args){
System.out.println(" 图书信息");
for(Book book : books){
System.out.println(" 书籍名称:"+book.getName()+" 书籍作者:"+book.getAuthor()+" 书籍价格:"+book.getPrice()+" 书籍库存:"+book.getCount());
}
}
}
图书信息
书籍名称:Java 书籍作者:Author1 书籍价格:18 书籍库存:100 书籍名称:PHP 书籍作者:Author2 书籍价格:24 书籍库存:400 书籍名称:JS 书籍作者:Author3 书籍价格:6 书籍库存:20 书籍名称:Ajax 书籍作者:Author4 书籍价格:12 书籍库存:4
里氏替换原则(LSP)
Liskov Substitution Principle:里氏替换原则
1. 定义:里氏替换原则简单易懂一点的定义就是只要父类出现的地方子类就可以出现,且替换成子类也不会出现任何错误或者异常。(但是反过来,有子类出现的地方,父类 不一定可以适用)。
2. 里氏替换原则是为继承定义了四个规范
① 子类必须完全实现父类的方法。
abstract class AbstractGun{
public abstract void shoot();
}
class Handgun extends AbstractGun {
@Override
public void shoot() {
System.out.println("Handgun shoot ......");
}
}
class Machinegun extends AbstractGun {
@Override
public void shoot() {
System.out.println("Machinegun shoot ......");
}
}
class Solider{
private AbstractGun gun;
//此处的AbstractGun gun 是父类出现的地方,子类可以出现 体现在传给它的实际参数类型可以是Machinegun或者Handgun任一种类型,并且都不会出错
public void setGun(AbstractGun gun){
this.gun = gun;
}
public void kill(){
this.gun.shoot();
}
}
public class Client{
public static void main(String[] args){
Solider solider = new Solider();
//传入的时候并不知道是哪种类型 ,运行时才知道,而且修改枪支的类型只需要new 不同的对象即可。而不用修改其他的任何地方
solider.setGun(new Machinegun());
solider.kill();
}
}
客户端运行结果:Machinegun shoot ......
此时假设有一个玩具枪,放到实际问题上,玩具枪是不能杀人的。所以给shoot方法一个空实现。
class Toygun extends AbstractGun {
@Override
public void shoot() {
//空实现
}
}
客户端代码修改如下:
public class Client{
public static void main(String[] args){
Solider solider = new Solider();
//修改部分为红色
solider.setGun(new Toygun());
solider.kill();
}
}
客户端运行是没有结果的,这是肯定的,因为Toygun的shoot方法是一个空实现。解决的方法有两个:第一种解决方案就是在Solider类中增加一个类型判断。如果是Toygun类 型,就不调用shoot方法。这样出现的问题是,每多出一个类就要增加一种类型判断,这样显然是不合理的。第二种解决方案就是让Toygun脱离继承,建立一个独立的父类。
总结:如果子类不能完全实现父类的方法,建议断开父子关系,采用Java类之间关联关系的另外三种依赖、关联、组合去实现。
② 子类可以有自己的个性。
里氏替换原则可以正着用,即父类出现的地方子类一定可以出现,但是反过来子类出现的地方父类就不一定适用。
在上一个代码实例中修改部分代码:
class HandgunOne extends Handgun{
public void shoot() {
System.out.println("HandgunOne射击......");
}
}
class Solider{
//HandgunOne gun是子类出现的地方,父类不一定适用体现在,当在客户端传递(HandgunOne) new Handgun()这样的参数时,运行会抛出java.lang.ClassCastException 异常。
public void kill(HandgunOne gun){
gun.shoot();
}
}
public class Client{
public static void main(String[] args){
Solider solider = new Solider();
//下面的代码会抛出异常,因为像下转型是不安全的
solider.kill((HandgunOne) new Handgun());
}
}
③覆盖或者实现父类的方法时输入参数可以被放大。
示例代码:
import java.util.HashMap;
import java.util.Map;
class Father{
public void doSomething(HashMap map){
System.out.println("father doSomething");
}
}
class Son extends Father{
//此方法不是重写,而是重载
public void doSomething(Map map){
System.out.println("son doSomething");
}
}
public class Client{
public static void main(String[] args){
//此处为父类出现的地方,一会根据里氏替换原则会换成子类
Father father = new Father();
father.doSomething(new HashMap());
}
}
客户端输出:father doSomething
根据里氏替换原则做对客户端代码做如下修改:
public class Client{
public static void main(String[] args){
Son son = new Son();
son.doSomething(new HashMap());
}
}
客户端依然输出:father doSomething。
父类方法的参数类型为:HashMap,子类方法的参数类型为:Map。很明显子类将父类的参数类型扩大了,子类代替父类后执行相同的方法,是不会执行子类的方法的。我们要知道,要想让子类的方法运行,就必须重写父类的同名方法。因为上面不是重写而是重载,所以子类的方法并没有被运行。因此父类的一个方法在子类中可以被重写,也可以被重载,但是重载时的参数类型必须大于父类同名方法中的参数类型。否则就会出现:子类没有覆盖父类的方法,调用时却运行了子类的方法。以下就是一个代码示例:
import java.util.HashMap;
import java.util.Map;
class Father{
public void doSomething(Map map){
System.out.println("father doSomething");
}
}
class Son extends Father{
//此方法不是重写,而是重载,但是和前一个不同,它没有扩大参数类型,而是缩小了参数的类型
public void doSomething(HashMap map){
System.out.println("son doSomething");
}
}
public class Client{
public static void main(String[] args){
//此处为父类出现的地方,下一个示例会用子类替换掉父类
Father father = new Father();
father.doSomething(new HashMap());
}
}
客户端输出:father doSomething
用子类替换掉父类代码示例(只修改客户端部分代码):
public class Client{
public static void main(String[] args){
Son son = new Son();
son.doSomething(new HashMap());
}
}
客户端输出:son doSomething
可以看到出现了问题:子类没有覆盖父类的同名方法(只是重载了),但是却运行了子类的方法。这样做就出现了逻辑混乱(要想让子类的方法运行,就必须覆盖重载父类的同名方,然而实际上子类并没有覆盖父类的同名方法,但是还是用了子类的方法)。
总结:父类的一个方法在子类中可以被重写,也可以被重载,但是重载时的参数类型必须大于父类同名方法中的参数类型
④复写或实现父类的方法时返回值可以缩小。
依赖倒置原则(DIP)
Dependence Inversion Principle:依赖倒置原则
1.定义:精简的定义就是面向接口编程。在Java语言中的表现就是为以下的三点
① 模块间的依赖关系通过 接口 和 抽象类 产生,实体类之间不直接发生依赖关系。
代码示例一:实例类之间产生依赖所出现的问题
class Driver{
//在这里产生了实体类之间的依赖
public void drive(Benz benz){
benz.run();
}
}
class Benz{
public void run(){
System.out.println("benz run......");
}
}
public class Client{
public static void main(String[] args){
Driver driver = new Driver();
driver.drive(new Benz());
}
}
假如有一天Driver不开benz了,则此时代码要修改两处:
class Driver{
public void drive(BMW bmw){ // 改动处: BMW bmw
bmw.run();
}
}
class Benz{
public void run(){
System.out.println("benz run......");
}
}
//此处的业务逻辑类的实现是必不可少的
class BMW{
public void run(){
System.out.println("bmw run......");
}
}
public class Client{
public static void main(String[] args){
Driver driver = new Driver();
driver.drive(new BMW());
}
}
因为Driver类和Benz类之间的紧耦合导致只是增加了一辆车就要修改Driver类。因此正确的做法是让Driver类去依赖一个接口。
interface Car{
public void run();
}
class Benz implements Car{
public void run(){
System.out.println("benz run......");
}
}
class BMW implements Car{
public void run(){
System.out.println("bmw run......");
}
}
class Driver{
//让Driver类依赖一个Car这个接口
public void drive(Car car){
car.run();
}
}
public class Client{
public static void main(String[] args){
Driver driver = new Driver();
driver.drive(new BMW());
}
}
这样之后再增加的车的种类,只需要修改客户端传递给Driver类的drive方法的类型就可以了
②接口和抽象类不依赖于实现类。
③实现类依赖接口或者抽象类。
2. 对象的依赖关系有三种实现方式。
①构造函数传递依赖对象。
class Driver{
private Car car;
Driver(Car car){
this.car = car;
}
public void drive(){
this.car.run();
}
}
②Setter传递依赖对象。
class Driver{
private Car car;
public void setCar(Car car){
this.car = car;
}
public void drive(){
this.car.run();
}
}
③接口声明依赖对象,也叫接口注入。
class Driver{
public void drive(Car car){
car.run();
}
}
接口隔离原则(ISP)
Interface Segregation Principle:接口隔离原则
1.定义: 建立单一接口,不要建立臃肿庞大的接口。即接口尽量细化,同时接口中的方法尽量少。在这里提一下单一职责和接口隔离原则的区别。首先两个侧重点是不一样的,
单一职责要求类和接口,或者方法的职责单一,侧重点在职责,这是根据业务逻辑进行划分的。而接口隔离原则要接口中的方法尽量少。比如,一个接口或者一个中有十个方法,不
同的方法做不同的事情,但是这个接口总体就是处理一件事情,然后具体细分成了10个方法。不同的模块根据不同的权限进行访问,这是符单一职责原则的。但是按照接口隔离的原
则是要求接口接口中的方法尽量少,落实到这个实例就是要求尽量多几个专门的接口供不同的模块使用,而不是只有一个臃肿的接口,依据权限去限制不同模块可以访问的方法。
2.接口隔离原则是对接口定义的规范。含义主要包含以下4点。
①接口尽量小。根据具体业务把一个接口按照接口隔离原则细化成更多的接口。但是在此基础之上必须不能违背单一职责原则。
②接口要高内聚。高内聚的意思就是提高接口和类处理能力,减少对外的交互。接口是对外的承诺,因此设计时应该尽量少公布接口中的public方法,承诺越少系统开发越有利且变更风险就越少。
③定制服务。定制服务就是单独为一个个体提供服务,即只提供访问者需要的方法。
举一个图书管理系统的例子,有一个查询接口BookSearch,包括如下方法:
searchById,
searchByBookName,
searchByCategory,
complexSearch,
其中前三个方法是提供给学生使用的,后一个方法是提供给管理员使用的,学生对这个方法的访问是有限制的,调用不会返回任何值。当这四个方法全部公布出去之后,学生对此方法的访问即使不返回任何值也会使服务器性能下降。因此合理的设计应该是:
拆分这个接口为两个接口:SimpleSearch和AdminSearch。
SimpleSearch接口提供
searchById,searchByBookName,searchByCategory方法,
AdminSearch接口提供complexSearch方法,此时学生实现SimpleSearch接口即可,管理员同时实现SimpleSearch和AdminSearch两个接口。
④接口设计是有限度的。接口设计越小越好,但是结构同时会变得复杂,维护也变得难了。因此就要把握住这个度。
合成复用原则
设计模式与设计原则是互相促进的。设计原则的诞生,促成新的设计模式;设计模式的出现,会抽象出新的设计原则。
迪米特法则(Law of Demeter)
1. 定义: 迪米特法则也叫做最少知识原则(Least Knowledge Principle,LKP),即一个对象应该对其他对象有最少的了解,也就是说一个类要对自己需要耦合或者调用的类知道的最少。我只知道你有多少public方法可以供我调用,而其他的一切都与我无关。
2. 迪米特法则是对类的低耦合做处理明确的要求,在此举一个例子:学校领导老师点名,老师让体育委员清点人数。其中第二段代码的耦合性较第一段代码有所改善。
import java.util.List;
import java.util.ArrayList;
class Teacher{
public void command(){
System.out.println("老师接到命令,委托体育委员清点人数......");
//耦合了Students类
List<Student> students = new ArrayList<Student>();
for(int i = 0; i < 20; i++ ){
students.add(new Student());
}
//耦合了StudentLeader类
StudentLeader StudentLeader = new StudentLeader();
int counts = StudentLeader.counts(students);
System.out.println("老师委托体育委员清点人数完毕......");
System.out.println("老师报告学校领导,人数为"+counts);
}
}
class Student{
}
class StudentLeader{
//耦合了Students类
public int counts(List<Student> students){
System.out.println("体育委员开始清点人数......");
int counts = students.size();
System.out.println("体育委员清点结束,人数为"+counts+",并且返回人数给老师");
return counts;
}
}
public class Client{
public static void main(String[] args){
System.out.println("周末收假,学校领导命令老师去点名.....");
Teacher teacher = new Teacher();
teacher.command();
}
}
控制台:
周末收假,学校领导命令老师去点名.....
老师接到命令,委托体育委员清点人数......
体育委员开始清点人数......
体育委员清点结束,人数为20,并且返回人数给老师
老师委托体育委员清点人数完毕......
老师报告学校领导,人数为20
import java.util.List;
import java.util.ArrayList;
class Teacher{
//只需要耦合体育委员 而无需知道students类,降低了Teacher类和Student类的耦合
public void command(StudentLeader StudentLeader ){
System.out.println("老师接到命令,委托体育委员清点人数......");
StudentLeader.counts();
}
}
class Student{
}
class StudentLeader{
private List<Student> students;
public StudentLeader(List<Student> students){
this.students = students;
}
public void counts(){
System.out.println("体育委员开始清点人数......");
int counts = students.size();
System.out.println("体育委员清点结束,人数为"+counts);
}
}
public class Client{
public static void main(String[] args){
System.out.println("周末收假,学校领导命令老师去点名.....");
List<Student> students = new ArrayList<Student>();
for(int i = 0; i < 20; i++ ){
students.add(new Student());
}
Teacher teacher = new Teacher();
teacher.command(new StudentLeader(students));
}
}
控制台:
周末收假,学校领导命令老师去点名.....
老师接到命令,委托体育委员清点人数......
体育委员开始清点人数......
体育委员清点结束,人数为20
3. 迪米特法则的核心观念就是类间解耦,最终可能产生的结果就是会产生了大量的中转类。为了把解耦做到极致导致实现一个业务逻辑的实现跳转了很多类,这也是不可取的做法。因此根据实际权衡利弊才是重要的。
(ref : https://www.cnblogs.com/HouJiao/p/5459022.html)