还在为频繁变动的需求而苦恼吗?学会这个原则,让你从容应对

工作过程中,开发人员根据需求文档完成程序的开发任务,但是,程序投入测试使用的时候,经常因为各种各样的原因,比如,用户体验不好、操作不方便等,需要变动需求,甚至添加新功能,这可能就会导致开发人员原来设计的方案不能满足新变动的需求。

那么,如何应对频繁变动的需求呢,那么就需要本文将要介绍的原则登场了,即开闭原则。

介绍开闭原则之前,首先会结合例子来讲解C++提供的std::sort排序函数的用法,主要是为辅助后续要说明的开闭原则的示例,然后介绍开闭原则的两个特性,接着再叙述开闭原则常用应用场景,最后会详细介绍一个应用开闭原则的经典例子,该例子可以细细推敲,相信对加深开闭原则的理解。

一、排序函数的用法

1、std::sort的一般用法

首先创建测试存储整数类型向量,然后写入乱序的整数数据。

std::vector<int> JDebugSort::Build()
{
    std::vector<int> vec_data;
    vec_data.push_back(2);
    vec_data.push_back(1);
    vec_data.push_back(3);
    vec_data.push_back(4);
    return vec_data;
}

调用std::sort对上面创建的向量变量进行排序。

std::vector<int> vec_data_01 = Build();
std::sort(vec_data_01.begin(), vec_data_01.end());
Print(vec_data_01);

运行程序,依次输出向量存储的数据如下图所示,可以看出std::sort默认按照升序进行排列。

如果想要降序排列,怎么办呢,首先需要自定义比较函数,具体实现如下所示

bool compare(int a, int b)
{
    return (a > b);
}

同样调用std::sort对上面的向量进行排序,但是std::sort的第三个参数为上面定义实现的比较函数compare。

std::vector<int> vec_data_02 = Build();
std::sort(vec_data_02.begin(), vec_data_02.end(), compare);
Print(vec_data_02);

再次运行程序,其输出的信息如下,可以看出std::sort已经按照降序进行排列。注意如果将compare内部实现使用的大于号(>)修改为小于号(<), 那么就会变成升序排序。

除了自定义比较函数来决定std::sort的排序顺序之外,如果排序的的类型是普通数据类型, 比如整数类型,那么可以直接使用标准库使用提供的函数std::less或者std::greater来决定是升序还是降序排列。

从输出的打印信息看,std::less是升序排列,std::greater是降序排列。

2、类内部重载

上面讲述的是std::sort的一般用法,接下来将讲解类内部重载operator<来控制升降序。假设需要对部门的ID进行排序,定义实现如下所示的部门类,该类主要存储部门id和部门的名称,并且重载了operator<运算符。

class Department
{
public:
    explicit Department(int id, const std::string &name)
        : m_id(id)
        , m_name(name)
    {}
    Department(){}
    ~Department(){}

    bool operator<(const Department& deparment) const
    {
        //return m_id < deparment.m_id; // 升序
        return m_id > deparment.m_id;  // 降序
    }

    int GetId() const { return m_id;}
    std::string GetName() const {return m_name;}

private:
    int m_id;
    std::string m_name;
};

为了验证效果,首先定义存储Department类型的向量,同样存入部门id号为乱序的部门信息

std::vector<Department> JDebugSort::BuildDeparment()
{
    std::vector<Department> vec_data;
    vec_data.push_back(Department(3,"li"));
    vec_data.push_back(Department(2,"zheng"));
    vec_data.push_back(Department(5,"xxx"));

    return vec_data;
}

调用std::sort对上面的Department类型的向量进行排序

std::vector<Department> vec_data_05 = BuildDeparment();
std::sort(vec_data_05.begin(), vec_data_05.end());
PrintDepartment(vec_data_05);

运行打印结果如下所示,输出的部门信息按照部门id进行降序排列。

3、自定义比较类

除了自定义函数来确定升降序之外,还可以自定义类来确定升降序,而自定义类需要重载operator()运算符。这里还是采用上面的部门类来进行说明。operator()运算符传入两个表示部门信息的参数,函数内部还是通过部门id号来确定升降序。

class JLess
{
public:
    bool operator()(const Department& d1, const Department& d2)
    {
        return d1.GetId() < d2.GetId(); //升序排列
    }
};

调用方法如下所示,从实际的测试结果看,std::sort会优先调用operator()中定义排序顺序,不管自定义类中是否重载了operator<运算符。

std::vector<Department> vec_data_06 = BuildDeparment();
std::sort(vec_data_06.begin(), vec_data_06.end(), JLess());
PrintDepartment(vec_data_06);

还可以定义如下所示的自定义比较类,还是内部直接通过部门对象进行比较,实际上最终是调用部门类重载的operator<运算符来确定排序顺序的。

class JLess2
{
public:
    bool operator()(const Department& d1, const Department& d2)
    {
        return d1 < d2;
    }
};

二、开闭原则的特性

开闭原则意思就是可以扩展,但是又不能修改。体现在代码上就是添加新的代码,但是不需要改变已经运行的代码。概况来说,它的两个基本特性是:1)、对于扩展是开放的,2)、对于更改是封闭的。

那么如何在不改变模块原有的代码的情况下,添加新的功能点呢?

三、开闭原则的应用

关键是抽象,即有一个抽象的基类,而可能变动的行为则由派生类来实现。

客户端与服务端的通信。client类使用的是抽象类client interface,  而实际功能由server去实现,当使用的时候,创建具体的server对象,然后将其传递给client对象,如果希望client类使用不同的server类,那么只要新的server类是从client interface类派生出来,那么新的server对象就可以传递给client对象,而且client类不需要进行任何修改。

上面的例子是遵循开闭原则,而另一个比较常见并且遵循开闭原则的是模版方法,简单来说就是,基类实现基本通用的逻辑,并且该逻辑过程包含虚函数或纯虚函数,而虚函数或者纯虚函数的具体功能则由派生子类来实现。例如,下图的模版方法,TemplateMethod是实现通用的逻辑,primitive1和primitive2则是虚函数或纯虚函数,需要子类SubClass1和SubClass2来实现。

四、经典示例

现在需要制作一个绘制正方形和圆形的应用程序,并且按照指定顺序进行绘制。那么如何实现才能遵循开闭原则呢。

根据前面介绍的std::sort用法,如果需要按照指定顺序来绘制图形,那么可以利用std::sort函数,并且自定义比较类模版,重载operator<运算符。

自定义比较类模版如下,该模版类重载operator()运算符。

template <typename T>
class Less
{
public:
    bool operator()(const T t1, const T t2)
    {
        return (*t1) < (*t2);
    }
};

定义形状基类,Draw是纯虚函数,需要子类实现,重载运算符operator<主要是为了控制绘制形状的顺序。静态成员变量m_OrderTable存放绘制形状顺序的名称。

class Shape
{
public:
    Shape();
    virtual ~Shape();
    virtual void Draw() = 0;

    bool Precedes(const Shape&) const;
    bool operator<(const Shape&) const;
private:
    static const char* m_OrderTable[];
};

实现形状基类,operator<运算符内部调用Precedes,Precedes实现升序绘制形状。m_OrderTable的赋值需要实现具体子类之后才能给出。

Shape::Shape(){}

Shape::~Shape(){}


bool Shape::Precedes(const Shape& s) const
{
    const char * this_type = typeid(*this).name();
    const char * arg_type = typeid(s).name();
    int i_thisord = -1;
    int i_argord = -1;

    int i_size = sizeof(m_OrderTable)/sizeof(m_OrderTable[0]);
    for(int i = 0; i < i_size; i++)
    {
        const char* p_table_entry = m_OrderTable[i];
        if (p_table_entry != nullptr)
        {
            if (strcmp(p_table_entry, this_type) == 0)
            {
                i_thisord = i;
            }
            if (strcmp(p_table_entry, arg_type) == 0)
            {
                i_argord = i;
            }
            if (i_thisord >= 0 && i_argord >= 0)
            {
                break;
            }
        }
    }
    return i_thisord < i_argord;
}

bool Shape::operator<(const Shape& s) const
{
    return Precedes(s);
}

定义实现正方形

/// 定义
class Square : public Shape
{
public :
    Square();
    virtual ~Square() override;
    virtual void Draw() override;
};

/// 实现
Square::Square(){}

Square::~Square(){}

void Square::Draw()
{
    LOG(INFO) << "draw Square";
}

定义实现圆形

/// 定义
class Circle : public Shape
{
public :
    Circle();
    virtual ~Circle() override;
    virtual void Draw() override;
};

/// 实现
Circle::Circle(){}

Circle::~Circle(){}

void Circle::Draw()
{
    LOG(INFO) << "draw Circle";
}

实现完成正方形和圆形之后,就可以给m_OrderTable赋值,其先后顺序就确定了对应形状的绘制顺序。

const char* Shape::m_OrderTable [] =
{
    typeid(Circle).name(),
    typeid (Square).name()
};

实现绘制所有形状的逻辑,函数DrawAllShape接受存储类型为Shape*的向量,内部实现如下所示,调用std::sort对向量进行排序,然后再循环调用向量中的每一个对象的Draw进行绘制。

void JDebugOCP::DrawAllShape(std::vector<Shape*> &allShape)
{
    std::vector<Shape*> order_all_shape = allShape;
    std::sort(order_all_shape.begin()
             ,order_all_shape.end()
             ,Less<Shape*>());

     std::vector<Shape*>::const_iterator iter_const;
     for(iter_const = order_all_shape.begin(); iter_const != order_all_shape.end(); iter_const++)
     {
         (*iter_const)->Draw();
     }
}

最后使用的方式如下,创建存储各个形状的的向量,并且创建的形状不需要按照顺序。再将其向量传入上面定义实现的函数DrawAllShape。

std::vector<Shape*> vec_shape;
vec_shape.push_back(new Square());
vec_shape.push_back(new Circle());

DrawAllShape(vec_shape);

运行的结果如下,程序按照m_OrderTable赋值的顺序绘制图形。并且后续添加新的形状,并且指定输出顺序,那么也只要调整驱动表m_OrderTable即可,其他代码都不需要改变,这也满足了开闭原则。

[virtual void Circle::Draw():66] draw Circle
[virtual void Square::Draw():57] draw Square

五、总结

std::sort默认按照升序进行排列,如果重载使用大于号,那么按照降序排列,如果使用小于号,那么按照升序排列。开发过程中,遵循开闭原则能够有效解决预防频繁变动的需求,开闭原则的特性就是:对于扩展是开放的,对于更改是封闭的。开闭原则的关键就是抽象,抽象体现在C++就是虚函数或者纯虚函数。

《还在为频繁变动的需求而苦恼吗?学会这个原则,让你从容应对》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注