Loading

设计模式的8个原则

依赖倒置原则——抽象(稳定)不应该依赖于实现细节(不稳定),实现细节要依赖于抽象

不妨假设一个场景来方便说明设计原则:我们准备开一家动物园,目前有老虎、狮子、熊猫等动物,然后对外开放展览,让这些动物出来活动活动。
那么,在不使用设计模式的情况下可能会写出这样的代码:

class Tiger {
public:
    void display() {
        std::cout << "tiger" << std::endl;
        // ...
    }
};

class Lion {
public:
    void display() {
        std::cout << "lion" << std::endl;
        // ...
    }
};

class Panda {
public:
    void display() {
        std::cout << "panda" << std::endl;
        // ...
    }
};

class Zoo {
public:
    Zoo(const std::vector<Tiger>& tigers, const std::vector<Lion>& lions,
        const std::vector<Panda>& pandas)
        : m_tigers(tigers), m_lions(lions), m_pandas(pandas) {}

public:
    void display() {
        for(auto& tiger : m_tigers) {
            tiger.display();
        }
        for(auto& lion : m_lions) {
            lion.display();
        }
        for(auto& panda : m_pandas) {
            panda.display();
        }
    }

private:
    std::vector<Tiger> m_tigers{};
    std::vector<Lion> m_lions{};
    std::vector<Panda> m_pandas{};
};

我们在动物园这个类中为每个动物都声明了一个数组用来储存每种动物,表示老虎有多少只,狮子有多少只,熊猫有多少只,然后在display方法中会依次调用每个具体动物对象的方法。目前为止,这段代码确实可以完成我们上述的要求。但不幸的是,动物园的规模在壮大,我们又引入了猴子、大象等新的动物,为了展示它们,我们不得不每次都修改动物园这个类,为其添加新的具体动物成员,显然,现在它已经不是一段好的代码了。
为了拯救程序员的肝和头发,我们需要使用更好的形式来编写代码:

class Animal {
public:
    virtual void display() = 0;
};

class Tiger : public Animal {
public:
    void display() override {
        std::cout << "tiger" << std::endl;
        // ...
    }
};

class Lion : public Animal {
public:
    void display() override {
        std::cout << "lion" << std::endl;
        // ...
    }
};

class Panda : public Animal {
public:
    void display() override {
        std::cout << "panda" << std::endl;
        // ...
    }
};

class Zoo {
public:
    Zoo(const std::vector<Animal*>& animals) : m_animals(animals) {}

public:
    void display() {
        for(auto& animal : m_animals) {
            animal->display();
        }
    }

private:
    std::vector<Animal*> m_animals{};
};

进一步思考我们发现,无论是狮子还是老虎,它们都应该属于动物,动物要求的是能展示动物,而不关心老虎是怎么叫的,狮子是怎么跑的这些细节。那么我们抽象出一个Animal的基类,所有的具体的动物都可以继承这个基类,重载其中的纯虚函数来表现具体老虎的形态或者狮子的形态;然后动物园中只维护动物这个基类的数组,就算再有新的动物加入,也不需要再去修改动物园这个类了。
照应该部分的原则,我们可以说,动物园依赖动物类这个抽象基类实现;动物基类中通过纯虚函数将具体的实现交给后面继承的具体动物来实现,无需依赖某种动物的实现细节,优化后的代码是好的代码设计。

开闭原则——对扩展开放,对修改封闭

还是看上一节代码的对比,优化前的代码每次增加新的动物都需要修改动物园这个类,它违反了开闭原则;而优化后的代码无论增加多少种动物,都不影响动物园类的使用,因此遵循了开闭原则。

单一职责原则——一个类应该仅有一个引起其变化的原因,变化的方向隐含着类的责任

单一职责说的是应该划分和规划好每个类的职责范围,而不要对所有的任务都大包大揽,类比现实的话就是“做好自己分内的事情,不要狗拿耗子多管闲事”。还是用动物举例,一个老虎对象,应该吃肉就可以长得很壮,可以说肉会影响老虎的变化,这个变化说明了老虎是肉食动物只吃肉(如果给它加了一个可以吃竹子的方法,那就改变了老虎这个类的性质,你让熊猫怎么想?)。举的这个例子比较简单,你可能会腹诽谁会犯这么离谱的错误,但当一个类的功能随着需求不断增加,变得越来越臃肿时,也许我们就该考虑是不是在不经意间让这个类做了太多原本不属于它职责的事情了。

里氏替换原则——子类应该能够替换它的基类

这个原则让我们从父类和子类间关系的角度来思考继承的使用。类的继承应该满足的是从属关系,例如,老虎是一种动物,所以老虎派生自动物类,说明了老虎is-a动物,且老虎是对动物抽象的一种具体实现。如果在一段代码中发现一棵树也继承自动物,那就该考虑下这里是不是就不应该使用继承来表述两者的关系了。

接口隔离原则——接口应该尽可能的小而完备,对外部程序无用的部分应保持透明

这一条原则可以看作是对类内部实现的规范,通过合理使用publicprotectedprivate关键字,只对外暴露一些必要的接口,而不要将类的一些具体细节的方法也暴露出去,因为实现细节就决定了其不稳定的特性,当暴露出去不稳定的方法,而其他的程序又使用到了这个接口时,就会影响整体程序的使用。

优先使用组合而不是继承

这条要对照着里氏替换原则来看,并不是所有的类之间都是使用继承来联系的,或者说在现在看来,组合应该是更加“松耦合”的联系方式。

封装变化点——一侧的变化不会对另一侧产生不良影响

这是更高层面的要求,通过对业务整体的梳理,应该可以抽象出一层接口层,来将具体的实现和具体的业务隔离开来,使得当内部实现改变了时,外部的表现不会受到影响。

针对接口编程,而不要针对具体实现编程——保证接口的标准化

这条原则提醒我们在编程时抽象的必要性,将必要的逻辑抽象出来,能够获得更加灵活的能力和稳定的表现。再回到最初的动物园的例子,庆幸我们抽象出了一个动物基类,否则随着后续动物园的壮大,动物园这个类也会变得越来越臃肿不堪。抽象出了动物园这个抽象类后,就可以说获得了一个动物的标准化的接口,后续无论是增加动物,还是修改动物园的表现形式,都只需要和这个抽象类打交道就可以了。

posted @ 2025-03-08 15:42  cwtxx  阅读(26)  评论(0)    收藏  举报