我想用贴近生活的语句描述一下自己对六种原则的理解。也就是不做专业性的阐述,而是描述一种自己学习后的理解和感受,因为能力一般而且水平有限,也许举的例子不尽妥当,还请谅解
原本我是想用JavaScript编写的,但是JavaScript到现在还没有提出接口的概念,而用TypeScript写又感觉普及度还不算特别高,所以还是决定用Java语言编写
设计模式有六大原则
首先要提的是:六大原则的灵魂是面向接口,以及如何合理地运用接口
应该有且仅有一个原因引起类的变更(There should never be more than one reason for a class to change)。
为了达到这个目标,我们需要对类和业务逻辑进行拆分。划分到合适的粒度,让这些各自执行单一职责的类,各司其职。让每个类尽量行使单一的功能,实现“高内聚”,这个结果也使得类和类之间不会有过多冗余的联系,从而“低耦合”。
比如我们现在有了这样一个类
public class People {
public void playCnBlogs () {
System.out.println("刷博客");
}
public void doSports () {
System.out.println("打乒乓球");
}
public void work () {
System.out.println("工作");
}
}
现在看起来有点混乱,因为这个类里面混合了三个职责:
OK,正如你所见,既然我们要遵循单一职责,那么怎么做呢?当然是要拆分了
我们要根据接口去拆,拆分成三个接口去约束People类(不是把People类拆了哈)
// 知乎er
public interface Blogger {
public void playCnBlogs();
}
// 上班族
public interface OfficeWorkers {
public void work();
}
// 业余运动爱好者
public interface AmateurPlayer {
public void doSports();
}
然后在People中继承这几个接口
public class People implements Blogger,AmateurPlayer,OfficeWorkers{
public void playCnBlogs () {
System.out.println("刷博客园");
}
public void doSports () {
System.out.println("打乒乓球");
}
public void work () {
System.out.println("工作");
}
}
最后创建实例运行一下
public class Index {
public static void main (String args []) {
People people = new People();
Blogger blogger = new People();
blogger.playCnBlogs(); // 输出:刷博客园
OfficeWorkers workers = new People();
workers.work(); // 输出: 工作
AmateurPlayer players = new People();
players.doSports(); // 输出:打乒乓球
}
}
备注:这个原则不是死的,而是活的,在实际开发中当然还要和业务相结合,不会纯粹为了理论贯彻单一职责,就像数据库开发时候,不会完全遵循“三大范式”,而是允许一定冗余的
里氏替换原则,一种比较好的理解方式是: 所有引用基类的地方必须能透明地使用其子类的对象。 换句话说,子类必须完全实现父类的功能。凡是父类出现的地方,就算完全替换成子类也不会有什么问题。
以上描述来自《设计模式之禅》,刚开始看的时候我有些疑惑,因为一开始觉得:只要继承了父类不都可以调用父类的方法吗?为什么还会有里氏替换所要求的:子类必须完全实现父类的功能呢, 难不成继承的子类还可以主动“消除”父类的方法?
还真可以,请看
父类
public abstract class Father {
// 认真工作
public abstract void work();
// 其他方法
}
子类
public class Son extends Father {
@Override
public void work() {
// 我实现了爸爸的work方法,旦我什么也不做!
}
}
子类虽然表面上实现了父类的方法,但是他实际上并没有实现父类要求的逻辑。里氏替换原则要求我们避免这种“塑料父子情”,如果出现子类不得不脱离父类方法范围的情况, 采取其他方式处理,详情参考《设计模式之禅》
(其实个人觉得《禅》的作者其实讲的“父类”其实着重指的是抽象类)
很多文章阐述依赖倒置原则都会阐述为三个方面
换句话说, 高层次的类不应该依赖于,或耦合于低层次的类,相反,这两者都应该通过相关的接口去实现。要面向接口编程,而不是面向实现编程,所以编程的时候并不是按照符合我们逻辑思考的“依赖关系”去编程掉的,这种不符,就是依赖倒置
举个例子,类好比是道德,接口好比是法律。
道德呢,有上层的也有下层的,春秋时代,孔圣人提出了上层道德理论:“仁”的思想,并进一步细化为低层道德理论:“三纲五常”(高层模块和底层模块),想要以此规约众生,实现天下大同。可是奈何民众的道德终究还是靠不住(没有接口约束的类,可能被混乱修改),何况道德标准是会随物质经济的变化而变化的,孔子时代和我们今天的已经大有不同了。(类可能会发生变化)所以才需要法律来进一步框定和要求道德。(我们用接口来约束和维护“类”,就好比用法律来维护和规约道德一样。)假如未来道德伦理的标杆发生了变化,肯定是先修缮法律,然后再次反向规制和落实道德(面向接口编程,而不是面向实现编程)。
我们看下下面没有遵循依赖倒置原则的代码是怎样的,我们设计了两个类:Coder类和Linux类,并且让它们之间产生交互:Coder对象的develop方法接收Linux对象并且输出系统名
// 底层模块1:开发者
public class Coder {
public void develop (Linux linux) {
System.out.printf("开发者正在%s系统上进行开发%n",linux.getSystemName());
}
}
// 底层模块2:Linux操作系统
public class Linux {
public String name;
public Linux(String name){
this.name = name;
}
public String getSystemName () {
return this.name;
}
}
// 高层模块
public class Index {
public static void main (String args []) {
Coder coder = new Coder();
Linux ubuntu = new Linux("ubuntu系统"); // ubuntu是一种linux操作系统
coder.develop(ubuntu);
}
}
输出
开发者正在ubuntu系统系统上进行开发
但是我们能发现其中的问题:
操作系统不仅仅有Linux家族,还有Windows家族,如果我们现在需要让开发者在windows系统上写代码怎么办呢? 我们可能要新建一个Windows类,但是问题来了,Code.develop方法的入参数类型是Linux,这样以来改造就变得很麻烦。
让我们利用依赖倒置原则改造一下,我们定义OperatingSystem接口,将windows/Linux抽象成操作系统,这样,OperatingSystem类型的入参就可以接收Windows或者Linux类型的参数了
// 程序员接口
public interface Programmer {
public void develop (OperatingSystem OS);
}
// 操作系统接口
public interface OperatingSystem {
public String getSystemName ();
}
// 低层模块:Linux操作系统
public class Linux implements OperatingSystem{
public String name;
public Linux (String name) {
this.name = name;
}
@Override
public String getSystemName() {
return this.name;
}
}
// 低层模块:Window操作系统
public class Window implements OperatingSystem {
String name;
public Window (String name) {
this.name = name;
}
@Override
public String getSystemName() {
return this.name;
}
}
// 低层模块:开发者
public class Coder implements Programmer{
@Override
public void develop(OperatingSystem OS) {
System.out.printf("开发者正在%s系统上进行开发%n",OS.getSystemName());
}
}
// 高层模块:测试用
public class Index {
public static void main (String args []) {
Programmer coder = new Coder();
OperatingSystem ubuntu = new Linux("ubuntu系统"); // ubuntu是一种linux操作系统
OperatingSystem windows10 = new Window("windows10系统"); // windows10
coder.develop(ubuntu);
coder.develop(windows10);
}
}
虽然接口的加入让代码多了一些,但是现在扩展性变得良好多了,即使有新的操作系统加入进来,Coder.develop也能处理
接口隔离原则的要求是:类间的依赖关系应该建立在最小的接口上。这个原则又具体分为两点
举个例子,中秋节其实只过了一个多月,现在假设你有一大盒“五仁月饼”想带回家喂猪,但是无奈的是包包太小放不下,而且一盒沉重的月饼对瘦弱的你是个沉重的负担。这个时候,我们可以把月饼盒子拆开,选出一部分自己需要(wei zhu)的月饼,放进包包里就好啦,既轻便又灵活。
还是上代码吧,比如我们有这样一个Blogger的接口,里面涵盖了一些可能的行为。大多数博客用户会保持友善,同时根据自己的专业知识认真写文章。但也有少数的人会把生活中的负面能量带到网络中
public interface Blogger {
// 认真撰文
public void seriouslyWrite();
// 友好评论
public void friendlyComment();
// 无脑抬杠
public void argue();
// 键盘攻击
public void keyboardAttack ();
}
我们发现,这个接口可以进一步拆分成两个接口,分别命名为PositiveBlogger,NegativeBlogger。这样,我们就把接口细化到了一个合理的范围
public interface PositiveBlogger {
// 认真撰文
public void seriouslyWrite();
// 友好评论
public void friendlyComment();
}
public interface NegativeBlogger {
// 无脑抬杠
public void argue();
// 键盘攻击
public void keyboardAttack ();
}
>> 备注:妥善处理 单一职责原则 和 接口隔离原则的关系
事实上,有两点要说明一下
迪米特原则又叫最少知道原则,在实现功能的前提下,一个对象接触的其他对象应该尽可能少,也即类和类之间的耦合度要低。
举个例子,我们经常说要“减少无效社交”,不要总是一昧的以交朋友的数量衡量自己的交际能力,否则会让自己很累的,也会难以打理好复杂的人际关系。对于并不很外向的人,多数时候和自己有交集的朋友交往就可以了。
我们看下代码:
有如下场景,现在你和你的朋友想要玩一个活动,也许是斗地主等游戏,这个时候需要再喊一个人,于是你让你的朋友帮你再叫一个人,有代码如下
// 我的直接朋友
public class MyFriend {
// 找他的朋友
public void findHisFriend (FriendOfMyFriend fof) {
System.out.println("这是朋友的朋友:"+ fof.name);
}
}
// 朋友的朋友,但不是我的朋友
public class FriendOfMyFriend {
public String name;
public FriendOfMyFriend(String name) {
this.name = name;
}
}
// 我
public class Me {
public void findFriend (MyFriend myFriend) {
System.out.println("我找我朋友");
// 注意这段代码
FriendOfMyFriend fmf = new FriendOfMyFriend("陌生人");
myFriend.findHisFriend(fmf);
};
}
这时我们发现一个问题,你和你朋友的朋友并不认识,但是他却出现在了你的“找朋友”的动作当中(在findFriend方法内),这个时候,我们认为这违反了迪米特原则(最少知道原则),迪米特原则我们对于对象关系的处理,要减少“无效社交”,具体原则是
所谓的“不交流”,就是不要在代码里看到他们
我们改造一下上面的代码
// 我朋友
public class MyFriend {
public void findHisFriend () {
FriendOfMyFriend fmf = new FriendOfMyFriend("陌生人");
System.out.println("这是朋友的朋友:"+ fmf.name);
}
}
// 朋友的朋友,但不是我的朋友
public class FriendOfMyFriend {
public String name;
public FriendOfMyFriend(String name) {
this.name = name;
}
}
// 我
public class Me {
public void findFriend (MyFriend myFriend) {
System.out.println("我找我朋友");
myFriend.findHisFriend();
};
}
开闭原则的意思是,软件架构要:对修改封闭,对扩展开放
举个例子
比如我们现在在玩某一款喜欢的游戏,A键攻击,F键闪现。这个时候我们想,如果游戏能额外给我定制一款“K”键,残血时解锁从而一击OK对手完成5杀,那岂不美哉,这就好比是“对扩展开放”。
但是呢,如果游戏突然搞个活动,把闪现/攻击/技能释放的键盘通通换个位置,给你一个“双十一的惊喜”,这恐怕就给人带来惨痛的回忆了。所以我们希望已有的结构不要动,也不能动,要“对修改封闭”
(本人不玩游戏,这些是自己查到的,如果错误还请指正)
就像很多人说的,其实设计模式是一种思想,关键的还是怎样和业务结合起来,我也刚学习不久呢,如果前辈们有什么好的见解,还请在评论区指点一下,不胜感激