Javascript设计原则

Javascript设计原则

在面向对象的程序设计思想中, 我们能够遵循一些原则能够让我们开发代码时结构层次清晰, 更具说服力, 可谓是事半功倍。 做到这一点我们掌握一些程序设计原则是非常有利的, 如果大家研究过数学(或者其他学科)的一些思想方法和解题方法时应该会这个概念有所了解, 设计原则类比数学中的思想方法(如分类讨论, 数形结合等都是思想, 指导你整个学习生涯), 是带有从全局角度的指导性的方案, 设计模式类比与解题方法(如换元法, 数学归纳法等都是针对特定类型的问题的一种解决方案)。 今天介绍一些常见的设计原则, 这些设计原则是独立于语言存在的, 并非Javascript特有。

单一职责原则

理解概念

从名字来看比较好理解, 各自(你创建的函数)干好各自的事情(上级分配的任务, 一般是指你分配)。 在程序世界里, 对象大多数要承担计算任务, 我们暂且把这些任务说成方法, 每个方法各司其职。 如果某个方法承担了过多的职责就会造成职责间的耦合, 致使程序难以维护, 内聚性差。 我们应尽可能的将这些职责分布到更细粒度的方法(或私有方法, 如闭包中的函数)中。 每个方法应该仅有一个引起它变化的原因, 但是这样往往是很困难的, 我们应尽量减少造成方法变化的原因, 一般指方法的参数。 这些方法应该尽可能相互独立, 互不干扰, 避免修改其中一个导致另外的方法产生异常。

使用举例

比如我们需要一个提供创建Ajax的对象。

const Ajax = {
        createAjax: function () {
                var xhr = null;
                if(window.XMLHttpRequest) {
                        xhr = new XMLHttpRequest();
                } else {
                        xhr = new ActiveXObject('Microsoft.XMLHTTP');
                }
        },
        send: function() {  }
};

// VS

const AjaxBetter = {
        ajaxFactory: function ajax() {
                if(window.XMLHttpRequest) {
                        return (this.ajaxFactory = function() {
                                return new XMLHttpRequest();
                        })();
                } else {
                        return (this.ajaxFactory = function() {
                                return new ActiveXObject('Microsoft.XMLHTTP');
                        })();
                }
        },
        createAjax() {
                var xhr = this.ajaxFactory();
                return xhr;
        },
        send: function() {  }
};

单一职责原则的优点

粒度细便于代码扩展和复用, 职责独立, 变更单个职责不会影响到其余的职责。

里氏替换原则

理解概念

在子类继承父类时, 子类不能取复写父类的方法。 举例说明一下, 对于父类Car来讲, 它提供了一个move方法, 子类BMW继承自Car时可以拓展Car的move方法但不能对其进行覆盖, move方法能被BMW的子类透明的调用。请看demo:

class Car {
        move() {
                console.log('I\'m running');
        }
};

class BMW extends Car {
        run() {
                this.move();
                console.log('new method');
        }
};

const car = new Car();
const bmw = new BMW();
// 父类实例自然能调用move
car.move();
// 子类应能透明调用父类方法, 不要让子类自己也实现一个move方法进而覆盖父类同名方法
bmw.move();
bmw.run();

不覆盖父类的同名方法是防止程序出错, 这和作用域里层同名会遮蔽外层同名变量的目的还不太一样, 我们用子类取继承父类多数是出于代码的复用, 如果取覆盖掉就没有这层意思了, 看下面这个例子:

class Odd {
        getOdd(n) {
                return 2 * n + 1;
        }
};

class Sub extends Odd {
        getOdd(n) {
                return 2 * n;
        }

        calc(n) {
                return this.getOdd(n) * 2;
        }
};

const sub = new Sub();
sub.calc(5);    // 20, 出错了, 期望是22

这个程序的意图是父类提供一个返回奇数的方法, 子类在继承时无意重写了, 导致子类在calc方法中调用getOdd会得到错误的结果, 以至于程序给出错误的结果

PS: 这里说到的子类不能覆盖父类方法是说不建议这样做, 并不是这样做就会发生语法错误, 子类完全有覆盖父类方法的能力, 如果要违背这一原则请务必三思而后行

依赖倒置原则

理解概念

高层模块不应该依赖底层模块, 二者应依赖其抽象, 抽象不应依赖细节, 细节依赖抽象, 这大多数针对类c语言讲的, 因为js并不具备抽象类概念

const test = {
        getValue() {
                return 1;
        }
};

const other = {
        getVal() {
                return 10;
        }
};

class SimpleMath {
        calc(obj) {
                console.log(obj.getValue() + 100);
        }
};

const s = new SimpleMath();
s.calc(test);   // 101
s.calc(other);  // 错误

这个错误是显而易见的, other底层类并不具备getValue方法, 所以报错, 我们当然可以修改other让其具备getValue, 可这样难保其他类似的情况, 这里有一个问题, SimpleMath类实例的calc方法与底层对象产生了一个强耦合, 必须具备getValue方法的对象才能传入calc方法, 这就违背了依赖倒置原则的高层模块不依赖底层模块, 所以更好的做法是消除该依赖

const test = {
        getValue() {
                return 1;
        }
};

const other = {
        getVal() {
                return 10;
        }
};

class SimpleMath {
        calc(getValue) {
                console.log(getValue() + 100);
        }
};

const s = new SimpleMath();
s.calc(test.getValue);  // 101
s.calc(other.getVal);   // 110