说明:本文有点长,但是只要看完肯定会有收获,建议收藏慢慢看。
如果给你一堆(30 件)衣服,让你放在一个柜子里,你会怎么放?以下是 A 和 B 的做法。
A:直接全部丢在柜子里。“好快啊,我的活儿干完了”。并且对 B 说:“你真慢”。
B:分门别类,将春夏秋冬的衣服分别整理,然后将不同颜色的衣服分别叠在一起。
这时候 A 的老公说:“老婆,你可以在 30 秒之内把我夏天经常穿的那件白裤子拿出来吗?”。A 傻眼儿了:“臣妾做不到啊。”
设计模式和设计原则是一种思想。其目的是面对经常变更的需求,怎样规整、构建代码,使其更容易拓展和维护。它的重点并不是能够减少多少代码量,反而对于有些场景还有可能增加代码量。当然如果设计合理,业务复杂,大部分情况下是可以减少很多代码量的。所以减少代码并不是重点,重点是如何让你的代码写得更有拓展性和维护性。
比如以上案例中如果这些衣服是直接丢掉,以后再也不用了。以上案例中的 B 也不需要分门别类去整理了。但是很显然,这些衣服是要重复使用的。就行我们的业务需求一样,会经常变更。
设计模式和设计原则就是引导你怎样写出高内聚、低耦合,更高的复用性,维护性,灵活性,拓展性的代码,并且让你的代码结构更清晰,更方便看出业务之间的逻辑关系。不管是 Java 还是 Javascript 都是面向对象编程。在构建和设计程序的时候,本着高内低耦的宗旨应当遵循以下7大设计原则。
说明一下:其实在程序界,函数、方法、类都是一个概念,都是用来实现功能的,只是在不同的语言里面叫法不一样而已。
概念:开闭原则是当需求发生改变时,对拓展开放,对修改关闭。
概念:单一职责很好理解。尽量保证每个功能的颗粒度最小,那么它能实现复用的可能性就更大。如果一个功能模块现在或者将来(应对需求变更)需要由多个功能组成。就应该将每个小功能作为单独的函数,再进行组合。
理解:比如很多公司都会将行政和人事这两个职责分开。行政专门负责办公物资采购,办公环境清洁等方面的工作。人事专门负责招聘、培训、绩效等与人力资源相关的工作。
概念:依赖倒置原则是通过面向接口编程来降低耦合度。抽象不应该依赖具体实现。这段话对于 Java 或者任何面向接口编程的开发者很好理解。但是对于写 JS 的人来说,可能不那么好理解。其实它的目的就是从抽象层面去保证系统(功能模块)的稳定性。而具体的实现则通过另一个类(JS 里面就是一个单独的函数)去完成。那什么叫从抽象层面保证功能模块的稳定性昵?以下案例会有说明。
理解:一个公司的经营状况不会依赖于一个底层员工。但是底层员工的薪水、福利待遇却依赖于公司的经营状况。
概念:合成复用原则指的是可以通过组合的方法实现复用的情况下,应当优先考虑组合,其次才是继承。也可以理解为是有些功能可以组合到一起,就不用分不同情况写很多独立的方法。以下案例会有说明。
概念:接口隔离原则其实跟单一职责原则很像,区别在于它们的颗粒度。单一职责是一个功能一个方法。而接口是一个包含相关功能的多个方法的集合。不同的功能要独立成不同的方法,那么一系列相关功能的集合与另一系列相关功能的集合也需要相互独立。
理解:假设你的午餐要吃苹果,梨,青菜,土豆。你可以定义一个接口为水果,包含苹果,梨子。一个接口叫蔬菜,包含青菜,土豆。这样你就为你的午餐设立了两个相互隔离独立的接口。而不是一个庞大的午餐接口。如果定义一个庞大的接口,假设你明天午餐没有水果了,这个接口就没法用了。以下也会有实际场景的案例说明。
概念:里氏替换原则其实是一种更高层面的抽象。其目的也是为了使父类更稳定,子类尽量去拓展父类的功能而不是重写。避免发生错误。以下案例会有说明。
理解:麻雀和企鹅都属于鸟类,但是企鹅不能飞。如果你要设计一个计算飞行时间的鸟类,企鹅是不能够直接继承去修改飞行时间的。所以你可以设计一个更抽象的动物类。让鸟类和企鹅继承这个动物类,麻雀继承鸟类。
概念:迪米特法则是为了降低 A 类与 B 类之间的耦合关系,使其更加独立。从而设计一个与 A 类和 B 类都有关联的 C 类来处理 A 类与 B 类之间的逻辑关系。
理解:迪米特法则是代理和中介的一种体现,通过 nginx 来转发服务就是一种迪米特法则。在现实生活中,明星与粉丝/媒体公司之间往往有一个经纪人角色,明星只需要演好戏,唱好歌就行。与签约媒体公司和与粉丝之间的微博互动等都是由经纪人去衔接。再比如领导与员工之间的助理角色。有些新员工对于行政上的疑问可以直接找助理,而不是事事都找领导。领导给员工分配任务也可以由助理来组织和传达。
用一个案例来说明什么叫【开闭原则】【单一职责原则】【依赖倒置原则】【合成复用原则】【接口隔离原则】【里氏替换原则】【迪米特法则】。
场景1:
产品经理说:“小王啊,你能帮我实现一个这样的功能吗?”。如下图实现一个改变任务状态功能(点击任务出现下划线表示完成, 再次点击去掉下划线表示未完成)。
你可以这样写代码
function init() {
$("#content").on('click','li',function(){
if($(this).hasClass("done")){
$(this).removeClass("done");
$(this).find("input").prop("checked",false)
}else {
$(this).addClass("done");
$(this).find("input").prop("checked",'checked')
}
})
}
init();
从实现功能的角度,这样写没问题。
场景2:
过了 2 天,产品经理又找你了,说道: “小王,能不能在完成任务的时候给用户弹个框嘉奖两句。”
你当然也可以这样写
function init() {
$("#content").on('click','li',function(){
if($(this).hasClass("done")){
$(this).removeClass("done");
$(this).find("input").prop("checked",false);
alert("加油哦,你的【"+$(this).find("span").html()+"】任务没有完成");
}else {
$(this).addClass("done");
$(this).find("input").prop("checked",'checked');
alert("真棒,你已经完成了【"+$(this).find("span").html()+"】这个任务");
}
})
}
init();
场景3:
过了一周,产品经理又找上你,“小王,小王,你能不能…”;
如果是跟某条记录相关的功能,你当然也可以继续在 li
的监听事件上去累加代码。但是随着功能的增加,监听事件里面会有非常多的代码。如果这时候想要修改或者删除某个功能,你就得在臃肿的代码里找到相关功能再去修改。时间一久,你根本就不记得那个功能写在那个位置。
【开闭原则】:上面案例中产品经理后来增加了弹框功能,所以在最开始设计功能的时候就应该把修改任务状态功能封装到一个方法里面。那么增加了弹框需求之后就只需要再拓展一个弹框方法。
function init() {
$("#content").on('click','li',function(){
changeStatus($(this));
Toast($(this))
};
}
init();
function changeStatus(it) {
if (it.hasClass("done")) {
it.removeClass("done");
it.find("input").prop("checked", false);
} else {
it.addClass("done");
it.find("input").prop("checked", 'checked');
}
}
function Toast(it) {
if (it.hasClass("done")) {
$("#trigger").removeClass("hide");
$("#alert").html("加油哦,你的【" + it.find("span").html() + "】任务没有完成");
} else {
$("#trigger").removeClass("hide");
$("#alert").html("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
}
}
【单一职责原则】:每个功能都应该单独封装在一个独立的函数里面,方便组合复用。比如上面案例中应该将
已完成任务封装在一个独立的函数 alreadyDone
,
未完成任务封装在一个独立的函数 notDone
,
已完成任务弹框封装在一个独立的函数 alreadyDoneAlert
,
未完成任务弹框封装在一个独立的函数 notDoneAlert
。
function alreadyDone(it) {
it.addClass("done");
it.find("input").prop("checked",'checked');
}
function notDone(it) {
it.removeClass("done");
it.find("input").prop("checked",false);
}
function alreadyDoneAlert(it) {
$("#trigger").removeClass("hide");
$("#alert").html("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
}
function notDoneAlert(it) {
$("#trigger").removeClass("hide");
$("#alert").html("加油哦,你的【"+it.find("span").html()+"】任务没有完成");
}
【合成复用原则】:有些功能可以合成到一起,就不用分不同情况写太多颗粒函数。比如上面案例中对于已完成和未完成弹框。假设又多了一种“待定”的任务状态,就需要再写一个待定弹框。其实这几个弹框状态的不同之处只是一句话而已,我们完全可以通过传参的方式写成一个函数,组合到其他功能中,而不用写多个。
function Toast(it) {
if (it.hasClass("done")) {
alertText("加油哦,你的【"+it.find("span").html()+"】任务没有完成");
} else {
alertText("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
}
}
function alertText(text) {
$("#trigger").removeClass("hide");
$("#alert").html(text);
}
【依赖倒置原则】:尽量从抽象层面去保证功能模块的稳定性。比如上面案例中用抽象层面理解它的需求就是一句话:绑定li
事件实现功能。所以我们可以这样写。
function init() {
$("#content").on('click','li',function(){
doSomething($(this))
})
}
function doSomething(it) {
changeStatus(it);
Toast(it);
}
init();
这样写的话不管 li
事件还要实现其他任何具体的功能,这个抽象层都是稳定的,不需要修改。
【接口隔离原则】:为了更好地实现复用,解耦依赖性。一种系列的功能集合(接口)与另一种系列的功能集合(接口)应当隔离开来。
比如以上案例中,在 doSomething
内部做了两件事。第一:修改任务状态,第二:出现弹框。假设现在有另外一个列表,也要实现类似的功能,只是不再需要弹框了。这时候你就需要把弹框相关的代码都找到,然后一点点地删掉。另外大家也知道,在 WEB 应用中,弹框的功能使用非常普遍。把弹框跟其他的功能写在一起就没法实现复用了。
所以这时候最好的办法就是将任务状态单独作为一个接口(其实也就是单独写成一个方法)。再把弹框单独作为一个接口。我们以上案例中已经实现了接口隔离。
过了几个月如果产品经理再找上你,跟你说 “喂,小王,我看这个弹框挺不爽的,去掉吧”。这时候你就可以很方便地直接把 Toast 方法注释掉就行,而不用去分析逻辑,一点点找代码,思考需要删掉哪些代码。
【里氏替换】:在以上案例中,我们把 alertText
方法当成了公共方法调用。但是如果HTML 代码如下。区别是添加了一个"温馨提示"。显然我们直接 $("#alert").html(text)
是不合理的,会把
替换掉。所以我们需要更改 alertText
方法。
<div id="trigger" class="hide">
<div class="container">
<div id="alert">
<div>温馨提示:</div>
<div class="text"></div>
</div>
</div>
</div>
更改后的 alertText
方法如下。 setText
方法属于具体实现类,根据不同的业务场景去实现就好了。
function alertText(text) {
$("#trigger").removeClass("hide");
setText(text);
}
function setText(text) {
$("#alert .text").html(text);
}
【迪米特法则】:由于以上案例比较简单,很难模拟出迪米特法则。就像如果一个部门只有一个领导和员工,肯定就不需要助理角色了。不过从抽象的角度来说。我们也可以把 doSomething
看成是一种迪米特法则。用 doSomething
隔离开了 li
事件监听这个抽象功能和 changeStatus(it)
及 Toast(it)
这些具体功能,使双方更加独立。
完整代码:
以下代码只是为了说明这 7 大设计原则而设定的,不一定是最佳实践。实际项目中可根据这 7 大设计原则随意搭配,灵活运用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1">
<title>React App</title>
</head>
<body>
<style>
html {
font-size: 20px;
}
body {
margin: 30px;
}
ul, li {
margin: 0;
padding: 0;
}
.fw-bold {
font-weight: bold;
}
.done {
text-decoration: line-through;
}
li {
list-style: none;
margin-top: 10px;
margin-left: 10px;
}
.hide {
display: none;
}
.container {
position: fixed;
left: 0;
bottom: 0;
top: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.container #alert {
width: 80%;
height: 40%;
background: #fff;
padding: 20px;
border-radius: 5%;
}
</style>
<div class="fw-bold">任务列表</div>
<ul id="content">
<li><input type="checkbox"><span>开会</span></li>
<li><input type="checkbox"><span>测试</span></li>
<li><input type="checkbox"><span>发版</span></li>
</ul>
<div id="trigger" class="hide">
<div class="container">
<div id="alert">
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.0.js"></script>
<script>
function init() {
$("#content").on('click', 'li', function (e) {
e.stopPropagation();
doSomething($(this))
})
$("body").on('click',function () {
$("#trigger").addClass("hide");
})
}
function doSomething(it) {
changeStatus(it);
Toast(it);
}
function changeStatus(it) {
if (it.hasClass("done")) {
notDone(it);
} else {
alreadyDone(it);
}
}
function Toast(it) {
if (it.hasClass("done")) {
alertText("真棒,你已经完成了【" + it.find("span").html() + "】这个任务");
} else {
alertText("加油哦,你的【" + it.find("span").html() + "】任务没有完成");
}
}
function alreadyDone(it) {
it.addClass("done");
it.find("input").prop("checked", 'checked');
}
function notDone(it) {
it.removeClass("done");
it.find("input").prop("checked", false);
}
function alertText(text) {
$("#trigger").removeClass("hide");
setText(text)
}
function setText(text) {
$("#alert").html(text);
}
init();
</script>
</body>
</html>