面向对象六大设计原则
单一职责(Single Responsibility Principle)
引起类变化的原因只有一个,即一个类就负责一件事情
单一职责要求定义一个类(不仅仅是类,大到模块,小到方法)时,功能要尽可能的简单,定义的越复杂,它能被可重用的可能性就越小。
比如,常见的管理系统中,要在页面上展示一个列表数据,类结构如下:
public interface UserApi<T> {
/**
* 获取数据库连接.
*
* @param url url
* @param driverClass driverClass
* @param user user
* @param password password
* @return connect
*/
Connection getConnection(String url, String driverClass, String user, String password);
/**
* 根据查询条件查询用户.
*
* @param condition 查询条件
* @return 返回符合条件的用户
*/
List<T> listUsers(T condition);
/**
* 对查询到的用户进行额外处理.
*
* @param list 用户集合
* @return 最终前台展示数据
*/
List<T> handleData(List list);
}
上类中,UserApi就承担的职责过多,既要获取连接,又要从数据库获取数据,还要在拿到数据之后做一些处理操作,这个类太“累”了。此时,如果按照 数据如何来
来重新划分职责:可以将UserApi这个类拆分为三类:
- DataSource:含有getConnection方法,专门获取用于数据库连接。
- UserDao:含有listUsers方法,专门用于从数据库获取数据。
- UserService:含有handleData方法,目的在于获取到数据库的数据后,进一步对数据进行业务处理。
上面这几种拆法,就是我们开发中常用的分层思想,将系统大概分为 dao、service、controller三层,这也是单一职责这一原则的体现。
上面是按 数据如何来
进行职责划分,所以比较关注数据的周期历程,从哪儿来,经过了什么。如果再将职责放大一点,按照 处理数据
来划分,那么就不会关注数据怎么来的了,只要有数据来就行了,重点应该就在怎么处理数据了,就会对handleData方法进行开刀。
当然了,一般在这种情况下我们不会这样去划分职责,这里仅仅是想说明单一职责的 职责
是很难去量化的,我们应该根据实际情况尽可能对类的职责进行细化(接口一定要做到单一职责,细化
)。
这样的思想不仅仅是只适用类,也适用于方法,例如,在 handleData(List list)
方法中,如果需要对 list
这个参数进行校验,那么就要将该方法拆分为 check(list)
和 handleData(list)
,因为参数校验这个方法很有可能会被其他方法调用,这样可以提高复用性。
单一职责原则能够 降低类的复杂性,提高可读性和可维护性,但是如果对职责量化适度,就会导致类暴增。
里氏替换原则(Liskov Substitution principle)
所有引用基类的地方必须能透明地使用其子类的对象
简而言之,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立。
举个栗子:基类——人,有个方法——生娃。子类可能是,亚洲人,非洲人,欧洲人,然后我让他们生娃,他们都能生出娃,而不是别的小动物或者无机物,这就满足了里氏替换原则(LSP)。举个不满足的加深理解:基类——人,有个方法——生娃。子类有超人,机器人,蜘蛛人。看似都具有人的基本特征,但是其中机器人让他生娃,是无法做到的,所以不满足里氏替换,所以机器人不能作为人的基类去定义。
父类:人,人都可以生娃,子类可以是亚洲人、欧洲人、非洲人,因为他们都能生娃,这些子类满足LSP,但子类不能是机器人,因为机器人不能生娃(至少现在是不能生的),所以机器人不能继承人,不满足LSP。
LSP原则还有个要求就是
子类的输入参数宽于或等于父类的输入参数
来个例子(参照秦小波《设计模式之禅》):
public class Father {
public Collection doSomeThing(HashMap map) {
System.out.println("父类被执行。。。");
return map.values();
}
public static void main(String[] args) {
Father father = new Father();
father.doSomeThing(new HashMap());
Son son = new Son();
son.doSomeThing(new HashMap());
}
}
class Son extends Father {
// 重载,放大输入参数类型
public Collection doSomeThing(Map map) {
System.out.println("子类被执行。。。");
return map.values();
}
}
输出结果:
父类被执行。。。
子类被执行。。。
如果将父类和子类的入参类型进行对换:
public class Father {
public Collection doSomeThing(Map map) {
System.out.println("父类被执行。。。");
return map.values();
}
public static void main(String[] args) {
Father father = new Father();
father.doSomeThing(new HashMap());
Son son = new Son();
son.doSomeThing(new HashMap());
}
}
class Son extends Father {
// 重载,缩小输入参数类型
public Collection doSomeThing(HashMap map) {
System.out.println("子类被执行。。。");
return map.values();
}
}
输出结果:
父类被执行。。。
子类被执行。。。
子类的方法被执行了,可能会导致系统逻辑混乱,所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或更宽松。
里氏替换原则可以增强程序的健壮性、重用性、可扩展性。
依赖倒置原则(Dependence Inversion Principle)
High level modules should not depend upon low level modules.Both should depend upon bstractions.
Abstractions should not depend on details. Details should depend on abstractions.
简而言之,包含了以下含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
- 抽象不应该依赖细节。
- 细节应该依赖抽象。
先来个反面教材:
public class DipClient {
public static void main(String[] args) {
Driver driver = new Driver();
Geely geely = new Geely();
driver.drive(geely);
}
static class Driver {
public void drive(Geely geely) {
geely.run();
}
}
static class Geely {
public void run() {
System.out.println("吉利汽车跑起来了。。。");
}
}
}
很明显,这样的设计是不符合DIP原则的,如果突然有一天换车了,把吉利换成比亚迪,那岂不是没办法开了。Driver作为高层模块,依赖了低层模块的汽车,不同的汽车类对Driver都有影响,这样是不符合逻辑的。
做出以下修改:
public class DipClient {
public static void main(String[] args) {
ICar car = new Geely();
//驾驶员开吉利汽车
Driver driver = new Driver();
driver.drive(car);
//驾驶员开比亚迪
driver.drive(new BYD());
}
interface ICar {
void run();
}
static class Geely implements ICar {
@Override
public void run() {
System.out.println("吉利汽车跑起来了。。。");
}
}
static class BYD implements ICar {
@Override
public void run() {
System.out.println("比亚迪跑起来了。。。build your dreams");
}
}
static class Driver {
public void drive(ICar iCar) {
iCar.run();
}
}
}
修改之后的结构,Driver由原来的依赖具体汽车(Geely),变成了依赖ICar抽象,并且Geely也依赖ICar抽象,发生了“倒置”;驾驶员只依赖汽车,不管它是什么汽车,。当然了还可以进一步抽象,Driver可以有很多种,A驾、B驾等等,开的车都不一样,所以就可以对ICar和Driver进行进一步抽象。
依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,提高扩展性。其核心思想就是OOP(面向接口编程)。
接口隔离原则(Interface Segregation Principle)
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。一个类对另外一个类的依赖性应当是建立在最小的接口上的。
以上对ISP的两种定义,可简要概括为:
接口应该尽量细化。这可能会和单一职责有些相似,单一职责和接口隔离的观察角度不同,单一职责要求类的
职责
应该尽量单一,是从业务逻辑区分的。
什么是码帝(专家):
public interface Expert {
// 精通java.
void proficientjava();
// 精通JavaScript.
void proficientJs();
// 精通c#.
void proficientCSharp();
}
上面这个case,定义了要成为一个编程专家所具备的特点。这么定义的问题是,我如果是个专家,实现了这个接口,那么必须实现这个接口的所有方法,换言之,我是编程专家,那么必须要 精通 Java、JavaScript、C#,然而,术业有专攻,虽然计算机语言有很多相同的地方,但是要是同时精通三门语言,还是比较困难的。一般情况下,只要精通一种,就已经是高手了。
显然,这么定义高手是不符合现实要求的,根据接口隔离原则,应该讲专家的特点拆分:Javaexpert、JsExpert、CSharpExpert,只要实现了其中一个接口,就已经是专家了。当然了会有同时精通java、js、C#的资深专家,那么只需同时实现这三个接口即可。
接口隔离原则需注意:
- 接口尽量细、小
- 接口要高内聚(提高处理能力的同时减少对外的交互)(少喊口号多做事)。
- 设计接口粒度的时候要有限度(根据自身系统情况设计)
迪米特法则Law of Demeter | Least Knowledge Principle)
也叫
最少知识原则(Least Knowledge Principle)
,一个对象应该对其它对象有最少的了解,简单讲,一个类应该对自己需要耦合或调用的类知道得最少。即:Only talk to your immediate friends.(只与直接的朋友通信)
以老师叫学习委员收全班同学的课后作业为例:
public class LodClient {
public static void main(String[] args) {
Teacher teacher = new Teacher();
teacher.command(new Commissary());
}
// 老师类.
static class Teacher {
public void command(Commissary commissary) {
// 模拟生成班上同学.
List<Student> students = new ArrayList<>();
for (int i = 0; i < 30; i++) {
students.add(new Student());
}
//学习委员收作业
commissary.collectWork(students);
}
}
// 学习委员.
static class Commissary {
public void collectWork(List<Student> student) {
System.out.println("学习委员一共强制收了:" + student.size() + "份作业!");
}
}
//学生
static class Student { }
}
首先确定一下Teacher类有几个朋友类——出现在成员变量、方法的输入输出参数中的类称为成员朋友类,出现在方法体内部的类不属于朋友类。而Student这个类出现在command
方法体内,因此不属于Teacher的朋友类,但是它却与command
有了交流,和陌生人进行了交流,违反了只与直接的朋友通信
这一原则,也就是违反了LOD原则。
经过简单修改后:
public class LodClient {
public static void main(String[] args) {
// 模拟生成班上同学.
List<Student> students = new ArrayList<>();
for (int i = 0; i < 30; i++) {
students.add(new Student());
}
Teacher teacher = new Teacher();
Commissary commissary = new Commissary(students);
// 老师给学习发布收作业命令.
teacher.command(commissary);
}
// 老师类.
static class Teacher {
public void command(Commissary commissary) {
//学习委员收作业
commissary.collectWork();
}
}
// 学习委员.
static class Commissary {
private List<Student> students;
public Commissary(List<Student> students) {
this.students = students;
}
public void collectWork() {
System.out.println("学习委员一共强制收了:" + this.students.size() + "份作业!");
}
}
//学生
static class Student { }
}
修改之后的类,Teacher避免和学生产生关系,只与学习委员有关系,降低了系统的耦合。迪米特原则为什么要求对自己需要耦合或调用的类知道得最少?举个例子,你去4S店修车,你关心这个车具体是怎么修的吗?你只需关心你的车能不能修好!车上的零部件那么多,你越想了解就觉得越“类”。
开闭原则(Open Closed Principle)
Software entities like classes, modules and functions should be open for extension but closed for modification.(软件实体如类、模块、函数应该对扩展开放,对修改关闭。)
接下来以改编自业界盛传的某超级大厂的网红代码为例:
public class VIPCenter {
/**
* user共同属性
*/
class BaseUser {
//do something
}
/**
* 穷逼user,送的那种
*/
class SlumDogVIP extends BaseUser {
//do something
}
/**
* 正儿八经买vip的user
*/
class RealVIP extends BaseUser {
//do something
}
/**
* 实际业务处理
*/
public void serviceVIP(BaseUser user) {
if (user instanceof SlumDogVIP) {
//穷逼VIP,活动送的那种
System.out.println("穷逼");
} else {
//正儿八经的VIP
System.out.println("不穷");
}
}
}
对于这段代码,业务逻辑集中在一起,当出现新的用户类型时, 比如,增加一个SuperVIP用户类型,这就需要直接去修改服务方法代码实现(在serviceVIP方法里面在加else if),违反了开关原则(Open-Close)(对新增开放,对修改关闭)。这可能会意外影响不相关的某个用户类型逻辑。
对业务处理类进行抽象改进:
public class VIPCenter {
//抽象策略
private ServiceHandler serviceHandler;
//构造函数设置具体策略
public VIPCenter(ServiceHandler serviceHandler) {
this.serviceHandler = serviceHandler;
}
/**
* Context封装角色
* 封装后的策略方法
*/
public void serviceVIP(BaseUser user) {
this.serviceHandler.service(user);
}
/*------------------------------------------------*/
/**
* user共同属性
*/
static class BaseUser {
private String desc;
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
//do something
}
/*------------------------------------------------*/
/**
* 穷逼user,送的那种
*/
static class SlumDogVIP extends BaseUser {
//do something
}
/*------------------------------------------------*/
/**
* 正儿八经买vip的user
*/
static class RealVIP extends BaseUser {
//do something
}
/*------------------------------------------------*/
/**
* 可以为所欲为的VIP
*/
static class SuperVip extends BaseUser{
}
/*------------------------------------------------*/
/**
* Strategy抽象策略角色
*/
interface ServiceHandler{
void service(BaseUser user);
}
/*------------------------------------------------*/
/**
* 具体策略(ConcreteStrategy)角色
* 处理穷逼VIP
*/
static class SlumDogVIPServiceHandler implements ServiceHandler{
@Override
public void service(BaseUser user){
System.out.println("该用户是:" + user.getDesc());
System.out.println("穷逼");
}
}
/*------------------------------------------------*/
/**
* 具体策略(ConcreteStrategy)角色
* 处理正常的VIP
*/
static class RealVIPServiceHandler implements ServiceHandler{
@Override
public void service(BaseUser user) {
System.out.println("该用户是:" + user.getDesc());
System.out.println("正常人");
}
}
/*------------------------------------------------*/
/**
* 具体策略(ConcreteStrategy)角色
* 处理可以为所欲为的VIP
*/
static class SuperVipServiceHandler implements ServiceHandler{
@Override
public void service(BaseUser user) {
System.out.println("该用户是:" + user.getDesc());
System.out.println("可以为所欲为的VIP");
}
}
/*------------------------------------------------*/
public static void main(String[] args) {
//声明一个具体的策略
ServiceHandler serviceHandler1 = new SlumDogVIPServiceHandler();
BaseUser user1 = new SlumDogVIP();
user1.setDesc("穷逼");
//声明一个上下文对象
VIPCenter vipCenter1 = new VIPCenter(serviceHandler1);
//执行封装后的方法
vipCenter1.serviceVIP(user1);
//第二种策略
serviceHandler1 = new RealVIPServiceHandler();
user1 = new RealVIP();
user1.setDesc("正常");
//只需“注入”策略实现类(RealVIPServiceHandler)
vipCenter1 = new VIPCenter(serviceHandler1);
vipCenter1.serviceVIP(user1);
//第三种策略
/*serviceHandler1 = new SuperVipServiceHandler();
user1 = new SuperVip();
user1.setDesc("富豪");
vipCenter1 = new VIPCenterImprove(serviceHandler1);
vipCenter1.serviceVIP(user1);*/
}
}
上面的示例(策略模式),将不同的对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码隔离方便扩展。如果以后再出现新的用户类型,只需要重新实现一个策略接口ServiceHandler即可,然后将其注入到VIPCenter,这样比直接在serviceVIP方法里面加else if 优雅!
开闭原则能屏蔽回归测试的影响(加一个 else if 就得再回归测试一遍功能,万一你 else if 加错了呐),提高了代码的可维护性。
个人看法
六大设计原则深深的贯彻了面向对象编程思想,设计原则仅仅是个工具,对我们软件设计是指导作用,而不是决定作用,不可作为一个软件好坏的唯一标准。设计原则必须得结合实际情况,在设计软件时,不能无限地考虑未来的变更情况,否则就会陷入设计的泥潭中不能自拔。