Disclaimer: Code is not public (for academic integrity purposes) but can be provided upon request.
For this project, I created a vim-like text editor. All of the following commands, including a b cc c[any motion] dd d[any motion] f h i j k l n o p q r s u w x yy y[any motion] A F I J N O P R S X ^ $ 0 . ; / ? % @ ^b ^d ^f ^g ^u :w :q :wq :q! :r :0 :line-number are supported with multipliers. To run the executable, the user run make and then run ./vm [filename]. From there, the ncurses user interface will open and the user will be able to read and write to the file. Now we will have a brief overview of what each class is responsible for. VM is a wrapper class that starts the Model View Controller cycle when it is instantiated. All the client has to do to start using the VM in their own program is to instantiate a VM object. To set up the process, the VM, which is essentially a wrapper, instantiates a VmModel and an NCursesView. VmModel loads the file into a text buffer in the form a list of strings, and creates a File object. This File object will be our representation of the file, which stores the text buffer, cursor position, and makes changes to the text buffer. NCursesKeyboard starts listening for keyboard input using the ncurses getch function, and notifies the VmModel if there was input. VmModel then uses one of its 4 Modes to parse the input from NCursesKeyboard and this creates an Action. From there, VmModel calls the execute function of the generated Action. This in turn will call File’s methods to modify the text buffer. Once the Action is complete, VmModel notifies NCursesView. NCursesView, which is responsible for displaying the screen buffer and other information to the screen, redraws the text buffer to the screen. Then NCursesView calls NCursesKeyboard’s listen function, which restarts the loop. Updated UML
The project uses MVC architecture, the observer pattern, the decorator pattern, the factory method pattern, and SOLID principles to solve various challenges.
The MVC architecture is used as there is a Model base class, a View base class, and a Controller base class. Using MVC architecture in this project is beneficial because of the separation of concerns: the 3 classes deal with different tasks, breaking down the problem and making it more approachable. On top of this, splitting the task into MVC makes it easier to test and makes the code more modular and flexible. Each of the MVC base classes are templates with 3 parameters, ModelStateType, ViewStateType, and ControllerStatetype. That is, the Model class is a template with the parameters ModelStateType, ViewStateType, and ConterollerStatetype, and so is the View class and Controller class. By making these 3 classes templates, derived classes can choose which state type they want to pass around to each other, so these 3 template classes are highly reusable. As a result, the classes could be even be reused for other non-related projects that may need MVC architecture. The VMModel, NCursesView, and NCursesKeyboard classes inherit from the Model, View, and Controller classes, respectively, and can take advantage of these properties.
The Observer pattern is also used in this project. The Model observes the Controller for input, the View observes the model for updates to the state, and the Model observes the View for updates to the view. In the derived classes, the VMModel observes the NCursesKeyboard for input in the form of characters one by one, the NCursesView observes the VMModel for updates to the text buffer and cursor position, and the VMModel observes the NCursesView for updates to screen height (Screen height must be observed in the case that the user uses a command that scrolls based on window height). Since the Model observes the View and the View observes the Model, there’s a circular dependency and so a forward declaration was used. The benefit of using the observer pattern is that we have loose coupling between the subject and the observer, which helps make my entire MVC structure have low coupling. Since the observers aren’t directly dependent on the subjects, they just depend on the subject’s interface. Using the observer pattern also uses dependency inversion, one of the SOLID principles.
The decorator pattern is used in the Window drawing. The statusbar, editor (and highlighting editor) are both window decorators, and there is a concrete window. This means that at the base we must always have the concrete window and the window decorators can be added on. This is useful because the concrete window must call initscr and set up all the parameters, and the decorator windows can actually draw onto the screen. Also, we can combine decorators and “mix and match” to customize what we want on the screen. This is very useful, since each window decorator has one responsibility (Single responsibility principle) and that is to print one thing onto the screen. Also, we can easily add in new types of window decorators (Open-closed principle), if we wanted to add widgets to the window for example.
Next, we discuss the factory method pattern. A challenge in this problem was parsing the input and deriving actions from it to execute on the text buffer. On top of this, there are different modes that the VM has to take on, which all must handle input differently. To deal with this issue, I made use of the factory method pattern. One of the 4 modes (InsertMode, ReplaceMode, CommandMode, and CommandLineMode) is used to parse the input and return an Action that corresponds to the input. To do this, the Mode class defines an interface for creating an Action, but leaves the choice of the Action type to the 4 modes. This is effective because each derived class of Action has a different doAction method to act on the text buffer or cursor differently. The Factory method is beneficial here because it’s flexible in terms of choosing which type of Action to instantiate, it’s easy to extend and add new modes and actions, and it makes maintenance easier.
Now I will discuss some changes in plan that I had since the first due date.
Firstly, to handle the undo command, I initially wanted to create an abstract Change class with an Add, Remove, and Replace subclass which would each be used to undo. Then to undo, I would pop a Change off the stack and call its undo method. However, when I was coding, I decided that to handle the undo command, it would be much more effective to add the text Buffer to a stack whenever the user makes a change. When the user calls undo, I would simply set the text buffer to the buffer at the top of the stack and pop it off.
Another change I decided to make was to remove the intermediate abstract Action classes like TextAction, CursorAction, FileAction, CommandAction, and MacroAction. I realized that having these intermediate abstract classes would not be helpful because they don’t have any extra methods or fields that would be useful. So in the end, I ended up making every Action class inherit directly from the Action class.
I also decided to make the Model, View, and Controller template classes have 3 parameters each instead of 1. While I was working on the program, I realized that if a template class only had 1 parameter for its own State type, it would have to fix the type of the other classes. This is because the classes must have pointers to the other classes if they’re being observed, and so the argument to the other class’ template have to be decided in the current class. When each of the 3 template class have 3 parameters (with one parameter for each class’ state type), the client can have complete control over the state types that each class will pass around. This leads to code that is much more reusable and effective.
I handled all memory management in this project using smart pointers and did not use delete statements at all. I would argue that this approach is easier than managing memory myself because I didn’t have to write destructors. I learned how to transfer ownership of an object from one unique pointer to another using std::move, which was a challenge. This was especially a new concept when I was using smart pointers for the decorator pattern in the Window, but it was a useful exercise to learn more about unique pointers. Overall, using smart pointers was a great challenge and I would actually prefer to use them over managing memory myself in future C++ projects, because they greatly helps in avoiding memory leaks.
I also implemented macros in this project. This was not very challenging, as I used a map to map characters to strings, where the character register corresponds to a string (the sequence of characters that make up the macro).
I also made the model observe the view for changes to the screen height. The model needs the screen height for commands that move the cursor based on the screen height. This was a challenge because it introduced a circular dependency in my MVC architecture, where my Model depended on the View and the View depended on the model. To overcome this, I used pointers and forward declarations to ensure that the program would compile.
If I had the chance to restart the project, I would have started earlier. Towards the deadline, I found myself running out of time so the project was quite rushed. Also at the beginning, I was not aware that the window would not be resized during runtime, so the Model wouldn’t have to observe the View. As a result, I made the Model observe the view, creating a circular dependency. Instead, I could’ve fetched the screen height from the view with my wrapper class and fed the screen height to the model at the beginning of the program. This would’ve made the architecture more simple.