Jonathan Boccara's blog

Mikado Refactoring with C++ Feature Macros

Published January 24, 2020 - 0 Comments

This is a guest post by Vaughn Cato. Vaughn has been developing using C++ since the early 90’s and is still learning!  You can find him on Twitter @vaughncato. Thanks to Ricardo Nabinger Sanchez for his review of the article.

Refactoring is a technique for making improvements to the design of a code base without changing its behavior. The basic principles of refactoring tell us that these changes should be applied in small steps, so that the structure of the code is always improving and never broken.

Sometimes it is easy to see small improvements that can be made to the code and to see how these small improvements might eventually lead to the bigger design changes that we want, but there are often cases where making a large design change can seem impenetrable. Maybe we can see the long-term goal, but it’s not clear any one step we can take that will send us in the right direction. Likewise, we may be able to see various small improvements we can make, but we’re not sure if they will directly help with our ultimate goal.

In this article, we’ll talk about ways a developer might attack a large refactoring.  Then we’ll discuss a lesser-known approach called the Mikado Method, which allow us to systematically turn a large refactoring into a series of small steps.  And finally, we’ll see how using C++ feature flag macros make the Mikado Method easier to apply in practice.

Bottom-up refactoring

Applying common refactoring techniques, we would tackle a complex refactoring by thinking about aspects of the code that makes the change difficult and trying to address them one at a time. There could be assumptions that have propagated through the code base that would now be violated, and each one of those assumptions needs to be addressed to make the code more amenable to change. Maybe there are parts of the code base that are difficult to understand, making it unclear how to make the larger change. We’ll need to improve these parts to make dependencies clearer.

With this approach, we only make changes which we know will not break anything. We extract functions, slide statements, split loops, and do any other micro-refactoring necessary to make the code easier to work with.  If everything goes well, these small changes lead to other improvements, and our large design change starts to seem less daunting. We’ll eventually find that the code base is in a good enough state that our original desired change is now easy.

These are good goals, but like with any bottom-up approach, the risk is that a lot of time could be spent in ways that ultimately doesn’t help with the final goal.

Big Bang Refactoring

Another approach is to do Big Bang Refactoring. We do a little up-front planning to try to define the goal and a general approach, but instead of working out every detail, we just make the most important changes first and try to fix everything that breaks. Maybe we create a new class which has the kind of API we wished for. Then we try to move code from various places in our codebase to implement the new class and we change old code to use the new class.

Everything doesn’t work on the first try of course. With Big Bang Refactoring, it is expected that it is going to take a few iterations to get everything working again. Maybe functionality is missing from the new class that we didn’t initially realize needed to be there, so we add that. Maybe the new class needs to have access to certain data that we didn’t expect, so we provide ways to pass that data around. And of course, we made some mistakes along the way and we’ve introduced bugs, so we have to fix those, but eventually we chase down all the little problems and fix them and everything is working again. At least we hope.

However, there is a big risk with this approach. The code may be in an unusable state for an indefinite period of time. Making changes in one place leads to changes in others, which leads to changes in others. As we continue chasing down issues and making changes, we could start to get the feeling that maybe we made a mistake. Maybe this is harder than it should be, or maybe we should have taken a different approach. We may also find that we’ve introduced a bug that is difficult to reproduce. We are faced with a difficult decision. Should we try to make a course correction, partially reverting what we’ve done? Should we throw everything that we’ve done away and start over? Or should we press ahead in the hope that you’ll eventually be able to get code back under control? Much work could be wasted if we make the wrong decision.

The Mikado Method for top-down refactoring

The Mikado Method offers a third alternative. It is a technique for breaking up large refactoring tasks into smaller ones in a systematic way, such that the code is practically never in a broken state.

With this approach, we start like we are going for the Big Bang, making a big change and dealing with the consequences. However, instead of fixing the unexpected side-effects that inevitably arise, we stop, make a note of the issues we are running into, and then undo our changes. We are now back to a code base that works, but with some new knowledge. We have some additional insight into what is going to make this change difficult.

Now, with the code still in a good state, we can take the time to think about the issues we ran into. What made these issues occur? What could be done differently? Perhaps we realize that if certain logic was factored out and centralized, our main change would have been much easier. Perhaps we realize that if some hidden dependencies were made more explicit, then it would have been easier to make the change at a higher level.

This ultimately leads to a new refactoring decision. We are back to wanting to make a refactoring, just a more basic one. Perhaps this is still a large refactoring, where all the possible side-effects are unclear. This is where the Mikado Method starts to take form. Applying the same principle again, we make the change and see what happens. If there are issues, we make a note of the unexpected consequences and what we could do about them, but then we revert to the last working state.

This leads us to a tree structure of refactorings. The root of the tree is the main change that we wanted to make. The immediate children are the changes necessary to make the root change easy. The grandchildren are the changes necessary to make the child changes easy, and so forth.

Eventually, we get to the leaf nodes of the tree. These leaf nodes are the atomic refactoring steps that we can take. They are easy and quick and have no side-effects. Applying the leaf refactorings and pruning them from the tree, new leaf changes are revealed. These leaf changes should now have become easy atomic refactorings themselves. If we continue this process, we eventually end up back at our root change. The root change is the reason we set this whole process in motion, but it is now itself an easy change, and we are done.

Avoiding losing work

The Mikado Method ultimately provides a more disciplined approach to large-scale refactoring. Instead of using bottom-up refactoring that we hope will eventually lead to a better design, every step has been directly tied back to our larger goal. There is no unnecessary work.

Except — what about all the undoing? We’ve had to make changes, and undo them and them redo them again later, and we’ve had to do this many times. This seems like a lot of extra work by itself. This is probably why the Big Bang Refactoring seemed appealing in the first place. Maybe the code will be broken for a while, but at least we would always be moving forward.

There are some source code control approaches to address this. For example, with Git, we can easily create branches. Instead of undoing, we can store our attempted change in a new branch and then switch back to the main branch where all the code is still in good shape. Later, instead of repeating the change, we can merge the change from the new branch back into our main branch.

This may be a viable approach, but merges are not always easy. Especially in this case, we know that child changes will have to be made that are directly connected to the parent change. There are going to be conflicts that have to be worked through for nearly every merge.

Using feature flag macros

Here, C++ offers a solution: the feature flag macro. Instead of making a change that we will have to undo and then redo again, we can make a change that is easy to turn off and back on:

#define APPLY_SOME_BIG_DESIGN_CHANGE 1
#if !APPLY_SOME_BIG_DESIGN_CHANGE
// old code here
#else
// new code here
#endif

If necessary, the single feature flag can be used in many places throughout the code to turn a relatively large change into a single character change.

By using the feature flag, instead of undoing a change that had side-effects, we merely turn it off. Now, we are back to a fully-functioning code base. At any point, we can turn the feature flag on, see what the issues are, and turn it back off. This gives us an opportunity to make a child change, and once it is done, turn the parent flag on and see if there are any additional issues. If not, then the parent feature is now complete as well, and the change was effectively atomic.

We may even want to start with the feature flag turned off. This gives us a chance to write some code and get a better feeling for what the change is going to look like before trying it.

A stack of feature flags

After turning off the feature flag for the top-level change and deciding how to make this change easier, we may need a feature flag for a second-level change. After turning off the feature flag for the second-level change, we may need another one for a third-level change, etc. We end up with a list of related feature flags:

#define APPLY_SOME_HUGE_CHANGE 0
#define APPLY_SOME_LARGE_CHANGE 0
#define APPLY_SOME_MODERATE_CHANGE 0
#define APPLY_SOME_SMALL_CHANGE 1

Baking in features

Eventually, we find a small enough change that it can be applied without side-effects on the code base. We make the change, everything compiles, and all the tests pass. At this point, we no longer need the last-level feature flag. To avoid the code being littered with unnecessary macros, it is important to “bake in” the unneeded macro. We change any place in the code where the macro is used so that it just has the new code, then we remove the use of the macro. When the macro has no remaining uses, we remove macro itself.

Working this way, we are traversing through the overall tree structure by using a stack of changes that we are making, where each level of the stack has a corresponding feature flag that we can turn off and on. Generally, we are working on the smallest changes, possibly discovering other even smaller changes and adding a new level to the stack, or possibly completing the change and removing the level from the stack.

Baking out features

Even though we are generally working at the lowest levels of the stack, we also might want to temporarily turn on the larger changes again, just to remind ourselves of where we are heading, and what issues we are facing. At some point, we may even decide that we should have approached one of the changes differently. Maybe there is a different way to achieve the same basic goal of making something easier to change, but with fewer side-effects. When this happens, we may want to “bake out” some of the features. To bake out a feature, instead of keeping the new version of the code, we keep the old version and remove the use of the corresponding macro.

Note that we don’t try to revert every change we’ve made to the code when we make a course correction. We may have made many improvements to the code along the way. We’ve found ways to make changing the code easier, and we’ve baked these in as soon as they could be made without breaking the build or tests. Ultimately, these changes may have been unnecessary to achieve our main goal, but that doesn’t mean they weren’t valuable. No need to revert that work.

Additional advantages of feature macros

In addition to providing an easy way to turn features on and off, feature macros provide a nice way to compare the old and new code. It is easy to search through the code for the use of these macros, and once found, it is easy to compare the old and new versions. This can lead to other refactorings. Here’s an example taken from the Gilded Rose Refactoring Kata. Let’s say we were changing from using explicit indexing to using an iterator:

#if !USE_ITERATOR
if (items[i].name != "Sulfuras, Hand of Ragnaros") {
    --items[i].quality;
}
#else
if (item_iter->name != "Sulfuras, Hand of Ragnaros") {
    --item_iter->quality;
}
#endif

Code like shows that an abstraction is missing.  We’re having to change multiple lines of code even though the underlying idea is the same.  We can use the Extract Variable refactoring to make the code more similar:

#if !USE_ITERATOR
const auto &item = items[i];
if (item.name != "Sulfuras, Hand of Ragnaros") {
    --item.quality;
}
#else
const auto &item = *item_ptr;
if (item.name != "Sulfuras, Hand of Ragnaros") {
    --item.quality;
}
#endif

Then we can use Consolidate Duplicate Conditional Fragments on the #if itself:

#if !USE_ITERATOR
const auto &item = items[i];
#else
const auto &item = *item_iter;
#endif
if (item.name != "Sulfuras, Hand of Ragnaros") {
    --item.quality;
}

Like with any bottom-up refactoring, one refactoring like this can lead to other refactorings that make the code easier to understand and work with. Seeing the old and new versions of the code at the same time makes it easier to see these opportunities.

One technique among many

Refactoring is a large topic. The use of feature macros as described here is closely related to the idea of Branch by Abstraction and can be a first-step in this process. Feature flag macros ultimately provide a simple (if ugly) way to turn a large change into a smaller one, which can be a gateway to having the refactoring that we really want. Typically, any other refactoring step is preferable to using a macro when it can be done without breaking existing code. A macro just has the advantage that it always works, since the code that hasn’t been turned on doesn’t even have to be syntactically correct.

A Larger Example

In this example, we’ll start with the following code, which is a direct port of the Java code presented in The Mikado Method. Our objective is to replace the use of the concrete FileDB with an abstraction that will let us more easily use other kinds of databases:

#include <vector>
#include <string>
#include <iostream>
#include "gui.hpp"
#include "applicationexception.hpp"
#include "filedb.hpp"

using std::vector;
using std::string;

class UI {
public:
    UI();

    void showLogin()
    {
        vector < string > users = database.load("users");
        addLoginSelector(users);
        addButtons();
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

private:
    Frame frame;
    FileDB database;
    void addLoginSelector(const vector < string > & users);
    void addButtons();
};

class App {
public:
    void launch()
    {
        ui.showLogin();
    }
    static const string & getStorageFile()
    {
        return store_path;
    }
    static void setStorageFile(const string & store_path)
    {
        App::store_path = store_path;
    }
private:
    UI ui;
    static inline string store_path;
};

UI::UI()
{
    database.setStore(App::getStorageFile());
}

int main(int argc, char ** argv)
{
    vector < string > args(argv + 1, argv + argc);
    try {
        App::setStorageFile(args[0]);
        App app;
        app.launch();
    }
    catch (ApplicationException & e) {
        std::cerr << "Could not start application.\n";
        e.printStackTrace();
    }
}

We start with our Mikado goal of replacing the use of the concrete class FileDB with the use of a Database interface.  Using the feature-flag approach, we create a feature flag to represent this goal:

#include "applicationexception.hpp"
#include "filedb.hpp"

#define REPLACE_FILEDB_WITH_DATABASE_INTERFACE 0

And we’ll just naively replace the FileDB with a Database reference.

class UI {
// ...
private:
    Frame frame;
#if !REPLACE_FILEDB_WITH_DATABASE_INTERFACE
    FileDB database;
#else
    Database &database;
#endif

One of the first things that is clear is that this isn’t going to work without database being a reference or a pointer. The simplest thing to do is to make it a reference. We’ll make that be a sub-goal and introduce a new feature flag:

#include "filedb.hpp"

#define REPLACE_FILEDB_WITH_DATABASE_INTERFACE 0
#define CHANGE_DATABASE_TO_REFERENCE 0

// ...

class UI {
// ...
private:
    Frame frame;
#if !REPLACE_FILEDB_WITH_DATABASE_INTERFACE
#if !CHANGE_DATABASE_TO_REFERENCE
    FileDB database;
#else
    FileDB &database;
#endif
#else
    Database &database;
#endif

This leads to a cascade of small changes.  First, we have to initialize the reference, and to initialize the reference we have to have something to initialize it with, so we need to have a parameter to the UI constructor, which means we’ll need to pass something to the constructor, which means we’ll need the FileDB to exist in the App.

All of these steps seem like part of the same CHANGE_DATABASE_TO_REFERENCE step, so we’ll extend the usage of our flag instead of creating a new one:

#define REPLACE_FILEDB_WITH_DATABASE_INTERFACE 0
#define CHANGE_DATABASE_TO_REFERENCE 0

// ...

class UI {
public:
#if !CHANGE_DATABASE_TO_REFERENCE
    UI();
#else
    UI(FileDB &);
#endif
// ...
};

// ...

class App {
// ...
private:
#if !CHANGE_DATABASE_TO_REFERENCE
    UI ui;
#else 
    FileDB database;
    UI ui{database};
#endif
    static inline string store_path;
};

// ... 

#if !CHANGE_DATABASE_TO_REFERENCE
UI::UI()
#else
UI::UI(FileDB &database) : database(database)
#endif
{
database.setStore(App::getStorageFile());
}

We can now enable CHANGE_DATABASE_TO_REFERENCE without introducing any compilation errors and without breaking anything. This seems like a complete change, so we’ll go ahead and bake in CHANGE_DATABASE_TO_REFERENCE.

In the below snippet of code, the lines highlighted in gray represent lines that are left after baking in the change. The other are presented commented out for comparison (even if they wouldn’t be left in the code).

#include "filedb.hpp"

#define REPLACE_FILEDB_WITH_DATABASE_INTERFACE 0
// #define CHANGE_DATABASE_TO_REFERENCE 1

// ...

class UI {
public:
// #if !CHANGE_DATABASE_TO_REFERENCE
//     UI();
// #else
    UI(FileDB &);
// #endif
// ...

private:
    Frame frame;
#if !REPLACE_FILEDB_WITH_DATABASE_INTERFACE
// #if !CHANGE_DATABASE_TO_REFERENCE
//     FileDB database;
// #else
    FileDB &database;
// #endif
#else
    Database &database;
#endif
};

// ...

class App {
// ...
private:
// #if !CHANGE_DATABASE_TO_REFERENCE
//     UI ui;
// #else
    FileDB database;
    UI ui{database};
// #endif
    static inline string store_path;
};

// #if !CHANGE_DATABASE_TO_REFERENCE
// UI::UI()
// #else
UI::UI(FileDB &database)
: database(database)
// #endif
{
    database.setStore(App::getStorageFile());
}

If we try to enable REPLACE_FILEDB_WITH_DATABASE_INTERFACE , we now see that the main issue is that we don’t have a Database interface class at all.  So we’ll create that, extending the use of the REPLACE_FILEDB_WITH_DATABASE_INTERFACE flag.

#if REPLACE_FILEDB_WITH_DATABASE_INTERFACE
struct Database {
};
#endif

If we enable REPLACE_FILEDB_WITH_DATABASE_INTERFACE , we see that the next issue is that we don’t have a load() method, so we’ll add that:

#if REPLACE_FILEDB_WITH_DATABASE_INTERFACE
struct Database {
    virtual vector<string> load(const string &name) = 0;
};
#endif

Now, if we enable REPLACE_FILEDB_WITH_DATABASE_INTERFACE , the main issue is that our FileDB doesn’t derive from Database .  We could consider making FileDB derive from Database , but since FileDB is an external dependency, we’re going to need to try something else.  The simplest solution is to use an adapter. This seems like a separate step from REPLACE_FILEDB_WITH_DATABASE_INTERFACE , so we introduce a new feature flag:

#include "filedb.hpp"

#define REPLACE_FILEDB_WITH_DATABASE_INTERFACE 0
#define CHANGE_PARAMETER_TO_ADAPTER 0

// ...

class UI {
public:
#if !CHANGE_PARAMETER_TO_ADAPTER
    UI(FileDB &);
#else
    UI(FileDBAdapter &);
#endif
// ...
};

// ...

#if !CHANGE_PARAMETER_TO_ADAPTER
UI::UI(FileDB &database)
#else
UI::UI(FileDBAdapter &database)
#endif
: database(database)
{
    database.setStore(App::getStorageFile());
}

To make this work, we’ll need to create the adapter:

#if CHANGE_PARAMETER_TO_ADAPTER
struct FileDBAdapter {
};
#endif

If we try to enable CHANGE_PARAMETER_TO_ADAPTER , we see that we’re not actually passing an adapter for the database parameter, and we don’t have an adapter to pass, so we add that:

class App {
public:
    App()
#if !CHANGE_PARAMETER_TO_ADAPTER
    : ui(database)
#else
    : ui(database_adapter)
#endif
    {
    }

// ...

private:
    FileDB database;
#if CHANGE_PARAMETER_TO_ADAPTER 
    FileDBAdapter database_adapter;
#endif
    UI ui;
    static inline string store_path;
};

If we try to enable CHANGE_PARAMETER_TO_ADAPTER , this doesn’t work because FileDBAdapter doesn’t actually derive from the Database interface.

#if CHANGE_PARAMETER_TO_ADAPTER
//struct FileDBAdapter {
struct FileDBAdapter : Database {
};
#endif

If we try to enable CHANGE_PARAMETER_TO_ADAPTER , we find that we can’t because we haven’t actually implemented the load() method:

#if CHANGE_PARAMETER_TO_ADAPTER
struct FileDBAdapter : Database {
    vector<string> load(const string &name) override
    {
        return file_db.load(name);
    }
};
#endif

If we try to enable ADD_LOAD_METHOD_TO_ADAPTER , we see that we can’t because we don’t have access to the FileDB from the adapter, so we can add that as a parameter to the constructor and pass in the parameter when we create the App::database_adapter member:

#if CHANGE_PARAMETER_TO_ADAPTER
struct FileDBAdapter : Database {
    FileDB &file_db;
    // ...
};
#endif

// ...

class App {
// ...
private:
    FileDB database;
#if CHANGE_PARAMETER_TO_ADAPTER
     FileDBAdapter database_adapter;
//    FileDBAdapter database_adapter{database};
#endif
    UI ui;
    static inline string store_path;
};

If we try to enable CHANGE_PARAMETER_TO_ADAPTER , the compiler warns about a missing constructor in FileDBAdapter , so we add that also:

#if CHANGE_PARAMETER_TO_ADAPTER
struct FileDBAdapter : Database {
    FileDB &file_db;

    FileDBAdapter(FileDB &file_db)
    : file_db(file_db)
    {
    }

If we try to enable CHANGE_PARAMETER_TO_ADAPTER , we see that we can’t because it doesn’t have a setStore() method. We might be tempted to add this to our FileDBAdapter class, but that doesn’t seem to match the role of an adapter. Instead, we can move this functionality to App, which still knows the database is a FileDB. We can make this change without having to enable CHANGE_PARAMETER_TO_ADAPTER and without introducing any issues.

class App {
public:
    App()
#if !CHANGE_PARAMETER_TO_ADAPTER
    : ui(database)
#else
    : ui(database_adapter)
#endif
    {
        database.setStore(App::getStorageFile());
    }
    // ...
};

// ...

#if !CHANGE_PARAMETER_TO_ADAPTER
UI::UI(FileDB &database)
#else
UI::UI(FileDBAdapter &database)
#endif
: database(database)
{
//   database.setStore(App::getStorageFile());
}

We now find that if we try to enable CHANGE_PARAMETER_TO_ADAPTER, it won’t work because our database is a FileDB and can’t be initialized with a FileDBAdapter reference. However, we already have the REPLACE_FILEDB_WITH_DATABASE_INTERFACE flag for changing the database to be Database reference though, and if we enable that flag as well, everything works. This now seems like a complete change, so we can bake it all in, leaving us with this code:

// ...

struct Database {
    virtual vector < string > load(const string & name) = 0;
};

struct FileDBAdapter: Database {
    FileDB & file_db;
    FileDBAdapter(FileDB & file_db)
        : file_db(file_db)
    {
    }

    vector < string > load(const string & name) override
    {
        return file_db.load(name);
    }
};

class UI {
    public:
        UI(FileDBAdapter & database_adapter);

    void showLogin()
    {
        vector < string > users = database.load("users");
        addLoginSelector(users);
        addButtons();
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

private:
    Frame frame;
    Database & database;
    void addLoginSelector(const vector < string > & users);
    void addButtons();
};

class App {
public:
    App()
    {
        database.setStore(App::getStorageFile());
    }

    void launch()
    {
        ui.showLogin();
    }

    static const string & getStorageFile()
    {
        return store_path;
    }

    static void setStorageFile(const string & store_path)
    {
        App::store_path = store_path;
    }

private:

    FileDB database;

    FileDBAdapter database_adapter {
        database
    };

    UI ui {
        database_adapter
    };
    static inline string store_path;
};

UI::UI(FileDBAdapter & database_adapter)
    : database(database_adapter)
{
}

int main(int argc, char ** argv)
{
    vector < string > args(argv + 1, argv + argc));
    try {
        App::setStorageFile(args[0]);
        App app;
        app.launch();
    }
    catch (ApplicationException & e) {
        cerr << "Could not start application.\n";
        e.printStackTrace();
    }
}

At this point, there are no more flags, but there’s some easy additional refactoring we can do. The first is to generalize the UI constructor to take a Database instead of a FileDBAdapter.

class UI {
public:
//    UI(FileDBAdapter &);
    UI(Database &); // ...
};

// ...

// UI::UI(FileDBAdapter &database)
UI::UI(Database &database) : database(database)
{
}

Using the same approach as before, we can move the FileDB up another level into main():

class App {
public:
//    App()
//    : ui(database_adapter)
    App(FileDB &database)
    : database_adapter(database),
    ui(database_adapter)
    {
//        database.setStore(App::getStorageFile());
    }
// ...

private:
//     FileDB database;
//     FileDBAdapter database_adapter{database};
    FileDBAdapter database_adapter;
    UI ui;
    static inline string store_path;
};

// ...

int main(int argc, char **argv)
{
    vector<string> args(argv+1, argv+argc));
    try {
        App::setStorageFile(args[0]);
//        App app;
        FileDB database;
        database.setStore(App::getStorageFile());
        App app{database};
        app.launch();
    }

This allows us to move the database_adapter up to main() as well:

class App {
public:
//     App(FileDB &database)
//     : database_adapter(database),
//     ui(database_adapter)
    App(FileDBAdapter &database_adapter)
    : ui(database_adapter)
    {
    } 
// ...

private:
//    FileDBAdapter database_adapter;
    UI ui;
    static inline string store_path;
};

// ...

int main(int argc, char **argv)
{
    vector<string> args(argv+1, argv+argc));
    try {
        App::setStorageFile(args[0]);
        FileDB database;
        database.setStore(App::getStorageFile());
//         App app{database};
        FileDBAdapter database_adapter(database);
        App app{database_adapter};
        app.launch();

And we generalize the App constructor:

class App {
public:
//    App(FileDBAdapter &database_adapter)
//    : ui(database_adapter)
    App(Database &database)
    : ui(database) {
}

The code is now looking much like we wanted.  We have a Database abstraction and that is being used in as many places as possible, but it looks like we may have some unnecessary code in main(). The calls to setStorageFile() and getStorageFile() now appear redundant:

int main(int argc, char **argv)
{
    vector<string> args(argv+1, argv+argc));
    try {
//         App::setStorageFile(args[0]);
        FileDB database;
        App::setStorageFile(args[0]);
//        database.setStore(App::getStorageFile());
        database.setStore(args[0]);
        FileDBAdapter database_adapter(database);
        App app{database_adapter};

There are no remaining calls to App::getStorageFile() , which means App::store_path is no longer needed, which means setStorageFile() no longer does anything, and we can remove all of this:

class App {
public:
    // ...

//    static const string& getStorageFile()
//    {
//        return store_path;
//    }
//    static void setStorageFile(const string &store_path)
//    {
//        App::store_path = store_path;
//    }

private:
    UI ui;
//    static inline string store_path;
};

// ...

int main(int argc, char **argv)
{
    vector<string> args(argv+1, argv+argc));
    try {
        FileDB database;
//        App::setStorageFile(args[0]);
        database.setStore(args[0]);
        FileDBAdapter database_adapter(database);
        App app{database_adapter};

At this point, we can say that we’ve achieved our objective of abstracting the database and it’s made a positive impact on the structure of our application.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin