编程框架日新月异,工具平台日新月异。但有趣的是,代码的恶臭并不会因为你使用工具的时尚而自行消散,团队成员的编程水平也不会随着工具的进化而上升。工具从来都不是你写出好代码的决定性因素。相反,它可能是最不起眼的条件之一,却恰恰给了大多数人一种救命稻草的错觉。它更像是一种催化剂,它可以帮助你的代码并加速它的消亡。Mixins就是一个很好的例子。Mixin语法回顾如果你对Vue中的mixin语法不是很了解,我可以用一句话和一个例子简单概括一下:mixin是一种组件间的代码共享机制,可以让你将代码封装成一个独立的模块,使用它用于跨多个组件共享。假设Toolbar和Card组件在实例化该组件时需要传递title和subTitle属性,那么可以考虑将这两个属性封装到一个名为ComminMixin的通用模块中,然后将该模块“插入”到需要的组件中。先定义CommonMixin模块代码:exportdefault{props:{title:string,subTitle:string,}}然后在Toolbar和Card组件中引用:importCommonMixinfrom'../mixins/commin-mixin.js'exportdefault{mixins:[CommonMixin]}这个方法和直接在Toolbar中定义title和subTitle属性没什么区别:importCommonMixinfrom'../mixins/commin-mixin.js'exportdefault{props:{title:string,subTitle:string}}什么样的mixin机制有什么问题?早在2016年,React官方就发布了一篇名为MixinsConsideredHarmful的文章,详细描述了mixin机制下会出现的几类问题,比如命名冲突,比如Causingsnowballingcomplexity等等。这里我只提一下我认为最有害的一点:隐式依赖。并以此引出我们下一节的主题。隐式依赖如上节代码所示,mixin模块中的代码和组件中的代码是等价的。如果你知道mixin中有一个名为hello的方法,你完全可以在组件中使用this.hello和()的形式乱调用。这种等价是双向的。虽然最初定义了mixin模块,但不知道以后哪些组件会引用它,但是如果你非常确定一个world方法注定存在于消费它的组件中,你可以调用this.world()在mixin模块中。这种关系也可以扩展到mixins。无论是并列关系中的mixin模块还是嵌套关系(一个mixin模块可以继续引用其他mixin模块),它们之间都可以相互访问变量和方法。你是不是已经嗅到了这里的危险?看起来特别方便!但是一旦代码需要维护,问题就会暴露出来,哪怕你只是想了解一个很小的代码片段。假设同事在组件中遇到了hello方法,想给它加一个参数,实现更多的功能——这个看似不起眼的东西,实际操作起来比天还难。因为他不知道这个方法是在哪个mixin模块里定义的,只知道这个方法是属于这个的,所以他要翻遍每个mixin的定义。退一步说,即使他在一个mixin中找到了方法的定义,他也会遇到另一个问题:他不敢修改这个函数的签名。虽然他可以知道有多少组件文件引用了这个mixin模块,但是不知道有多少地方直接或间接调用了这个方法。换句话说,这种修改的后果和影响很难评估。所有这些问题的根源都在components和mixins之间,mixins和mixins之间的依赖是隐式的。也就是说,当模块A依赖于函数B时,这种关系既不是通过显式声明(如导入语句或依赖注入)获得的,也不是通过公共约定(如windows上是否存在getComputedStyle方法)确定的目的)。这种关系也让IDE武功无用武之地。我发现最后解决这个问题的方法是Ctrl+C(复制)、Ctrl+V(粘贴)、Ctrl+Shift+F(全局搜索)、Ctrl+H(文本替换))。隐式依赖不仅会对脚本代码产生负面影响,还会对样式代码产生负面影响。Flex布局就是一个正面的例子:如果想控制子元素在父容器中的布局,只需要在父容器上修改flex相关的属性即可。您不依赖于子元素的DOM结构,更不用说子元素上的样式了;反模式的一个示例是text-overflow:ellipsis属性。单个样式属性不足以自动忽略容器中的文本。容器还需要满足1)宽度必须以px像素为单位2)元素必须有overflow:hidden和white-space:nowrap样式。text-overflow属性本身并没有告诉我们需要这些“配套设施”。由此产生的情况正好符合BobMartin大叔在他的OOD原则系列文章中谈到的BadDesign的几个特点,比如僵化(Ridigity):代码很难修改,因为变化会影响到太多地方Fragility:当你进行更改时,系统中意想不到的地方会被破坏Immobility:代码难以重用,因为它与当前系统中的功能耦合在一起,前面两个已经在上面解释过了,很容易理解。至于最后一个特性,mixin不仅看起来没有违和,而且表现也很好不是吗?这就涉及到Defactoring的内容,我们下一节会讲到。暂停一下,我们似乎进退两难:我们都承认mixin是极其强大和灵活的,它最大限度地提高了代码复用。但是现在来看,正是这种灵活性给我们的代码带来了灾难。我们应该如何理解这个矛盾呢?我们必须回答的第一个问题是:这种灵活性真的是我们想要的吗?ReginaldBraithwaite在2013年写了一篇有趣的技术文章Defactoring。请注意,它并不重要Constructed那个词Refctoring。什么是分解?简而言之,如果我们把将一个大的单体代码拆分成细粒度的碎片化代码的过程称为分解,那么分解就是将代码碎片组装起来的过程。为什么我们需要分解?因为灵活性并不总是带来好处,它会给我们带来认知上的困扰,你总是需要将不同的片段拼凑起来才能理解整个画面的本来面目;模棱两可的代码总是会让你搞不清它的意图;更不用说代码的复杂性了。你可能会问如果?有时候“灵活性”的背后是我们对未来的恐惧:我们可能需要支持功能A或者支持功能B,但实际上你不需要提前意识到这些可能性,让你的代码能够做到就足够了处理这些可能性。所以适当的分解是必要的。第二点我们需要考虑到人的因素。我很喜欢CodingHorror提出的FallIntoThePitofSuccess理论。引用原文是:一个设计良好的系统可以很容易地做正确的事情,而烦人(但并非不可能)做错误的事情。从项目和团队的角度考虑代码的可维护性时尤其如此。除此之外,代码应该易于修改,并且易于修改。例如,TypeScript与JavaScript相比,但显然mixins不是。从你写出一段代码的那一刻起,它的命运就不再掌握在你的手中。别人可能不理解你设计某个属性的用意,你精心设计的一段性能优化代码很容易被毁掉。所以我们需要一个适应度函数,我们需要一个测试。在实践中,mixins大多被滥用。您可以定义一个名为ComponentCommonMixin的模块,用于存储与所有组件关联的公共属性。但是后面的开发者并不知道你的初衷是什么,所以在规划接下来的公共属性的时候就想都没想就往这个模块上加,让它臃肿——“哦,因为它是公共的”。从表面上看,它分离了公共属性代码和组件特定代码,但实际上它恰好是mixin模块内部紧耦合反模式的最好体现。这种状态下的mixin根本就没有“单一责任(SingleResponsibility)”。一个模块可能同时包含与样式相关的属性和与权限相关的行为,涉及任何业务。需求的变化会导致模块“打开”重新修改,这也违反了开闭原则(Open-closed)。通用mixin模式的好处在于,这种mixin中的隐式依赖问题是Vue框架下的特例。说到代码复用,我们首先想到的就是继承,但是继承并不是万能的:继承打破了父类的封装;继承要求子类在重写方法时要与父类兼容;大多数语言不支持多重继承。总之,继承机制对类的抽象设计能力要求很高,低级抽象比不抽象更难维护。在这些约束下,组合模式似乎是一个不错的选择,mixin就是实现组合的一种方式。这里我们直接参考TypeScript2.2RC官方技术博客中的一个例子来说明mixin是如何实现的。简单来说,它分为以下四个步骤:采用构造函数声明一个类,该类扩展该构造函数向该新类添加成员并返回该类本身。这里我们尝试实现一个Timestampedmixin,它会添加一个需要扩展时间戳属性的类:typeConstructor
