一、单一职责原则
单一职责原则(Single Responsibility Principle),简称SRP。
定义:
There should never be more than one reason for a class to change.
应该有且仅有一个原因引起类的变更。
有时候,开发人员设计接口的时候会有些问题,比如用户的属性和用户的行为被放在一个接口中声明。这就造成了业务对象和业务逻辑被放在了一起,这样就造成了这个接口有两种职责,接口职责不明确,按照SRP的定义就违背了接口的单一职责原则了。
下面是个例子:
package com.loulijun.chapter1;
public interface Itutu {
//身高
void setShengao(double height);
double getShengao();
//体重
void setTizhong(double weight);
double getTizhong();
//吃饭
boolean chiFan(boolean hungry);
//上网
boolean shangWang(boolean silly);
}
上面的例子就存在这个问题,身高、体重属于业务对象,与之相应的方法主要负责用户的属性。而吃饭、上网是相应的业务逻辑,主要负责用户的行为。但是这就会给人一种不知道这个接口到底是做什么的感觉,职责不清晰,后期维护的时候也会造成各种各样的问题。
解决办法:单一职责原则,将这个接口分解成两个职责不同的接口即可
ItutuBO.java:负责tutu(涂涂,假如是个人名)的属性
package com.loulijun.chapter1;
/**
* BO:Bussiness Object,业务对象
* 负责用户的属性
* @author Administrator
*
*/
public interface ItutuBO {
//身高
void setShengao(double height);
double getShengao();
//体重
void setTizhong(double weight);
double getTizhong();
}
ItutuBL.java:负责涂涂的行为
package com.loulijun.chapter1;
/**
* BL:Business Logic,业务逻辑
* 负责用户的行为
* @author Administrator
*
*/
public interface ItutuBL {
//吃饭
boolean chiFan(boolean hungry);
//上网
boolean shangWang(boolean silly);
}
这样就实现了接口的单一职责。那么实现接口的时候,就需要有两个不同的类
TutuBO.java
package com.loulijun.chapter1;
public class TutuBO implements ItutuBO {
private double height;
private double weight;
@Override
public double getShengao() {
return height;
}
@Override
public double getTizhong() {
return weight;
}
@Override
public void setShengao(double height) {
this.height = height;
}
@Override
public void setTizhong(double weight) {
this.weight = weight;
}
}
package com.loulijun.chapter1;
public class TutuBL implements ItutuBL {
@Override
public boolean chiFan(boolean hungry) {
if(hungry)
{
System.out.println("去吃火锅...");
return true;
}
return false;
}
@Override
public boolean shangWang(boolean silly) {
if(silly)
{
System.out.println("好无聊啊,上会网...");
return true;
}
return false;
}
}
这样就清晰了,当需要修改用户属性的时候只需要对ItutuBO这个接口来修改,只会影响到TutuBO这个类,不会影响其他类。
那么单一职责原则的意义何在呢?
1、降低类的复杂性,实现什么样的职责都有清晰的定义
2、提高可读性
3、提高可维护性
4、降低变更引起的风险,对系统扩展性和维护性很有帮助
但是、使用单一职责原则有一个问题,“职责”没有一个明确的划分标准,如果把职责划分的太细的话会导致接口和实现类的数量剧增,反而提高了复杂度,降低了代码的可维护性。所以使用这个职责的时候还要具体情况具体分析。建议就是接口一定要采用单一职责原则,实现类的设计上尽可能做到单一职责原则,最好是一个原因引起一个类的变化。
二、里氏置换原则
里氏置换原则(Liskov Substitution Principle),简称LSP
定义:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须能够透明的使用其子类对象。
也就是说,只要父类出现的地方子类就能够出现,而且替换为子类不会产生任何错误或异常。但是反过来,子类出现的地方,替换为父类就可能出现问题了。
这个原则是为良好的继承定义一个规范,简单的讲,有4层含义:
一、子类必须完全实现父类的方法
public abstract class ViewPoint {
//去丽江旅游
public abstract void where();
}
下面两个类是实现这个抽象类
public class Lijiang extends ViewPoint {
@Override
public void where() {
System.out.println("欢迎来到丽江...");
}
}
public class Zhangjiajie extends ViewPoint {
@Override
public void where() {
System.out.println("欢迎来到张家界...");
}
}
人物是涂涂,在里面设置类类型来传递参数。此时涂涂要去的旅游景点还是抽象的
public class Tutu {
//定义要旅游的景点
private ViewPoint viewpoint;
//涂涂要去的景点
public void setViewPoint(ViewPoint viewpoint)
{
this.viewpoint = viewpoint;
}
public void travelTo()
{
System.out.println("涂涂要去旅游了");
viewpoint.where();
}
}
场景类:设置具体要去的景点
public class Sence {
public static void main(String args[])
{
Tutu tutu = new Tutu();
//设置要去的旅游景点
tutu.setViewPoint(new Lijiang());
tutu.travelTo();
}
}
运行结果:
涂涂要去旅游了
欢迎来到丽江...
二、子类可以有自己的特性
三、覆盖或者实现父类的方法时输入参数可以被放大
父类能够存在的地方,子类就能存在,并且不会对运行结果有变动。反之则不行。
父类,say()里面的参数是HashMap类型,是Map类型的子类型。(因为子类的范围应该比父类大)
import java.util.Collection;
import java.util.HashMap;
public class Father {
public Collection say(HashMap map)
{
System.out.println("父类被执行...");
return map.values();
}
}
子类,say()里面的参数变成了Map类型,Map范围比HashMap类型大,符合LSP原则。注意这里的say不是覆写父类的say,因为参数类型不同。而是重载。
import java.util.Collection;
import java.util.Map;
/*
* 子类继承了父类的所有属性
*/
public class Son extends Father {
//方法输入参数类型
public Collection say(Map map)
{
System.out.println("子类被执行...");
return map.values();
}
}
import java.util.HashMap;
public class Home {
public static void main(String args[])
{
invoke();
}
public static void invoke()
{
//父类存在的地方,子类就应该能够存在
//Father f = new Father();
Son s = new Son();
HashMap map = new HashMap();
//f.say(map);
s.say(map);
}
}
无论是用父类还是子类调用say方法,得到的结果都是
父类被执行...
但是,如果将上面Father里的say参数改为Map,子类Son里的say参数改为HashMap,得到的结果就变成了
f.say(map)结果:父类被执行...
s.say(map)结果: 子类被执行...
这样会造成逻辑混乱。所以子类中方法的前置条件必须与父类中被覆写的前置条件相同或者更宽。
四、覆写或者实现父类的方法时输出结果可以被缩小
其实与上面的类似,也就是父类能出现的地方子类就可以出现,而且替换为子类不会产生任何错误或者异常,使用者也无需知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就适应。(毕竟子类的范围要>=父类的范围)
三、依赖倒置原则
依赖倒置原则(Dependence Inversion Principle),简称DIP
定义
High level modules should 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、通过构造函数传递依赖对象
比如在构造函数中的需要传递的参数是抽象类或接口的方式实现。
2、通过setter方法传递依赖对象
即在我们设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象
3、接口声明实现依赖对象
例如下面的例子
涂涂是个女生
public class Tutu {
//涂涂是个女孩,会煮面
public void cook(Noodles noodles)
{
noodles.eat();
}
}
面条(目前只会煮面)
public class Noodles {
//吃面条
public void eat()
{
System.out.println("涂涂吃面条...");
}
}
涂涂坐在家里吃面(场景类)
public class Home {
public static void main(String args[])
{
Tutu tutu = new Tutu();
Noodles food = new Noodles();
tutu.cook(food);
}
}
运行结果:涂涂吃面条...
但是这有个问题,涂涂只会做面条,不可能每次都吃面条吧,天天吃面吃死你,所以在上面的Tutu类中的cook方法中,如果涂涂会做其他吃的,那岂不是更好。于是她向家庭主妇迈进了一步,使用了依赖倒置原则。
也就是涂涂通过学习还可以焖米饭,炒鱿鱼(虽然听着不爽,但是很好吃),京酱肉丝啊等等。要想在代码中实现,就需要实现接口:IFood。
除了与吃相关的东西外,涂涂还会打球、游泳等,要想在代码中实现,需要实现接口ITutu。(省略实现)
public interface ITutu {
//这样就会做很多饭菜了
public void cook(IFood food);
}
实现类
public class Tutu implements ITutu {
@Override
public void cook(IFood food) {
food.eat();
}
}
食物接口
public interface IFood {
public void eat();
}
这样就为扩展留出了很大的空间,方面扩展其他的类。也不会对细节有变动。以后涂涂想吃什么学一下就可以自己做了
实现面条
public class Noodles implements IFood {
@Override
public void eat() {
System.out.println("涂涂吃面条...");
}
}
实现米饭
public class Rice implements IFood {
@Override
public void eat() {
System.out.println("涂涂吃米饭(终于吃上米饭了)...");
}
}
场景类:涂涂在家里开吃了,想吃什么直接做就是了
public class Home {
public static void main(String args[])
{
//接口使不能实例化滴
ITutu tutu = new Tutu();
//实例化米饭,涂涂可以吃米饭了
IFood rice = new Rice();
//吃面条
//IFood noodles = new Noodles();
tutu.cook(rice);
}
}
这样各个类或模块的实现彼此独立,不互相影响,实现了模块间的松耦合。
四、接口隔离原则
首先看看接口隔离原则的定义,有两种定义
第一种: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.(类间的依赖关系应该建立在最小的接口上)
而这里的接口,却不仅仅指的是通过interface关键字定义的接口,接口分为2种:
1、对象接口(Object Interface)
JAVA中声明的一个类,通过new关键字产生的一个实例,它是对一个类型的事物的描述,这也是一种接口。例如:
Phone phone = new Phone();这里的类Person就是实例phone的一个接口
2、类接口(Class Interface)
这种接口就是通过interface关键字定义的接口了
也就是说,接口隔离原则要求的是在一个模块应该只依赖它需要的接口,以保证接口的小纯洁。而且需要保证接口应该尽量小,即设计接口的时候应该让接口尽量细化,不要定义太臃肿的接口(比如接口中有很多不相干的逻辑的方法声明)。
接口隔离原则与单一职责原则有些相似,不过不同在于:单一职责原则要求的是类和接口职责单一,注重的是职责,是业务逻辑上的划分。而接口隔离原则要求的是接口的方法尽量少,尽量有用(针对一个模块)
在使用接口隔离原则的时候需要有一些规范:
1、接口尽量小
接口尽量小主要是为了保证一个接口只服务一个子模块或者业务逻辑
2、接口高内聚
接口高内聚是对内高度依赖,对外尽可能隔离。即一个接口内部的声明的方法相互之间都与某一个子模块相关,且是这个子模块必须的。
3、接口设计是有限度的
但是如果完全遵循接口隔离原则的话,会出现一个问题。即接口的设计力度会越来越小,这样就造成了接口数量剧增,系统复杂度一下子增加了,而这不是真实项目所需要的,所以在使用这个原则的时候还要在特定的项目,根据经验或者尝试判断,不过没有一个固定的标准。
举个例子
在春熙路上逛街,到处都是女的,有穿着丝袜的大妈(恶心一下),有文静的女生,有性感的辣妹,总之很多女的。然而当你对前面喊一声“美女,钱掉了”,估计前面连同大妈一起回头看看,以为在叫自己。如今美女这个词已经变得大众化了,反正跟女的打招呼就说美女。但是真正的美女是这样吗,男淫们心中的美女应该是这样的:身材好、长相好、气质佳。
IPrettyGirl.java:定义美女标准
//定义美女接口
public interface IPrettyGirl {
//长相好
public void greatLooks();
//好身材
public void greatFigure();
//气质佳
public void greatTemperament();
}
PrettyGril.java:实现美女类
public class PrettyGirl implements IPrettyGirl {
private String name;
//构造函数,美女名字
public PrettyGirl(String name)
{
this.name = name;
}
//好身材
@Override
public void greatFigure() {
System.out.println(name+":身材非常好");
}
//好长相
@Override
public void greatLooks() {
System.out.println(name+":长相非常好");
}
//好气质
@Override
public void greatTemperament() {
System.out.println(name+":气质非常好");
}
}
AMan:是个抽象类,抽象出一个男人来
public abstract class AMan {
protected IPrettyGirl prettyGirl;
public AMan(IPrettyGirl prettyGirl)
{
this.prettyGirl = prettyGirl;
}
//帅哥开始找美女啦
public abstract void findGirl();
}
Man:
public class Man extends AMan {
public Man(IPrettyGirl prettyGirl) {
super(prettyGirl);
}
@Override
public void findGirl() {
System.out.println("美女在这里:----------------------");
super.prettyGirl.greatLooks();
super.prettyGirl.greatFigure();
super.prettyGirl.greatTemperament();
}
}
场景类,春熙路,帅哥开始寻找美女了
public class ChunxiRoad {
public static void main(String args[])
{
IPrettyGirl jiajia = new PrettyGirl("佳佳");
AMan man = new Man(jiajia);
man.findGirl();
}
}
运行结果:
美女在这里:----------------------
佳佳:长相非常好
佳佳:身材非常好
佳佳:气质非常好
但是这里有个问题,接口划分的不是很清晰,比如有些男人认为某些女生长相好,身材好就是美女(而不管这个女的素质啥样,比如那些“压脉带”)。而某些女生虽然长相一般,身材也一般,但是气质很好,这就是某些宅男心目中的气质型美女,或者三者具备。所以需要把接口划分的再细一点以满足不同男人的审美观。
我们把接口分为两种
好身材
public interface IGreatBody {
//好长相
public void greatLooks();
//身材
public void greatFigure();
}
好气质
public interface IGreatTemperament {
//气质好
public void greatTemperament();
}
然后我们就可以根据自己的需求来寻找自己心目中认为的美女啦,举个例子
上面的AMan和Man传递的接口变为IGreatBody
接下来,我们来到了--东京。看到了cang老师
public class Tokyo {
public static void main(String args[])
{
IGreatBody canglaoshi = new PrettyGirl("cang老师");
AMan man = new Man(canglaoshi);
man.findGirl();
}
}
运行结果:
美女在这里:----------------------
cang老师:长相非常好
cang老师:身材非常好
五、迪米特法则
迪米特法则(Law of emeter)
定义:一个对象应该对其他对象了解最少
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用性才可以提高。
形象一点的比喻类似于:监狱内的犯人是不应该跟外面的人接触的,当然或许会有探亲的。这里的监狱就是类,里面的犯人就是类内部的信息,而监狱里的狱警就相当于迪米特法则的执行者
举个例子
家人探望犯人
家人:家人只与犯人是亲人,但是不认识他的狱友
package com.loulijun.chapter5;
public class Family {
//家人探望犯人
public void visitPrisoner(Prisoners prisoners)
{
//家人希望犯人与狱友互帮互助
Inmates inmates = prisoners.helpEachOther();
//狱友说,我们是盟友
inmates.weAreFriend();
}
}
犯人:犯人与家人是亲人,犯人与狱友是朋友
package com.loulijun.chapter5;
public class Prisoners {
private Inmates inmates = new Inmates();
public Inmates helpEachOther()
{
System.out.println("家人说:你和狱友之间应该互相帮助...");
return inmates;
}
}
狱友:犯人与狱友是朋友,但是不认识他的家人
package com.loulijun.chapter5;
//Inmates是狱友的意思
public class Inmates {
public void weAreFriend()
{
System.out.println("狱友说:我们是狱友...");
}
}
场景类:发生在监狱里
package com.loulijun.chapter5;
public class Prison {
public static void main(String args[])
{
Family family = new Family();
family.visitPrisoner(new Prisoners());
}
}
运行结果:
家人说:你和狱友之间应该互相帮助...
狱友说:我们是狱友...
看到这样的结果,是不是有些别扭,家人告诉犯人要与狱友好好相处,而狱友确冒出来说话。这显然越界了,因为监狱只允许家人探望犯人,而不是随便谁都可以见的
这里的家人和狱友有了沟通是违背迪米特法则的,所以我们需要将家人和狱友隔离开,对其进行重构
家人
package com.loulijun.chapter5;
public class Family {
//家人探望犯人
public void visitPrisoner(Prisoners prisoners)
{
System.out.print("家人说:");
prisoners.helpEachOther();
}
}
犯人
package com.loulijun.chapter5;
public class Prisoners {
private Inmates inmates = new Inmates();
public Inmates helpEachOther()
{
System.out.println("犯人和狱友之间应该互相帮助...");
System.out.print("犯人说:");
inmates.weAreFriend();
return inmates;
}
}
狱友
package com.loulijun.chapter5;
//Inmates是狱友的意思
public class Inmates {
public void weAreFriend()
{
System.out.println("我们是狱友...");
}
}
监狱
package com.loulijun.chapter5;
public class Prison {
public static void main(String args[])
{
Family family = new Family();
family.visitPrisoner(new Prisoners());
}
}
运行结果
家人说:犯人和狱友之间应该互相帮助...
犯人说:我们是狱友...
这样家人和狱友就分开了,但是也表达了家人希望狱友能跟犯人互相帮助的意愿。也就是两个类通过第三个类实现信息传递
网上还有如下一些关于应用迪米特法则的注意事项:
① 在类的划分上,应该创建有弱耦合的类;
② 在类的结构设计上,每一个类都应当尽量降低成员的访问权限;
③ 在类的设计上,只要有可能,一个类应当设计成不变类;
④ 在对其他类的引用上,一个对象对其它对象的引用应当降到最低;
⑤ 尽量降低类的访问权限;
⑥ 谨慎使用序列化功能;
⑦ 不要暴露类成员,而应该提供相应的访问器(属性)。
六、开闭原则
开闭原则(Open Closed Principle)
开闭原则的核心是:对扩展开放,对修改关闭
我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。
其实笔者认为,开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
说到这里,再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
下面是使用开闭原则的一个简单示例,虽有些不准确,但是是这个意思(领会精神)
定义一个接口,寻找美女
package com.loulijun.chapter6;
public interface IFindGirl {
//年龄
public int getAge();
//姓名
public String getName();
//长相
public String getFace();
//身材
public String getFigure();
}
实现这个接口
package com.loulijun.chapter6;
public class FindGirl implements IFindGirl {
private String name;
private int age;
private String face;
private String figure;
public FindGirl(String name, int age, String face, String figure)
{
this.name = name;
this.age = age;
this.face = face;
this.figure = figure;
}
@Override
public int getAge() {
return age;
}
@Override
public String getFace() {
return face;
}
@Override
public String getFigure() {
return figure;
}
@Override
public String getName() {
return name;
}
}
场景:大街上
package com.loulijun.chapter6;
import java.text.NumberFormat;
import java.util.ArrayList;
public class Street {
private final static ArrayList<IFindGirl> girls = new ArrayList<IFindGirl>();
//静态初始化块
static
{
girls.add(new FindGirl("张含韵",23,"可爱型","165cm/47kg"));
girls.add(new FindGirl("高圆圆",33,"时尚型","165cm/48kg"));
girls.add(new FindGirl("章泽天",19,"清纯型","168cm/47kg"));
}
public static void main(String args[])
{
System.out.println("----------美女在这里----------");
for(IFindGirl girl:girls)
{
System.out.println("姓名:"+girl.getName()+" 年龄:"+girl.getAge()+
" 长相:"+girl.getFace()+" 身材:"+girl.getFigure());
}
}
}
运行结果:
----------美女在这里----------
姓名:张含韵 年龄:23 长相:可爱型 身材:165cm/47kg
姓名:高圆圆 年龄:33 长相:时尚型 身材:165cm/48kg
姓名:章泽天 年龄:19 长相:清纯型 身材:168cm/47kg
但是如果想独立分出一个外国美女的类别的话(比如增加一个国籍),可以通过修改接口、修改实现类、通过扩展来实现。
如果修改接口,也就意味着修改实现类,这样对项目的变动太大了,所以不推荐
如果修改实现类,这样虽能解决问题,但是明显有些牵强,如果需要其他变动的时候会显得逻辑混乱
所以,通过扩展来实现是最简单的方式
如何扩展:
可以定义一个IForeigner接口继承自IFindGirl,在IForeigner接口中添加国籍属性getCountry(),然后实现这个接口即可,然后就只需要在场景类中做稍微修改就可以了
package com.loulijun.chapter6;
public interface IForeigner extends IFindGirl {
//国籍
public String getCountry();
}
实现接口
package com.loulijun.chapter6;
public class ForeignerGirl implements IForeigner {
private String name;
private int age;
private String country;
private String face;
private String figure;
public ForeignerGirl(String name, int age, String country, String face, String figure)
{
this.name = name;
this.age = age;
this.country = country;
this.face =face;
this.figure = figure;
}
@Override
public String getCountry() {
// TODO Auto-generated method stub
return country;
}
@Override
public int getAge() {
// TODO Auto-generated method stub
return age;
}
@Override
public String getFace() {
// TODO Auto-generated method stub
return face;
}
@Override
public String getFigure() {
// TODO Auto-generated method stub
return figure;
}
@Override
public String getName() {
// TODO Auto-generated method stub
return name;
}
}
然后在场景类中只需要修改如下代码即可,其他不变
girls.add(
new
ForeignerGirl(
"Avirl"
,
28
,
"美国"
,
"性感型"
,
"160cm/45kg"
));
|
不过这些设计原则到不是绝对的,而是根据项目需求,实际需求来定夺使用