Chapter 6. Separation of concerns in practice

This chapter covers

  • Developing an application with separate high-level concerns
  • Using specific types of encapsulation to loosen the coupling of different concerns
  • Creating a well-separated foundation to enable future extension

In chapter 2, I showed you some of the best practices around separation of concerns in Python. Separating concerns means creating boundaries between code that deals with distinct activities to make the code more understandable. You learned how functions, classes, modules, and packages are useful in decomposing code into pieces that are easier to reason about. Although chapter 2 covered several of the tools available for separating concerns, it’s helpful to get some experience applying them.

As is true for many, I learn best by doing. As I work through a real project, I often discover connections I didn’t see before or find new questions to explore. In this chapter, you’ll work through a real application that exhibits a good use case for separating concerns. You’ll improve upon it in the chapters to come, and you’ll end up with something you can extend for your own personal use.

Note

This and future chapters make light use of structured query language (SQL), a domain-specific language for manipulating and retrieving data from databases. If you haven’t used SQL before, or need a refresher, you might want to run through a tutorial before continuing. Ben Brumm’s SQL in Motion course (www.manning.com/livevideo/sql-in-motion) is a good primer.

6.1. A command-line bookmarking application

In this chapter, you’ll develop an application for saving and organizing bookmarks (more specifics on that in a minute).

I’m not a great notetaker. Throughout school and my career, I’ve struggled to find a way of writing things down for myself that helps me learn and retain information. When I find a great resource that goes through a concept in a novel way or with insightful examples, I’ve struck gold, but I usually need to dedicate time to read and practice the information in that resource. As a result, I’ve amassed a great number of bookmarks over the last few years. Maybe I’ll have the time to read through them someday!

The default bookmark feature in most browsers is lacking. Although things can be nested in folders and given a title, it’s often pretty difficult to recall why you saved something in the first place. A bunch of my bookmarks are code-related articles about testing, performance, new programming languages, and the like. When I find an interesting repository on GitHub, I also use GitHub’s “star” feature to save it for later. But GitHub stars are limited too; at the time of writing, they’re one big flat list that you can filter only by programming language. Whatever bookmark implementations you might use, they’re mostly built on the same foundational principles.

Bookmarks are an example of a small CRUD workflow: create, read, update, and delete (figure 6.1). These four operations make up a lot of data-driven tools in the world. You can create a bookmark to save for later, and then read its information to get the URL. You may want to update a bookmark’s title if the one you gave it originally was confusing, and you usually delete them when you’re done with them. This is a pretty good place to start your application.

Because a long description is one of the features missing from some existing bookmark tools, your application will include that off the bat. You’ll add a few more features in the following chapters, and do so in a way that will enable you to keep adding features you want.

Figure 6.1. CRUD operations are the basis of many applications that manage user data.

6.2. A tour of Bark

You’re going to develop Bark, a command-line bookmarking application. Bark allows you to create bookmarks that, for now, will be made up of a few pieces of information:

  • ID—A unique, numerical identifier for each bookmark
  • Title—A short text title for the bookmark, like “GitHub”
  • URL—A link to the article or website being saved
  • Notes—An optional, long description or explanation about the bookmark
  • Date added—A timestamp so you can see how old the bookmark is (in a bid to stave off that pesky procrastination)

Bark will also let you list all bookmarks that have been added and then delete a specific bookmark by its ID. This is all managed through a command-line interface (CLI)—an application you interact with in your terminal. On startup, Bark’s CLI will present you with a menu of options. Each option, when selected, will trigger an action that will read or modify a database.

Note

You won’t develop a feature to cover the update portion of CRUD for bookmarking in this chapter; you’ll get to that in chapter 7.

6.2.1. The benefits of separation: Reprise

Even though the CRUD-like operations Bark supports are fairly common for this kind of application, there’s a sizable amount of stuff happening. For an application this big, it’s important to remember what benefits separation of concerns will offer:

  • Reduced duplication—If each piece of your software does one thing, it will be easier to see when two of them do the same thing. You can analyze similar pieces of code to see if it makes sense to combine them into a single source of truth for that behavior.
  • Improved maintainability—Code is read much more often than it’s written. Code that can be understood incrementally because each piece has a clear responsibility allows developers to jump into areas of interest, understand what they need, and jump back out.
  • Ease of generalization and extension—Code with one responsibility can be generalized to cover that responsibility for a number of use cases, or it can be broken up further to support more varied behavior. Code that does numerous things will have a hard time supporting such flexibility because it’s hard to see where changes may have an effect.

Keep these ideas in mind as you work through the exercise in this chapter. My goal is for you to come out of this chapter with something you can continue developing and adding features to. To do this, you’ll first think about and then implement a high-level architecture that will support that outcome.

6.3. An initial code structure, by concern

I try to start developing applications like Bark with a concise explanation of how it does what it does. This tends to lead me toward an initial architecture.

For example, how does Bark work? What is its concise description? Perhaps the following statement comprises the answers to these questions: Using a command-line interface, a user chooses options for adding, removing, and listing bookmarks stored in a database.

Now let’s break that down a bit:

  • Command-line interface—This is a way to present options to a user and understand which options they choose.
  • Choosing options—Once an option is chosen, some action or business logic happens as a result.
  • Stored in a database—Data needs to be persisted for later use.

These points represent the high-level layers of abstraction for Bark. The CLI is the presentation layer of the application. The database is the persistence layer. The actions and business logic are kind of like the glue that connects the presentation and persistence layers. Each is a fairly separate concern, as shown in figure 6.2. This kind of multitier architecture, where each layer (tier) of an application has freedom to evolve, is used by many organizations. Teams can assemble around each tier based on areas of expertise, and each layer can potentially be reused with other applications if desired.

Figure 6.2. CRUD operations are the basis of many applications that manage user data.

You’ll develop each of these layers of Bark as you work through the chapter. Because each is a separate concern, it makes sense to think of them as separate Python modules:

  • A database module
  • A commands module
  • A bark module, which contains the code that actually runs the Bark application

We’ll start from the persistence layer and work our way up.

Application architecture patterns

Separating applications into layers of presentation, persistence, and actions or rules is a common pattern. Some variations on this approach are so common, they’ve been given names. Model-view-controller (MVC) is a way of modeling data for persistence, providing users with a view into that data, and allowing them to control changes to that data with some set of actions. Model-view-viewmodel (MVVM) puts an emphasis on allowing the view and data model to communicate freely. These and other multitier architectures are great examples of separation of concerns.

6.3.1. The persistence layer

The persistence layer is the lowest level of Bark (figure 6.3). This layer will be concerned with taking information it receives and communicating it to the database.

Figure 6.3. The persistence layer deals with data storage—it’s the lowest level of the application.

You’ll be using SQLite, a portable database that stores data in a single file by default (www.sqlite.org/index.html). This is convenient, compared to more complex database systems, because you can start from scratch by deleting the file if something goes wrong.

Note

Despite being one of the most widely used databases, SQLite is installed on only some operating systems by default. I recommend downloading a precompiled binary for your operating system from the official download page (https://sqlite.org/download.html).

Starting in the database module, you’ll create a Database-Manager class for manipulating data in the database. Python provides a built-in sqlite3 module, which you can use to get a connection to the database, make queries, and iterate over results. SQLite databases are usually a single file with a .db extension; if you make a sqlite3 connection to a file that doesn’t exist, the module will create it for you.

The database module provides most of what you need to manage bookmark data, including the following:

  • Creating a table (for initializing the database)
  • Adding or deleting a record
  • Listing the records in a table
  • Selecting and sorting records from a table based on some criteria
  • Counting the number of records in a table

How can these tasks be broken down further? Each seems somewhat separate, from the business logic perspective described earlier, but what about at the persistence layer? Most of the activities described can be achieved by constructing an appropriate SQL statement and executing it. Executing it requires a connection to the database, which requires the path to the database file.

Whereas managing the persistence is a high-level concern, these individual concerns are what you get when you peel open the persistence layer. They should each be separate as well. First things first, though—you need a connection to the database.

Working with databases

Many smart people have produced wonderful and robust packages that make working with databases in Python easier. SQLAlchemy (www.sqlalchemy.org) is a widely used tool for not only interacting with databases, but abstracting data models via an object-relational mapping (ORM). An ORM allows you to treat database records as objects in languages like Python, without worrying much about the details of a database at all. The Django web framework also provides an ORM for writing data models.

In the spirit of learning by doing, you’ll write the database interaction code yourself in this chapter. It’s limited to the scope of Bark, but it can be added to or replaced if you’d like to do more with the rest of the application. If you need to use a database in future projects, consider whether you want to write your database code from scratch or use one of these third-party packages instead.

CREATING AND CLOSING THE DATABASE CONNECTION

While Bark is running, it needs only one connection to the database—it can reuse this connection for all its operations. To make this connection, you can use sqlite3 .connect, which accepts the path of the database file to which it should connect. Again, if the file does not exist, it will be created.

The __init__ for DatabaseManager should

  1. Accept an argument containing the path to the database file (Don’t hardcode it; separate your concerns!)
  2. Use the database file path to create a SQLite connection using sqlite3 .connect(path) and store it as an instance attribute

It’s good practice to close the connection to the SQLite database when the program finishes, to limit the possibility of data corruption. For symmetry, the __del__ for DatabaseManager should close the connection with the connection’s .close() method.

This will serve as the foundation for executing statements.

import sqlite3


class DatabaseManager:
    def __init__(self, database_filename):
        self.connection = sqlite3.connect(database_filename)  1

    def __del__(self):
        self.connection.close()                               2

  • 1 Creates and stores a connection to the database for later use
  • 2 Cleans up the connection when done, to be safe
EXECUTING STATEMENTS

Your DatabaseManager will need a way to execute statements. These statements have a couple of things in common, so encapsulating those aspects into a reusable method will reduce the likelihood of errors from rewriting the same code each time you want to execute a new kind of statement.

Some SQL statements return data; these statements are called queries. Sqlite3 manages query results with a concept called a cursor. Using a cursor to execute a statement lets you iterate over the results it returns. Statements that aren’t queries (INSERT, DELETE, and so on) don’t return any results, but the cursor manages this by returning an empty list.

Write an _execute method on DatabaseManager that you can use to execute all statements using a cursor, returning a result that you can choose to use where you need to. The _execute method should

  1. Accept a statement as a string argument
  2. Get a cursor from the database connection
  3. Execute a statement using the cursor (more on this shortly)
  4. Return the cursor, which has stored the result of the executed statement (if any)
def _execute(self, statement):
    cursor = self.connection.cursor()  1
    cursor.execute(statement)          2
    return cursor                      3

  • 1 Creates the cursor
  • 2 Uses the cursor to execute the SQL statement
  • 3 Returns the cursor, which has stored the results

Statements that aren’t queries usually manipulate data, and if anything bad happens while they’re executing, the data could become corrupted. Databases guard against this with a feature called a transaction. If a statement executing within a transaction fails or is otherwise interrupted, the database will roll back to its last known working state. Sqlite3 lets you use the connection object to create a transaction via a context manager, a Python block using the with keyword that provides some special behavior when the code enters and exits the block.

Update _execute to put the cursor creation, execution, and return inside a transaction, like the following:

def _execute(self, statement):
    with self.connection:                   1
        cursor = self.connection.cursor()
        cursor.execute(statement)           2
        return cursor

  • 1 This creates a database transaction context.
  • 2 This happens within the database transaction.

Using .execute inside a transaction will get you where you need to go, functionally speaking. But it’s a good security practice to use placeholders for real values in SQL statements to prevent users from doing malicious things with specially crafted queries.[1] Update _execute to accept two things:

1

See the Wikipedia article on SQL injection: https://en.wikipedia.org/wiki/SQL_injection.

  • A SQL statement as a string, possibly containing placeholders
  • A list of values to fill in the placeholders in the statement

The method should then execute the statement by passing both arguments to the cursor’s execute, which accepts the same arguments. It should look something like the following snippet:

def _execute(self, statement, values=None):           1
        with self.connection:
            cursor = self.connection.cursor()
            cursor.execute(statement, values or [])   2
            return cursor

  • 1 values is optional; some statements don’t have placeholders to fill in.
  • 2 Executes the statement, providing any passed-in values to the placeholders

Now you have a database connection and the ability to execute arbitrary statements on that connection. Remember that the connection is managed for you automatically when you create a DatabaseManager instance, so you don’t need to think about how it’s opened and closed, unless you want to change it. Now, statement execution is managed within the _execute method, so you also don’t need to think about how a statement is executed; you only need to tell it what statement to execute. This is the power of separating your concerns.

Now that you’ve got these building blocks, it’s time to develop some database interactions.

CREATING TABLES

One of the first things you’ll need is a database table in which to store your bookmark data. You’ll have to create this table using a SQL statement. Because the concerns of connecting to the database and executing statements are now abstracted, the work of creating a table includes the following:

  1. Determine the column names for the table.
  2. Determine the data type of each column.
  3. Construct the right SQL statement to create a table with those columns.

Remember that each bookmark has an ID, title, URL, optional notes, and the date it was added. The data type and constraints for each column follow:

  • ID—The ID is the primary key of the table, or the main identifier of each record. It should automatically increment each time a new record is added, using the AUTOINCREMENT keyword. This column is an INTEGER type; the rest are TEXT.
  • Title—The title is required because it’s hard to skim your existing bookmarks if they’re only URLs. You can tell SQLite the column can’t be empty by using the NOT NULL keyword.
  • URL—The URL is required, so it gets NOT NULL as well.
  • Notes—Notes for a bookmark are optional, so only the TEXT specifier is necessary.
  • Date—The date the bookmark was added is required, so it gets NOT NULL.

A table creation statement in SQLite uses the CREATE TABLE keywords, followed by the table name, the list of columns, and their data type information in parentheses. Because you’ll want Bark to create the table on startup if it doesn’t already exist, you can use CREATE TABLE IF NOT EXISTS.

Based on the previous descriptions of the bookmark columns, what would the SQL statement look like for creating a bookmarks table? See if you can write it out, then come back to check your work against the following listing.

Listing 6.1. The creation statement for a bookmarks table
CREATE TABLE IF NOT EXISTS bookmarks
(
    id INTEGER PRIMARY KEY AUTOINCREMENT,  1
    title TEXT NOT NULL,                   2
    url TEXT NOT NULL,
    notes TEXT,
    date_added TEXT NOT NULL
);

  • 1 The main ID of each record, which increments automatically as records are added
  • 2 NOT NULL requires a column to be populated with a value.

You can now write your method for creating tables. Each column is identified by a name, like title, that maps to a data type and constraints, like TEXT NOT NULL, so a dictionary seems like an appropriate Python type for representing columns. The method needs to

  1. Accept two arguments: the name of the table to create, and a dictionary of column names mapped to their data types and constraints
  2. Construct a CREATE TABLE SQL statement like the one shown earlier
  3. Execute the statement using DatabaseManager._execute

Try writing the create_table method now, and then check back to see how it compares to the following listing.

Listing 6.2. Creating a SQLite table
def create_table(self, table_name, columns):
        columns_with_types = [                     1
            f'{column_name} {data_type}'
            for column_name, data_type in columns.items()
        ]
        self._execute(                             2
            f'''
            CREATE TABLE IF NOT EXISTS {table_name}
            ({', '.join(columns_with_types)});
            '''
        )

  • 1 Constructs the column definitions, with their data types and constraints
  • 2 Constructs the full create table statement and executes it
A note on generalization

Right now, you need only the bookmarks table for Bark. I’ve already argued in this book that early optimization is a no-no, and the same is true for generalization. So why make a general-use create_table method?

When I start building a method with hardcoded values, I check to see if it’s much work to parameterize those values with arguments to the method. For example, replacing the string 'bookmarks' with a table_name string argument isn’t much work. The columns and their data types follow similarly. Using this approach, the create_table method can be made general enough to create most any table you’ll need.

You’ll use this method later on to create a bookmarks table, which is what Bark will interact with to manage bookmarks as you develop the application.

ADDING RECORDS

Now that you can create a table, you need to be able to add bookmark records to it. This is the “C” in CRUD (figure 6.4).

SQLite expects the INSERT INTO keyword, followed by the table name, to indicate the intent to add a new record to the table. This is followed by the list of columns you’re supplying values for in parentheses, the VALUES keyword, and then the values you’re supplying in parentheses. A record insert statement in SQLite looks like this:

INSERT INTO bookmarks
(title, url, notes, date_added)
VALUES ('GitHub', 'https://github.com',
 'A place to store repositories of code', '2019-02-01T18:46:32.125467');
Figure 6.4. Creation is the most basic operation necessary for CRUD, so it’s the crux of many systems.

Remember that it’s a good practice to use placeholders instead, as in the _execute method earlier. What parts of the preceding query should use placeholders?

  1. bookmarks
  2. title, url, and so on
  3. 'GitHub', 'https://github.com', and so on
  4. All of the above

Only places where literal values go can use placeholders in statements, so 3 is the correct answer. An INSERT statement for the bookmarks table, with placeholders, looks like this:

INSERT INTO bookmarks
(title, url, notes, date_added)
VALUES (?, ?, ?, ?);

To construct this statement, you’ll need to write an add method in DatabaseManager that

  1. Accepts two arguments: the name of the table, and a dictionary that maps column names to column values
  2. Constructs a placeholder string (a ? for each column specified)
  3. Constructs the string of the column names
  4. Gets the column values as a tuple (A dictionary’s .values() returns a dict_ values object, which happens not to work with sqlite3’s execute method.)
  5. Executes the statement with _execute, passing the SQL statement with placeholders and the column values as separate arguments

Write the add method now, and check back with the following listing to see how it compares.

Listing 6.3. Adding a record to a SQLite table
def add(self, table_name, data):
        placeholders = ', '.join('?' * len(data))
        column_names = ', '.join(data.keys())        1
        column_values = tuple(data.values())         2

        self._execute(
            f'''
            INSERT INTO {table_name}
            ({column_names})
            VALUES ({placeholders});
            ''',
            column_values,                           3
        )

  • 1 The keys of the data are the names of the columns.
  • 2 .values() returns a dict_values object, but execute needs a list or tuple.
  • 3 Passes the optional values argument to _execute
USING CLAUSES TO LIMIT ACTION SCOPE

To insert records into a database, all you need is the info to be inserted, but some database statements are used in tandem with one or more additional clauses. Clauses affect which records the statement will operate on. Using a DELETE statement without a clause, for example, could end up deleting all the records in the table. You don’t want that.

WHERE clauses can be appended to several kinds of statements to limit the statement’s effect to records matching that criteria. You can combine multiple WHERE criteria using AND or OR. In Bark, for example, each bookmark record has an ID, so you can limit a statement to acting on a particular record by its ID with a clause like WHERE id = 3.

This kind of limiting is useful both for queries (to search for specific records) and for regular statements. Clauses will be useful when you need to delete specific records.

DELETING RECORDS

After a bookmark has outlived its usefulness, you need a way to delete it (figure 6.5). To delete a bookmark, you can issue a DELETE statement to the database, using a WHERE clause to specify a bookmark by its ID.

Figure 6.5. Delete is the counterpart of create, so most systems cover this operation as well.

In SQLite, the statement to delete the bookmark with an ID of 3 looks like this:

DELETE FROM bookmarks
WHERE ID = 3;

As in the create_table and add methods, you can represent the criteria as a dictionary that maps column names to the values you want to match. Write a delete method that

  1. Accepts two arguments: the table name to delete records from, and a dictionary mapping column names to the value to match on. The criteria should be a required argument, because you don’t want to delete all your records.
  2. Constructs a string of placeholders for the WHERE clause.
  3. Constructs the full DELETE FROM query and executes it with _execute.

Check your results against the following listing.

Listing 6.4. Deleting records in SQLite
def delete(self, table_name, criteria):                                 1
        placeholders = [f'{column} = ?' for column in criteria.keys()]
        delete_criteria = ' AND '.join(placeholders)
        self._execute(
            f'''
            DELETE FROM {table_name}
            WHERE {delete_criteria};
            ''',
            tuple(criteria.values()),                                   2
        )

  • 1 The criteria argument isn’t optional here; all records would be deleted without any criteria.
  • 2 Uses the values argument of _execute as the values to match against
SELECTING AND SORTING RECORDS

You can add and remove records from a table now, but how can you retrieve them? Aside from creating and deleting information, you’ll want to be able to read what you’ve already stored (figure 6.6).

Figure 6.6. Reading existing data is usually a necessary part of a CRUD application.

You can create a query statement in SQLite using SELECT * FROM bookmarks (the * means “all columns”) and some criteria:

SELECT * FROM bookmarks
WHERE ID = 3;

Additionally, you can sort these results by a specific column using an ORDER BY clause:

SELECT * FROM bookmarks
WHERE ID = 3
ORDER BY title;           1

  • 1 This orders the results by the title column in ascending order.

Again, you should use placeholders where there are literal values in the query:

SELECT * FROM bookmarks
WHERE ID = ?
ORDER BY title;

Your select method will look somewhat similar to the delete method, except that criteria can be optional. (It will fetch all records by default.) It should also accept an optional order_by argument that specifies a column to sort the results by (the default is the primary key of the table). Using delete as a guide, write select now and come back to compare with the following listing when you’re done.

Listing 6.5. A method for selecting SQL table data
def select(self, table_name, criteria=None, order_by=None):
        criteria = criteria or {}                               1

        query = f'SELECT * FROM {table_name}'
        if criteria:                                            2
            placeholders = [f'{column} = ?' for column in criteria.keys()]
            select_criteria = ' AND '.join(placeholders)
            query += f' WHERE {select_criteria}'

        if order_by:                                            3
            query += f' ORDER BY {order_by}'

        return self._execute(                                   4
            query,
            tuple(criteria.values()),
        )

  • 1 Criteria can be empty by default, because selecting all records in the table is all right.
  • 2 Constructs the WHERE clause to limit the results
  • 3 Constructs the ORDER BY clause to sort the results
  • 4 This time, you want the return value from _execute to iterate over the results.

You’ve now created a database connection; written an _execute method for executing arbitrary SQL statements with placeholders in a transaction; and written methods to add, query, and delete records. This is about all you’ll need for manipulating a SQLite database for the moment. You just finished a database manager in fewer than 100 lines of code. Nice work.

Next, you’ll develop the business logic that interacts with the persistence layer.

6.3.2. The business logic layer

Now that the persistence layer for Bark is in place, you can work on the layer that figures out what to put in and get out of the persistence layer (figure 6.7).

Figure 6.7. The business logic layer determines when and how data is read from or written to the persistence layer.

When a user interacts with something in the presentation layer of Bark, Bark needs to trigger something to happen in the business logic and ultimately in the persistence layer. It might be tempting to do something like the following:

if user_input == 'add bookmark'
    # add bookmark
elif user_input == 'delete bookmark #4':
    # delete bookmark

But this would couple the text presented to the user with the actions that need to be triggered. You would have new conditions for each menu option, and if you wanted multiple options to trigger the same command, or you wanted to change the text, you would have to refactor some code. It would be nice if the presentation layer were the only place that knows about the menu option text displayed to the user.

Each action is kind of like a command that needs to be executed in response to a user’s menu choice. By encapsulating the logic of each action as a command object, and providing a consistent way to trigger them via an execute method, these actions can be decoupled from the presentation layer. The presentation layer can then point menu options to commands without worrying about how those commands work. This is called the command pattern.[2]

2

See Wikipedia’s “Command pattern” article for more on this pattern: https://en.wikipedia.org/wiki/Command _pattern.

You’ll develop each of the CRUD actions and some peripheral functionality as commands in the business logic layer.

CREATING THE BOOKMARKS TABLE

Now that you’re working in the business logic layer, create a new “commands” module to house all the commands you’re going to write. Because most of the commands will need to make use of the DatabaseManager, import it from the database module and create an instance of it (called db) to be used throughout the commands module. Remember that its __init__ method requires the file path to a SQLite database; I suggest calling it bookmarks.db. Leaving out any leading path will create the database file in the same directory as the Bark code.

Because you’ll need to initialize the bookmarks database table if it doesn’t already exist, start by writing a CreateBookmarksTableCommand class whose execute method creates the table for your bookmarks. You can make use of the db.create_table method you wrote earlier to create your bookmarks table. Later in the chapter, you’ll trigger this command to run when Bark starts up. Check your work against the following listing.

Listing 6.6. A command for creating a table
db = DatabaseManager('bookmarks.db')                   1


class CreateBookmarksTableCommand:
    def execute(self):                                 2
        db.create_table('bookmarks', {                 3
            'id': 'integer primary key autoincrement',
            'title': 'text not null',
            'url': 'text not null',
            'notes': 'text',
            'date_added': 'text not null',
        })

  • 1 Remember, sqlite3 will automatically create this database file if it doesn’t exist.
  • 2 This will eventually be called when Bark starts up.
  • 3 Creates the bookmarks table with the necessary columns and constraints

Notice that the command is only aware of its duties (calling persistence layer logic) and the interface of its dependency (DatabaseManager.create_table). This is loose coupling, thanks in part to separating the persistence logic and (eventually) the presentation logic. You should be seeing the benefits of separation of concerns more and more clearly as you work through these exercises.

ADDING BOOKMARKS

To add a bookmark, you’ll need to pass data received from the presentation layer on to the persistence layer. The data will be passed as a dictionary mapping column names to values. This is a great example of code relying on a shared interface rather than the specifics of an implementation. If the persistence layer and the business logic layer agree on a data format, they can each do what they need to, as long as the data format stays consistent.

Write an AddBookmarkCommand class that will perform this operation. This class will

  1. Expect a dictionary containing the title, URL, and (optional) notes information for a bookmark.
  2. Add the current datetime to the dictionary as date_added. To get the current time in UTC, in a standardized format with wide compatibility, use datetime .datetime.utcnow().isoformat().[3]

    3

    See Wikipedia’s article on “ISO 8601” for more information on this time format: https://en.wikipedia.org/ wiki/ISO_8601.

  3. Insert the data into the bookmarks table using the DatabaseManager.add method.
  4. Return a success message that will eventually be displayed by the presentation layer.

Check your work against the following listing.

Listing 6.7. A command for adding a bookmark
from datetime import datetime

...
class AddBookmarkCommand:
    def execute(self, data):
        data['date_added'] = datetime.utcnow().isoformat()   1
        db.add('bookmarks', data)                            2
        return 'Bookmark added!'                             3

  • 1 Adds the current datetime as the record is added
  • 2 Using the DatabaseManager.add method makes short work of adding a record.
  • 3 You’ll use this message in the presentation layer later.

You’ve now written all the business logic needed for creating bookmarks. Next, you’ll want to be able to list the bookmarks you’ve added.

LISTING BOOKMARKS

Bark needs to be able to show you the bookmarks you’ve saved—without that, Bark wouldn’t be of much use. You’re going to write a ListBookmarksCommand that will provide the logic for displaying the bookmarks in the database.

You’ll want to make use of the DatabaseManager.select method to get the bookmarks from the database. By default, SQLite sorts records by their order of creation (that is, by the primary key of the table), but it might also be useful to sort bookmarks by date or title. In Bark, bookmarks’ IDs and dates sort identically because they both strictly increase as you add bookmarks, but it’s good practice to sort explicitly by the column of interest in case that changes.

ListBookmarksCommand should do the following:

  • Accept the column to order by, and save it as an instance attribute. You can set the default value to date_added if you like.
  • Pass this information along to db.select in its execute method.
  • Return the result (using the cursor’s .fetchall() method) because select is a query.

Write the command to list bookmarks, and come back to check your work against the following listing.

Listing 6.8. A command to list existing bookmarks
class ListBookmarksCommand:
    def __init__(self, order_by='date_added'):                            1
        self.order_by = order_by

    def execute(self):
        return db.select('bookmarks', order_by=self.order_by).fetchall()  2

  • 1 You can create a version of this command for sorting by date or by title.
  • 2 db.select returns a cursor you can iterate over to get the records.

Now you’ve got enough functionality to add bookmarks and view existing ones. The last step for managing bookmarks is a command for deleting them.

DELETING BOOKMARKS

Similar to adding a new bookmark, the deletion of a bookmark requires some data to be passed from the presentation layer. This time, though, the data is simply an integer value representing the ID of the bookmark to delete.

Write a DeleteBookmarkCommand command that accepts this information in its execute method and passes it to the DatabaseManager.delete method. Remember that delete accepts a dictionary mapping column names to values to match against; here, you’ll want to match the given value in the id column. Once the record is deleted, return a success message for use in the presentation layer.

Come back and check your work against the following listing.

Listing 6.9. A command to delete bookmarks
class DeleteBookmarkCommand
:
    def execute(self, data):
    db.delete('bookmarks', {'id': data})   1
    return 'Bookmark deleted!'

  • 1 delete accepts a dictionary of column name, match value pairs.
QUITTING BARK

There’s one piece of polish left: a command for exiting Bark. A user could use the usual Ctrl-C method of stopping the Python program, but an option to exit is a little nicer.

Python provides the sys.exit function for stopping program execution. Write a QuitCommand whose execute method exits the program using this approach, then come back and check your work against the following listing.

Listing 6.10. A command to exit the program
import sys

...

class QuitCommand
:
    def execute(self):
        sys.exit()        1

  • 1 This should immediately exit Bark.

Now you can wipe the sweat from your brow . . . not because you’re done, but because you’ll be developing the presentation layer next.

6.3.3. The presentation layer

Bark uses a command-line interface (CLI). Its presentation layer (the part the user sees, as shown in figure 6.8) is text in a terminal. Depending on the application, a CLI can run until the completion of a specific task, or it can keep running until the user explicitly exits. Because you wrote QuitCommand, you might guess that you’ll be doing the latter.

The presentation layer of Bark contains an infinite loop:

  1. Clears the screen
  2. Prints the menu options
  3. Gets the user’s choice
  4. Clears the screen and executes the command corresponding to the user’s choice
  5. Waits for the user to review the result, pressing Enter when they’re done
Figure 6.8. The presentation layer shows users what actions can be taken and a way to trigger them.

Now that you’re working on the presentation layer, you’ll need to create a new bark module. It’s a good practice to put code for command-line applications into an if name == 'main': block; this will make sure you don’t unintentionally execute the code in the module by importing the bark module somewhere. If you start with a Hello, World! type of program, you can do a quick check to make sure things are set up properly.

Start with the following in your bark module:

if __name__ == '__main__':
    print('Welcome to Bark!')

Try running python bark.py in your terminal; you should see Welcome to Bark! as a result. Now you can start hooking up the presentation layer to some business logic.

DATABASE INITIALIZATION

Remember that Bark needs to initialize the database, creating the bookmarks table if it doesn’t already exist. Import the commands module and update your code to execute the CreateBookmarksTableCommand, as shown in the following snippet. After making this update and running python bark.py, you won’t see any text output, but you should see that a bookmarks.db file is created.

import commands


if __name__ == '__main__':
    commands.CreateBookmarksTableCommand().execute()

It may seem small, but you’ve just accomplished something pretty remarkable. This represents a full pass through all the layers of your multitier architecture. The presentation layer (the act of running bark.py, so far) has triggered a command in the business logic, which, in turn, set up a table in the persistence layer fit for storing bookmarks. Each layer knows just enough about its surroundings to do its job; things are well separated and loosely coupled. You’ll experience this a few more times as you start adding menu options to Bark that trigger more commands.

MENU OPTIONS

When you start Bark, it should present you with a menu of options that looks something like this:

(A) Add a bookmark
(B) List bookmarks by date
(T) List bookmarks by title
(D) Delete a bookmark
(Q) Quit

Each option has a keyboard shortcut and a descriptive title. If you look carefully, each of these options corresponds to one of the commands you wrote earlier. Because you wrote the commands using the command pattern, each command can be triggered the same way as the others—using its execute method. Commands differ only in what setup and input they require, and then from the presentation layer’s perspective they do whatever they do.

Based on what you’ve learned about encapsulation, how would you go about hooking up the items in the presentation layer to the business logic they control?

  1. Use conditional logic to call the right Command class’s execute method based on the user input.
  2. Make a class that pairs the text to be displayed to the user and the command it triggers.

I recommend choice 2. To hook each menu option up to the command it should trigger, you can create an Option class. The class’s __init__ method can accept the name to display to the user in the menu, an instance of the command to execute when chosen by the user, and an optional preparation step (to get additional input from the user, for example). All of these can be stored as instance attributes.

When chosen, an Option instance needs to

  1. Run the specified preparation step, if any.
  2. Pass the return value from the preparation step, if any, to the specified command’s execute method.
  3. Print the result of the execution. These are the success messages or bookmark results returned from the business logic.

An Option instance should be represented as its text description when shown to the user; you can use __str__ to override the default behavior. Abstracting this work from the rest of the code that gets and validates user input allows you to keep your concerns separate.

Try writing the Option class, then check the following listing to see how you’ve done.

Listing 6.11. Connecting menu text to business logic commands
class Option:
    def __init__(self, name, command, prep_call=None):
        self.name = name                                     1
        self.command = command                               2
        self.prep_call = prep_call                           3


    def choose(self):                                        4
        data = self.prep_call() if self.prep_call else None  5
        message = self.command.execute(data) if data
else self.command.execute()                                6
        print(message)

    def __str__(self):                                       7
        return self.name

  • 1 The name displayed in the menu
  • 2 An instance of the command to execute
  • 3 The optional preparation step to call before executing the command
  • 4 choose will be called when the option is chosen by the user.
  • 5 Calls the preparation step if specified
  • 6 Executes the command, passing in the data from the preparation, if any
  • 7 Represents the option as its name instead of the default Python behavior

With the Option class in place, now is a good time to start hooking up more of the business logic you created earlier. Remember that you need to do a few things with each option:

  1. Print the keyboard key for the user to enter to choose the option.
  2. Print the option text.
  3. Check if the user’s input matches an option and, if so, choose it.

What Python data structure would work well to hold all your options?

  1. list
  2. set
  3. dict

Each keyboard key maps to a menu option, and you need to check the user’s input against the available options, so you need to keep those pairings stored somehow. Choice 3 is a good one because a dict can provide keyboard key and option pairs that you can also iterate over, with the dictionary’s .items() method, for printing the option text. I also recommend using collections.OrderedDict specifically, to ensure that your menu options will always be printed in the order you specify.

Add your options dictionary after CreateBookmarksTableCommand now, adding an item for each menu option. Once the dictionary is in place, create a print_options function that iterates over the options and prints them in the format you saw earlier:

(A) Add a bookmark
(B) List bookmarks by date
(T) List bookmarks by title
(D) Delete a bookmark
(Q) Quit

Check your work with the following listing.

Listing 6.12. Specifying and printing menu options
def print_options(options):
    for shortcut, option in options.items():
        print(f'({shortcut}) {option}')
    print()

...

if __name__ == '__main__':
    ...
    options = {
        'A': Option('Add a bookmark', commands.AddBookmarkCommand()),
        'B': Option('List bookmarks by date',
commands.ListBookmarksCommand()),
        'T': Option('List bookmarks by title',
commands.ListBookmarksCommand(order_by='title')),
        'D': Option('Delete a bookmark', commands.DeleteBookmarkCommand()),
        'Q': Option('Quit', commands.QuitCommand()),
    }
    print_options(options)

After you’ve added the menu options, running Bark should print all of the options you added. You can’t yet trigger them; for that, you’ll need to get some user input.

USER INPUT

With our overall goal of threading presentation to business logic to persistence, what remains to be added is a bit of interactivity with Bark users. The approach for getting the user’s desired option goes like this:

  1. Prompt the user to enter a choice, using Python’s built-in input function.
  2. If the user’s choice matches one of those listed, call that option’s choose method.
  3. Otherwise, repeat.

What approach might you use in Python to get this repeating behavior?

  1. A while loop
  2. A for loop
  3. A recursive function call

Because there isn’t a definitive end state for getting the user’s input (they might enter an invalid choice four billion times), a while loop (option 1) makes the most sense. While the user’s choice is invalid, keep prompting them. You can take it easy on them by accepting the upper- and lowercase versions of each option if you like.

Write a get_option_choice function, and use it after printing the options to get the user’s choice. Then call that option’s choose method. Try it out, then compare your work with the following listing.

Listing 6.13. Getting a user’s choice of menu option
def option_choice_is_valid(choice, options):                1
    return choice in options or choice.upper() in options


def get_option_choice(options):
    choice = input('Choose an option: ')                    2
    while not option_choice_is_valid(choice, options):      3
         print('Invalid choice')
        choice = input('Choose an option: ')
    return options[choice.upper()]                          4

if __name__ == '__main__':
    ...

    chosen_option = get_option_choice(options)
    chosen_option.choose()

  • 1 The choice is valid if the letter matches one of the keys in the options dictionary.
  • 2 Gets an initial choice from the user
  • 3 While the user’s choice is invalid, keep prompting them.
  • 4 Returns the matching option once they’ve made a valid choice

At this point, you can run Bark, and some of the commands, like listing bookmarks and quitting, will respond to your input. But a couple of options require some additional preparation, as I alluded to earlier. You need to supply a title, description, and so on to add a bookmark, and you need to specify the ID of a bookmark to delete it. Much like you got user input for the menu option to choose, you’ll need to prompt the user for this bookmark data.

Here’s another opportunity to encapsulate some behavior. For each piece of information you need, you should

  1. Prompt the user with a label—“Title” or “Description”, for example
  2. If the information is required and the user presses Enter without entering any info, keep prompting them

Write three functions—one to provide the repeating prompt behavior, and two that use it to get information for adding or deleting a bookmark. Then add each information-fetching function as the prep_call to the appropriate Option instance. Check your results against the following listing to see how you did, or if you get stuck.

Listing 6.14. Gathering bookmark information from the user
def get_user_input(label, required=True):                   1
    value = input(f'{label}: ') or None                     2
    while required and not value:                           3
        value = input(f'{label}: ') or None
    return value


def get_new_bookmark_data():                                4
    return {
        'title': get_user_input('Title'),
        'url': get_user_input('URL'),
        'notes': get_user_input('Notes', required=False),   5
    }


def get_bookmark_id_for_deletion():                         6
    return get_user_input('Enter a bookmark ID to delete')

if __name__ == '__main__':
    ...
    'A': Option('Add a bookmark', commands.AddBookmarkCommand(),
prep_call=get_new_bookmark_data),
    ...
    'D': Option('Delete a bookmark', commands.DeleteBookmarkCommand(),
prep_call=get_bookmark_id_for_deletion),

  • 1 A general function for prompting users for input
  • 2 Gets initial user input
  • 3 Continues prompting while the input is empty, if required
  • 4 Function to get the necessary data for adding a new bookmark
  • 5 The notes for a bookmark are optional, so don’t keep prompting.
  • 6 Gets the necessary information for deleting a bookmark

If all is well, you should now be able to run Bark and add, list, or delete bookmarks! Congratulations on a job well done.

Nerding out

We just covered a heck of a lot of stuff, but I want to point out something I find exciting. Because of the way you’ve built Bark, if you want to add new functionality, there’s a clear roadmap:

  1. Add any new database manipulation methods you may need to database.py.
  2. Add a command class that performs the business logic you need in commands.py.
  3. Hook up the new command to a new menu option in bark.py.

How cool is that? Separating concerns allows you to clearly see which areas of code you need to augment when adding new functionality.

Before finishing up this chapter, there are a couple of remaining pieces of polish to attend to.

CLEARING THE SCREEN

Clearing the screen just before printing the menu or executing a command will make it easier to see the current context the user is in. To clear the screen, you can defer to your operating system’s command-line program for clearing the terminal text. The command for clearing the screen is clear on many operating systems, but it’s cls on Windows. You can figure out if you’re on Windows by checking os.name—on Windows this is 'nt'. (Windows NT is to Windows 10 as macOS is to Mojave.)

Write a clear_screen function that makes the appropriate call using os.system, as in the following code:

import os


def clear_screen():
    clear = 'cls' if os.name == 'nt' else 'clear'
    os.system(clear)

Call this just before calling print_options, and just before calling the .choose() method of the user’s selected option:

if __name__ == '__main__':
    ...

    clear_screen()
    print_options(options)
    chosen_option = get_option_choice(options)
    clear_screen()
    chosen_option.choose()

This will be most helpful when the menu and command results get printed over and over again, which is the final piece of this puzzle.

APPLICATION LOOP

The last step is to run Bark in a loop so that users can perform several actions in a row. To do this, create a loop method and move everything but the database initialization from the if __name__ == '__main__' block into it. Back in the if __name__ == '__main__' block, call loop inside a while True: block. At the end of loop, add a line to pause and wait for the user to press Enter before proceeding.

def loop():                                           1
    # All the steps for showing/selecting options
    ...
    _ = input('Press ENTER to return to menu')        2

if __name__ == '__main__':
    commands.CreateBookmarksTableCommand().execute()

    while True:                                       3
        loop()

  • 1 Everything that happens for each menu > option > result loop goes here.
  • 2 Prompts the user to press Enter and reviews the result before proceeding (_ means “unused value”)
  • 3 Loops forever (until the user chooses the option corresponding to QuitCommand)

Now Bark will give the user a way to return to the menu after each interaction, and the menu gives them an option to exit. This covers all the bases. What do you think? I think it’s about time to start using Bark.

Summary

  • Separation of concerns is a tool for achieving more readable, maintainable code.
  • End-user applications are often separated into persistence, business logic, and presentation layers.
  • Separation of concerns works closely with encapsulation, abstraction, and loose coupling.
  • Applying effective separation of concerns allows you to add, change, and delete functionality without affecting the surrounding code.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.87.161