本文最初发布于 hackernnon 网站,经原作者授权由 InfoQ 中文站翻译并分享。
SOLID 原则是开发人员创建灵活、可理解和可维护代码的基础。但你要正确遵循这些原则就可能明显减慢开发速度,并且大多数人没那么操心代码质量,因此我发明了一套更好用的原则。
DILOS 原则是我们构建可怕代码的坚实支柱。
我个人已经用上了 DILOS 原则,成功创建出大堆混乱难懂和臃肿的代码,这篇文章我就来具体介绍一下:
高级模块必须依赖低级模块。依赖实体而不是抽象。
把某些东西抽象出来,就是要隐藏这些东西内部的实现细节,有时是原型,有时是函数。因此当你调用这个函数时不必完全了解其机制。如果你非得先搞懂大型代码库中的所有函数,那就别想着写代码了。可能需要几个月的时间才能看完那些东西。
但现在我们要把这条原则倒过来:不要抽象任何东西。也就是尽量少用小块函数,把所有东西都塞到一个单体函数里。如果别人想调用你的函数,让他看懂你的每一行代码再说吧。
下面是整洁代码的一个示例:
function hitAPI(url, httpMethods){
// Implementation example
}
hitAPI("https://www.kealanparr.com/retrieveData", "GET");
hitAPI("https://www.kealanparr.com/retrieveInitialData", "GET");
你看,你用不着操心 hitAPI 在做什么,我们只传递了一个 URL 和一个 HTTP 请求,然后就搞定了。现在这段代码是高度可重用和可维护的。这个函数可以在一个地方处理所有 URL。我们已经尽可能让高级函数(我们放在 base 原型中的函数,可以和下层的许多东西共享)不依赖于任何低级函数。
那么如果我们反转这个依赖倒置(Dependency-Inversion)原则呢?
function hitDifferentAPI(type, httpMethods){
if (this instanceof initialLoad) {
// Implementation example
} else if (this instanceof navBar) {
// Implementation example
} else {
// Implementation example
}
}
现在我们让高级 api 请求依赖于许多较低级别的类型。完成任务后,它不再是完全通用的了,并且会依赖其继承链中较低的类型。
强迫客户端依赖它们不用的 [代码]。
其他语言里的接口用于定义不同对象拥有的方法和属性。
不要向不需要的对象添加代码?不要将太多无关的功能捆绑在一起?嗯,胡扯嘛这是。
我们一定要把松散耦合的代码都绑在一个地方。关键在于一定要依赖你用不着的东西。
我们稍后将在“多重职责原则”中进一步解释,但请记住这条原则,在所有地方疯狂用它。还记得花半天时间查找几百个文件搜索 bug 的经历吗?那种事情不会再有了。搞一个名为 main.js 的 JS 文件,然后把所有代码都塞进去。
让你的站点预加载所有内容,不要搞什么 JS 脚本按需加载,这样初始加载速度就会慢如蜗牛啦。
写代码的时候把宇宙毁灭时的需求都想好,然后提前写好对应的逻辑,反正你迟早用得上嘛。
如果有人需要你代码里的一根香蕉,那就塞给他一头拿着香蕉的大猩猩。客户端要啥就给它附送一堆垃圾,它们肯定会感谢你的。
写的函数越少越好。把什么东西封装在一个放在其他地方的新函数里,并抽象化它的逻辑?可别这么干。怎么让人犯迷糊怎么来,需要代码的时候复制粘贴过来就行。
理想情况下,我们的代码流只有 1 个对象。在非常大的代码库中,我们可能有 2 个对象。通常将其称为“上帝对象”反模式,其中我们要到处用单独的一个对象,因为所有事情都得它来做。稍后我们将详细讨论。
软件各部分的子级和父级不可以互换。
你竟然会在代码中使用继承吗?这绝对要注意。你应该复制粘贴而不是继承代码。Copy-Paste 反模式就是这个意思,你不应该把代码的通用功能抽象为模块化的可重用功能,而应当在所有需要的地方都复制代码。这会增加技术债(将来你迟早要回来修复的),而且每更改一段代码,都需要多次搜索才能找到它在代码库中出现在了哪些位置。
DRY 原则表示 Don't Repeat Yourself,而 WET 原则恰恰相反,指的是我们 Write Everything Twice。必要时应该写更多次数。抵制继承,随意复制粘贴。
但是,如果你确实需要利用里氏分离原则,请确保在与继承链中较高子级(父级)交换对象时,继承链中较低子级的对象原型不能正常工作。
为什么?
因为如果我们不遵循里氏分离原则,我们就会构建准确而健壮的继承链。将逻辑抽象为封装好的 base 原型 / 对象。我们还会对原型链中的不同方法按逻辑分组,它们的特定覆盖会让代码路径更加可预期和可发现。
如果你正确地遵循了这一原则,那么父级能用的时候子级也没法用,继承就会毫无意义。如果你的程序尝试引用的函数并不存在于自己的子级中,因此崩溃掉——你就会完全避免继承会给你带来的任何好处——这正是我们遵循这一原则的目的。
对象应对修改开放,对扩展封闭。
好的代码通常会扩展对象的代码,以限制修改 base 原型。这样,完成扩展的对象就可以处理自己的状态以及需要执行的新功能(仅处理子项需要做的少量更改即可)。
上面这段话是胡说八道,我们真正应该做的是:
如果在处理每个场景时都分支,那么上面这两步带来的负担就更重了。所以最后你会看到诸如这样的代码:
function makeSound(animal) {
if (animal == "dog") {
return "bark";
} else if (animal == "duck") {
return "quack";
} else if (animal == "cat") {
return "meow";
} else if (animal == "crow") {
return "caw";
} else if (animal == "sheep") {
return "baa";
} else if (animal == "cow") {
return "moo";
} else if (animal == "pig") {
return "oink";
} else if (animal == "horse") {
return "neigh";
} else if (animal == "chicken") {
return "cluck";
} else if (animal == "owl") {
return "twit-twoo"; } else {
/// It has to be a human at this point
return "hi";
}
}
现在,如果你需要更改某些内容,只需添加另一个 if 检查即可。这和下文列出的多职责原则有关,但核心在于将所有内容都包含在 base 函数 / 原型中。
这样你就会进入“脆弱基类”反模式。在这种模式下,更改 base 函数 / 原型时,你最后会在调用此函数的其他触点上出现错误。
例如,假设 human 不该再掉进 else 里,而你添加了新的 animal,名为 wolf,你就会引入错误(除非你更新了期望 human 被记录的位置)。
确保你的函数 / 对象有多重职责。
优秀的编程人员常常会将他们的代码分成多个不同的对象或模块。但我可搞不清楚这种事情,我记不住它们都负责什么事情。
下面是一个例子:
const godObject = {
handleClicks: function(){},
getUserName: function(){},
handleLogin: function(){},
logTransactionId: function(){},
initialSiteLoad: function(){} };
godObject 负责网站的许多功能。付款、登录、网站负载、记录交易 ID 以及网站上的所有点击功能都包括在其中。太棒了,这样如果你遇到了错误,就会知道它只能出现在这里。确保代码库中的每个地方都需要访问 godObject。让它做一切工作。
我们在代码中真正想要的是高耦合(确保系统的各个部分相互依赖)和低内聚(将许多随机的数据和片段放在一起)。
这种反模式有时被称为“瑞士军刀”,因为就算你要的只是一把剪子,但它也可以是指甲锉、锯子、镊子、开瓶器,也可以是软木钉。
我不断强调的是,要把所有东西放在一起,然后绑定、打包、捆在一起。
我希望大家看完这篇文章后,就知道软件究竟应该怎么写才能尽可能增加调试需求、尽可能把人搞糊涂,并且搞出来最多的技术债。
延伸阅读:
https://hackernoon.com/introducing-dilos-principles-for-JAVAscript-code-jp1d3w1b