C++ Papers promised me not to go anywhere else if/else?

Learn English one sentence a day and make a little progress every day:
  • "Without purpose, the days would have ended, as such days always end, in disintegration."
  • "Without a goal, the day will end. It always ends in pieces."

Preface

Yang Ge wrote an interesting article before " Promise me, don't go anywhere else if/else?| CodeSheep "Implements three ways of enumerating classes, factory mode, and policy mode to eliminate if / else chains in the text using Java language.I love the way the content goes from shallow to deep.

Seeing that there was a message about a partner who wanted to see the C++ version, I wrote this article (with the consent of Goat Brother).However, since C++ does not have enumeration classes, this approach is not covered in this paper, but it will take you step by step to optimize the plant mode and strategy mode.

text

Bad if / else links

If / else can be said to be the first branch sentence we learn when we learn programming. It is easy to understand and there are many if / else examples everywhere in our life:

The wife calls the programmer's husband: "Buy a kilo of buns and bring them back when you leave work. If you see a watermelon seller, buy one."
That night, the programmer's husband entered the house with a bun in his hand.
"Why did you buy a bun?" said the wife angrily.
The husband said, "Because he saw a watermelon seller."

Wife's thoughts:

Buy a kilo of buns;
If (see watermelon sellers)
  Buy a (watermelon);

And the programmer's husband's program:

if(! See watermelon sellers) 
   Buy a kilo of buns;
else
   Buy one (bun);

Very vivid examples of life!If you are a programmer and make the same mistake of thinking, don't ask your daughter-in-law why, just kneel on the keyboard:

Enter the main topic of this article.Consider the following chestnuts: Generally speaking, our normal background management system has so-called role concept, different administrators have different privileges and can perform different operations.

  • System Administrator (ROLE_ROOT_ADMIN): Has A Operational Rights
  • Order Administrator (ROLE_ORDER_ADMIN): Has B Operational Rights
  • Ordinary User (ROLE_NORMAL): Has C Operational Rights

Assuming a user comes in, we need to judge what behavior they have based on their role.If we use too many if / else series, we will feel subconsciously that this is not easy. I put on a series of series:

class JudgeRole
{
public:
    std::string Judge( std::string roleName )
    {
        std::string result = "";
        if( roleName == "ROLE_ROOT_ADMIN" )       // system administrator
        {
            result = roleName + "has A permission";
        }
        else if( roleName == "ROLE_ORDER_ADMIN" ) // Order Administrator
        {
            result = roleName + "has B permission";
        }
        else if( roleName == "ROLE_NORMAL" )       // Ordinary user
        {
            result = roleName + "has C permission";
        }
        return result;
    }
};

When there are dozens of roles in the system, isn't that dozens of if / else nested, the visual effect is absolutely refreshing...This is a very inelegant way to do it.

Someone else would have read the code and shouted, "I X, which waterborne one wrote it!"

When you hear this, don't say, "I'll switch / case instead."Don't say anything, don't say oh, or you might be carrying your bag home...

Because switch / case is the same as if / else, it's hard to write, hard to read, and hard to expand.

Next, let's talk about a few ways to improve it. Don't let if / else go anywhere else.

Factory mode - does it not smell?

The fact that different roles do different things clearly provides an opportunity to use the factory model, and we just need to define the different situations separately and aggregate them into factories.

First, define a common interface, RoleOperation, in which there is a pure virtual function, Op, for the implementation of derived classes (subclasses):

// base class
class RoleOperation
{
public:
    virtual std::string Op() = 0; // Pure virtual function
    virtual ~RoleOperation() {} // virtual destructor
};

Next, for different role classes, inherit the base class and implement the Op function:

// system administrator(Yes A Operational privileges)
class RootAdminRole : public RoleOperation {
public:
    RootAdminRole(const std::string &roleName)
            : m_RoleName(roleName) {}

    std::string Op() {
        return m_RoleName + " has A permission";
    }

private:
    std::string m_RoleName;
};


// Order Administrator(Yes B Operational privileges)
class OrderAdminRole : public RoleOperation {
public:
    OrderAdminRole(const std::string &roleName)
            : m_RoleName(roleName) {}

    std::string Op() {
        return m_RoleName + " has B permission";
    }

private:
    std::string m_RoleName;
};

// Ordinary user(Yes C Operational privileges)
class NormalRole : public RoleOperation {
public:
    NormalRole(const std::string &roleName)
            : m_RoleName(roleName) {}

    std::string Op() {
        return m_RoleName + " has C permission";
    }

private:
    std::string m_RoleName;
};

Next, I'm writing a factory class, RoleFactory, that provides two interfaces:

  • RegisterRole member function to register role pointer objects to factories
  • GetRole member function to get corresponding role pointer object
// Role Factory
class RoleFactory {
public:
    // Get the factory instance, the factory instance is unique
    static RoleFactory& Instance() {
        static RoleFactory instance; // C++11 Thread Security Above
        return instance;
    }

    // Register pointer object to factory
    void RegisterRole(const std::string& name, RoleOperation* registrar) {
        m_RoleRegistry[name] = registrar;
    }

    // By name name,Get the corresponding role pointer object
    RoleOperation* GetRole(const std::string& name) {

        std::map<std::string, RoleOperation*>::iterator it;

        // from map Find a registered role and return the role pointer object
        it = m_RoleRegistry.find(name);
        if (it != m_RoleRegistry.end()) {
            return it->second;
        }

        return nullptr; // If the role is not registered, a null pointer is returned
    }

private:
    // Prohibit external construction and fiction
    RoleFactory() {}
    ~RoleFactory() {}

    // Prohibit external copy and assignment operations
    RoleFactory(const RoleFactory &);
    const RoleFactory &operator=(const RoleFactory &);

    // Save registered roles, key:Role Name , value:Role Pointer Object
    std::map<std::string, RoleOperation *> m_RoleRegistry;
};

Register (aggregate) all roles in the factory and encapsulate them as role initialization functions InitializeRole:

void InitializeRole() // Initialize role to factory
{
    static bool bInitialized = false;

    if (bInitialized == false) {
        // Register System Administrator
        RoleFactory::Instance().RegisterRole("ROLE_ROOT_ADMIN", new RootAdminRole("ROLE_ROOT_ADMIN"));
        // Register Order Administrator
        RoleFactory::Instance().RegisterRole("ROLE_ORDER_ADMIN", new OrderAdminRole("ROLE_ORDER_ADMIN"));
        // Register Ordinary User
        RoleFactory::Instance().RegisterRole("ROLE_NORMAL", new NormalRole("ROLE_NORMAL"));
        bInitialized = true;
    }
}

Next, with the help of the above factory, business code calls require only one line of code, and if / else is eliminated as plain as it is clear:

class JudgeRole {
public:
    std::string Judge(const std::string &roleName) {
        return RoleFactory::Instance().GetRole(roleName)->Op();
    }
};

Note that when using Judge, the InitializeRole function that initializes all roles (which can be placed at the beginning of the main function, etc.) is called first:

int main() {
    InitializeRole(); // Priority initialization of all roles to factory

    JudgeRole judgeRole;

    std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl;
    std::cout << judgeRole.Judge("ROLE_ORDER_ADMIN") << std::endl;
    std::cout << judgeRole.Judge("ROLE_NORMAL") << std::endl;
}

It is also easy to expand conditions by factory mode, simply adding new code without changing the previous business code, which is very in line with the Open Close Principle.


I don't know if my little partner finds it or not, but the above implementation of factory class seems to be in good order, but when improper use causes the program to run, what happens?

Let's start by analyzing the two external interfaces of the above factory class:

  • RegisterRole Registers Role Pointer Objects to Factory
  • GetRole Gets Role Pointer Object from Factory

Does it mean that a resource leak occurs when an object is not released?No, that's not the problem, and we don't have to release the pointer manually because the factory above is a "singleton mode". Its life cycle is from the first initialization to the end of the program. When the program is finished, the operating system will naturally recycle all the pointer object resources in the factory class.

But when we manually release the role pointer objects we get from the factory, there's a problem:

RoleOperation* pRoleOperation =  RoleFactory::Instance().GetRole(roleName);
...
delete pRoleOperation; // Manually Release Pointer Object

If we manually release the pointer object, it will also cause the pointer object stored in the map in the factory to point to empty, which will cause the program to burst when used again!Examples include the following:

class JudgeRole {
public:
    std::string Judge(const std::string &roleName) {
        RoleOperation *pRoleOperation = RoleFactory::Instance().GetRole(roleName);
        std::string ret = pRoleOperation->Op();
        delete pRoleOperation; // Manually Release Pointer Object
        return ret;
    }
};

int main() {
    InitializeRole(); // Priority initialization of all roles to factory

    JudgeRole judgeRole;

    std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl;
    std::cout << judgeRole.Judge("ROLE_ROOT_ADMIN") << std::endl; // Error!The program will run away!

    return 0;
}

The code above causes the program to run away when the second ROLE_ROOT_ADMIN role pointer object is used, because the ROLE_ROOT_ADMIN role pointer object is manually released after the first use, and the factory map stores the empty pointer.

Can you optimize it?Because some programmers will manually release the target obtained from the factory.

The disadvantage of the above factory class is that the new ly initialized pointer object has only been initialized once. If the pointer object is manually released, it will cause the pointer object to point empty and reuse will cause the system to crash.

To improve this problem, we put the new initialization into the member function of the factory class to get the pointer object, which returns the new initialized pointer object each time the member function is called, then the external pointer object needs to be released manually.

The following factory class, which improves the above problem, uses template technology to further encapsulate the factory class so that it can be used by any class with polymorphic properties, whether it is a role class or another class. It can be said to be an omnipotent factory class:

 

 

Next, use the new Universal factory template class for the role object in this example.

1. The way to register (aggregate) roles into a factory is to construct a ProductRegistrar object, which you should be aware of when using:

  • The template parameter ProductType_t specifies the base class (for example, RoleOperation)
  • The template parameter ProductImpl_t specifies derived classes (such as RootAdminRole, OrderAdminRole, and NormalRole in this example)

We have improved the InitializeRole initialization role function using a new registration (aggregation) method, see the following:

void InitializeRole() // Initialize role to factory
{
    static bool bInitialized = false;

    if (bInitialized == false) {
        // Register System Administrator
        static ProductRegistrar<RoleOperation, RootAdminRole> rootRegistrar("ROLE_ROOT_ADMIN");
        // Register Order Administrator
        static ProductRegistrar<RoleOperation, OrderAdminRole> orderRegistrar("ROLE_ORDER_ADMIN");
        // Register Ordinary User
        static ProductRegistrar<RoleOperation, NormalRole> normalRegistrar("ROLE_NORMAL");
        bInitialized = true;
    }
}

2. The function to get the role pointer object from the factory is GetProduct. Note that:

  • After using the role pointer object, delete the resource manually.

We improved the Judge function using the new way of getting role objects, see the following:

class JudgeRole {
public:
    std::string Judge(const std::string &roleName) {
        ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance();
        // Get the corresponding pointer object from the factory
        RoleOperation *pRoleOperation = factory.GetProduct(roleName);
        // Call the corresponding permissions of the role
        std::string result = pRoleOperation->Op();
        // Manually Release Resources
        delete pRoleOperation;
        return result;
    }
};

Well, it's easy to miss things like releasing resources manually every time.If we miss it, it will cause a memory leak.To avoid this possibility, let's use the Smart Pointer to help us manage it:

class JudgeRole {
public:
    std::string Judge(const std::string &roleName) {
        ProductFactory<RoleOperation>& factory = ProductFactory<RoleOperation>::Instance();
        std::shared_ptr<RoleOperation> pRoleOperation(factory.GetProduct(roleName));
        return pRoleOperation->Op();
    }
};

With the std::shared_ptr reference count smart pointer, we don't need to keep in mind the things we need to manually release our resources (we usually forget...)The smart pointer automatically releases the pointer resource when the number of references is 0.

Come on, let's go on, besides factory mode, strategy mode might as well try

Strategy mode - is it not fragrant?

The difference between strategy mode and factory mode is not great!The strategy pattern also employs object-oriented inheritance and polymorphism.

On the basis of the above factory pattern code and following the guidelines of the policy pattern, we will also create a so-called policy context class, named RoleContext here:

class RoleContext {
public:
    RoleContext(RoleOperation *operation) : m_pOperation(operation) {
    }

    ~RoleContext() {
        if (m_pOperation) {
            delete m_pOperation;
        }
    }

    std::string execute() {
        return m_pOperation->Op();
    }

private:
    // Prohibit external copy and assignment operations
    RoleContext(const RoleContext &);
    const RoleContext &operator=(const RoleContext &);

    RoleOperation *m_pOperation;
};

It is clear that the parameter operation passed in above represents a different "policy".By passing in different roles in the business code, we get different results:

class JudgeRole {
public:
    std::string Judge(RoleOperation *pOperation) {
        RoleContext roleContext(pOperation);
        return roleContext.execute();
    }
};

int main() {
    JudgeRole judgeRole;

    std::cout << judgeRole.Judge(new RootAdminRole("ROLE_ROOT_ADMIN")) << std::endl;
    std::cout << judgeRole.Judge(new OrderAdminRole("ROLE_ORDER_ADMIN")) << std::endl;
    std::cout << judgeRole.Judge(new NormalRole("ROLE_NORMAL")) << std::endl;

    return 0;
}

Of course, the above policy classes can be further optimized:

  • Encapsulate further with template technology so that it is not restricted to role classes.
// Policy Class Template
// Template parameters ProductType_t,Represents a base class
template <class ProductType_t>
class ProductContext {
public:
    ProductContext(ProductType_t *operation) 
                : m_pOperation(operation) {
    }

    ~ProductContext() {
        if (m_pOperation) {
            delete m_pOperation;
        }
    }

    std::string execute() {
        return m_pOperation->Op();
    }

private:
    // Prohibit external copy and assignment operations
    ProductContext(const ProductContext &);
    const ProductContext &operator=(const ProductContext &);

    ProductType_t* m_pOperation;
};

It doesn't make much difference how you use it, just specify that the class template parameter is a base class (for example, RoleOperation):

class JudgeRole {
public:
    std::string Judge(RoleOperation *pOperation) {
        ProductContext<RoleOperation> roleContext(pOperation);
        return roleContext.execute();
    }
};

Total Responding

Both C++ and Java languages are object-oriented programming, so both can reduce code coupling through object-oriented and polymorphic attributes, while also making code scalable.So for writing code, don't rush to get started. Think first about whether there is a simpler, better way to do it.

Bjarne Stroustrup, the father of C++, once mentioned that the three virtues of process programmer are laziness, impatience and pride. One of them is the quality of laziness that tells us to think hard and avoid wasting too much energy (such as tapping code).

If there are any errors or inappropriateness, you can feedback in this public number and learn to communicate together!

Recommended reading:

Focus on the public number and reply to "I want to learn" in the background to get free access to the carefully organized "Server Linux C/C++" growth journey (books + mind maps)

Tags: C++ Java Programming Linux

Posted on Sat, 15 Feb 2020 00:46:40 -0500 by Tjk