ARTS-6-面向对象设计的SOLID原则

博客首页文章列表 松花皮蛋me 2019-04-21 12:53
文章首发于公众号 松花皮蛋的黑板报松花皮蛋的黑板报,作者就职于京东,在稳定性保障、敏捷开发、高级JAVA、微服务架构有深入的理解

ARTS的初衷

Algorithm: 主要是为了编程训练和学习。

Review:主要是为了学习英文

Tip:主要是为了总结和归纳在是常工作中所遇到的知识点。学习至少一个技术技巧。在工作中遇到的问题,踩过的坑,学习的点滴知识。

Share:主要是为了建立影响力,能够输出价值观。分享一篇有观点和思考的技术文章

https://www.zhihu.com/question/301150832

一、Algorithm

Perfect Squares

Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, …) which sum to n.

Example 1:

Input: n = 12
Output: 3
Explanation: 12 = 4 + 4 + 4.
Example 2:

Input: n = 13
Output: 2
Explanation: 13 = 4 + 9.

class Solution {
    public int numSquares(int n) {
        // 动态规划, 设 f[i] 表示加和为 i 的最少完全平方数的个数.
        // 状态转移方程: f[i] = min{f[i], f[i - j * j] + 1} (j*j <= i)
        // 边界: f[i*i] = 1
        int[] dp = new int[n+1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        for(int i=0;i*i<=n;++i) {
            dp[i*i] = 1;
        }

        for(int i=0;i<=n;++i) {
            for(int j=1;j*j<=i;++j) {
                dp[i] = Math.min(dp[i],dp[i-j*j]+1);
            }
        }
        return dp[n];         
    }
}  

二、Review

1、单一职责原则

考虑下面这个类

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
    saveAnimal(a: Animal) { }
}

它实际上违背了单一职责原则SRP。上面的类其实有两个职责,一为动物实体的持久化管理,另外一个为动物的属性管理。

那我们应该如何设计避免这种错误呢?我们可以新建另外一个类,它负责将实体对象存储到数据库上。如下所示:

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}
class AnimalDB {
    getAnimal(a: Animal) { }
    saveAnimal(a: Animal) { }
}

当我们设计类时需要注意的一点是,我们应该将那些会同时修改,并且为相同目的而修改的方法放在同一个类,也就是说一个类不应该同时存在着多个变更原因,这有点像组件开发中的共同闭包原则。

2、开闭原则

这个原则强调现有接口规范可以通过继承重用,不要修改现有已完成的接口。如果对原始需求的小小延伸就需要对原始设计进行大幅修改的话,显然是失败的。

我们继续以动物这个类说明

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}

我们的需求是让列表中每个动物发出不同的声音,如下所示

//...
const animals: Array<Animal> = [
    new Animal('lion'),
    new Animal('mouse')
];
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(a[i].name == 'lion')
            log('roar');
        if(a[i].name == 'mouse')
            log('squeak');
    }
}
AnimalSound(animals);

上面的示例违背了开闭原则,当有新的类型需要处理时,将不得不在原有代码上进行修改,导致大量的阅读性差的IF条件语句。

那我们应该怎么设计避免这种错误呢?我们可以定义一个有makeSound方法的共同类比如说Animal类,然后每个具体动物类继承并重写makeSound方法,完成个性化处理。另外真正的处理业务的AnimalSound类遍历Animal列表然后调用makeSound方法即可。当新扩展一个具体动物类时,AnimalSound类无须做任何修改。代码如下:

class Animal {
        makeSound();
        //...
}
class Lion extends Animal {
    makeSound() {
        return 'roar';
    }
}
class Squirrel extends Animal {
    makeSound() {
        return 'squeak';
    }
}
class Snake extends Animal {
    makeSound() {
        return 'hiss';
    }
}
//...
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        log(a[i].makeSound());
    }
}
AnimalSound(animals);

总结一下,开闭原则的实现方式是将系统划分为一系列的组件,并且将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。

3、里氏替换原则

这个原则强调当一个类继承另外一个类时,除添加新的方法外,尽量不要重写或者重载父类的方法。否则引用基类完成的功能,换成子类后就会出现异常。可以看到它指导如何使用继承关系,实际上它还指导接口与其实现方式的设计原则。

我们继续使用Animal类进行说明,先看一段代码,如下:

//...
function AnimalLegCount(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(typeof a[i] == Lion)
            log(LionLegCount(a[i]));
        if(typeof a[i] == Mouse)
            log(MouseLegCount(a[i]));
        if(typeof a[i] == Snake)
            log(SnakeLegCount(a[i]));
    }
}
AnimalLegCount(animals);

上面的示例违背了这个原则,因为必须知道每个类型才能决定使用哪个方法。

那我们应该怎么设计才能避免这种错误呢?我们应该保证子类的方法参数必须和超类的参数类型等同,或者为其超类参数的子类;我们应该保证子类的方法返回类型必须和超类的返回类型等同,或者为其超类返回类型的子类。

接下来我们进行代码改造:

function AnimalLegCount(a: Array<Animal>) {
    for(let i = 0; i <= a.length; i++) {
        a[i].LegCount();
    }
}
AnimalLegCount(animals);
改造后的方法不对参数进行任何类型判断,只关心它是否为Animal类或者为Animal的子类然后调用LegCount方法,从而避免了增添大量复杂的应对机制。

4、接口隔离原则

这个原则强调不应该强迫实现类实现它不需要的接口方法。

假设有如下这个接口类:

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
}

它有三个互不相关的方法,然而子类必须实现永远使用不上的方法。当我们在接口类中新增一个抽象方法时,必须修改所有子类然后实现可能更离谱的方法。

那我们应该怎么设计避免这种错误呢?答案是接口隔离,如下:

interface IShape {
    draw();
}
interface ICircle {
    drawCircle();
}
interface ISquare {
    drawSquare();
}
interface IRectangle {
    drawRectangle();
}
interface ITriangle {
    drawTriangle();
}
class Circle implements ICircle {
    drawCircle() {
        //...
    }
}
class Square implements ISquare {
    drawSquare() {
        //...
    }
}
class Rectangle implements IRectangle {
    drawRectangle() {
        //...
    }    
}
class Triangle implements ITriangle {
    drawTriangle() {
        //...
    }
}
class CustomShape implements IShape {
   draw(){
      //...
   }
}

5、依赖倒置原则

这个原则强调在系统层次依赖关系中应该多引用抽象类型而非具体实现,另外一种说法是高层模块不应该依赖具体细节实现模块,两者都应该依赖上层的抽象模块。

我们以下面的代码进行说明:

class XMLHttpService extends XMLHttpRequestService {}
class Http {
    constructor(private xmlhttpService: XMLHttpService) { }
    get(url: string , options: any) {
        this.xmlhttpService.request(url,'GET');
    }
    post() {
        this.xmlhttpService.request(url,'POST');
    }
    //...
}

上面的示例违背了这个原则,高层模块的Http类依赖了低层模块的XMLHttpService类,当我们改变请求的实现时,比如使用NodeJs、Mock服务,我们需要细心地重构上述代码,得不偿失。

实际上高层模块的Http类不应该关心具体的的实现细节,接起来,我们进行改造。

定义一个请求抽象类Connection类

interface Connection {
    request(url: string, opts:any);
}

Http类的参数修改为抽象Connection类

class Http {
    constructor(private httpConnection: Connection) { }
    get(url: string , options: any) {
        this.httpConnection.request(url,'GET');
    }
    post() {
        this.httpConnection.request(url,'POST');
    }
    //...
}

这样的好处是,Http类可以轻松完成请求业务逻辑而无须关注具体的实现类型。

实际上,这条设计原则可以归结为以下几条具体的编码守则:

松花皮蛋的黑板报此处精彩内容已经被作者隐藏,请输入验证码查看内容
验证码:
请关注本站微信公众号,回复“wx”,获取验证码。在微信里搜索“松花皮蛋的黑板报”或者微信扫描右侧二维码都可以关注本站微信公众号。

文章翻译修改自:

https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688

三、Tip

推荐专注力不够好的的朋友比如我这种下载forest这个软件,如果达成一个目标就加一个树,如果途中玩手机,成长中的小树就会消失

四、Share

ES学习分享

京东技术解密之配置中心DUCC