49 views
# Lecture 15 -- Review of Object Oriented Design ## Motivating Questions - How should the many components of a program with a user-interface be split across multiple classes? - How to we handle errors without terminating the entire program? ## Setup: A Simple Banking Application Consider the following essential code for a simple banking application. The application lets customers withdraw and deposit funds, and also has a notion of customers logging in. It also has a simple text-based interface for letting a user communicate with the program: ```=java= public class Customer { String name; String password; LinkedList<Account> accounts = new LinkedList<Account>(); } public class Account { int number; Customer owner; double balance; } public class BankingService { LinkedList<Account> accounts = new LinkedList<Account>(); LinkedList<Customer> customers = new LinkedList<Customer>(); public void addAccount(Account newA) { this.accounts.addFirst(newA); } public double getBalance(int forAcctNum) { for (Account acct:accounts) { if (acct.number == forAcctNum) return acct.balance; } return 0; } public double withdraw(int forAcctNum, double amt) { for (Account acct:accounts) { if (acct.number == forAcctNum) { acct.balance = acct.balance - amt; return amt; } } return 0; } public String login(String custname, String withPwd) { for (Customer cust:customers) { if (cust.name.equals(custname)) { if (cust.password.equals(withPwd)) { return "Welcome"; } else { return "Try Again"; } } } return "Oops -- don't know this customer"; } } // end of BankingService class // in a separate Main class public static void main(String[] args) { BankingService B = new BankingService(); Customer kCust = new Customer("kathi", "cs200"); Account kAcct = new Account(100465, kCust, 150); B.addAccount(kAcct); System.out.println("Kathi's balance is " + kAcct.balance); // prints 150 B.withdraw(100465, 30); System.out.println("Kathi's balance is " + kAcct.balance); // prints 120 } ``` Critique it: what problems do you see in this code? - Any class that has access to a customer object has the ability to access or change that customer’s password. In the BankingService class, for example, the login method directly accesses the password to check whether it is valid; that method could just as easily (maliciously!) change the password. The contents of the password should never get out of the Customer class. (The real problem here is that login should be a method on Customer, which has the data that the method needs.) - A similar concern applies to the balance field in withdraw, but withdraw illustrates another problem. Imagine that the bank adds more details to accounts (such as overdraft protection or a withdrawal fee). The BankingService class would have to keep changing as the notion of Accounts changes, which makes no sense. The BankingService class simply wants a way to execute a withdrawal without concern for the detailed structure of Account objects. The withdraw method needs to be a method on Account, not BankingService. - The BankingService class has written all of its code over accounts and customers against a fixed data structure (the LinkedList). The dependency is clear in the code for the methods (getBalance, withdraw, and login): each includes a for-loop over the list in its implementation. - The use of LinkedLists (as opposed to HashMaps) could be a slow approach to managing accounts and customers - The dummy return value of 0 in getBalance and withdraw is awful, because it does not distinguish between a valid answer (an account balance of 0) and an error condition. Picking a dummy value to satisfy the type system is never a good idea. This program needs a better way of handing errors. Over the next two lectures, we're going to adapt this code to avoid these problems. ## Encapsulation: Protecting Customer and Account Data Let's focus on the passwords. Right now, these are public within the banking application, whereas sensitive information like this should be private instead. Adding the `private` modifier is easy enough, but then our code fails to run. ```=java= public class Customer { String name; private String password; LinkedList<Account> accounts = new LinkedList<Account>(); } public class BankingService { public String login(String custname, String withPwd) { for (Customer cust:customers) { if (cust.name.equals(custname)) { if (cust.password.equals(withPwd)) { // <-- ERROR return "Welcome"; } else { return "Try Again"; } } } return "Oops -- don't know this customer"; } } ``` Line 12 causes the problem: once the password is marked private, the `login` method cannot dig into the `cust` object to retrieve the password. How do we solve this? One idea would be to make a getter method that returns the password, modifying line 12 to be ```=java if (cust.getPassword().equals(withPwd)) { ``` But our goal should be to *protect* the password, not to find another way to hand it out to any part of the code that wants it. A better solution is to move the computation that compares passwords into the `Customer` class. ```=java public class Customer { // new method public boolean checkPwd(String checkAgainst) { return this.password.equals(checkAgainst); } } // in login method if (cust.checkPwd(withPwd)) { ``` All we did was move the computation (the `equals` check) over private data into the class that holds that data. This follows a core principle of object oriented design: **A computation should live in the class that holds its primary data** This concept often does under the term *encapsulation*. Intuitively, encapsulation is about bundling data and code together in order to (1) reduce dependencies of one part of a system on structural details of another, and (2) control manipulation of and access to data. Classes and objects provide built-in structures for encapsulation. The same idea matters in other languages, but as a programmer you may need to provide that structure manually. At this point, we should proceed to make the other fields of the `Customer` and `Account` classes private, and to create other methods within those classes as needed to get the code running again. The code posted alongside these notes will show the final versions that do this. ## Being Flexible about Data Structure Choices Right now, the BankingService uses LinkedLists to store its collections of Accounts and Customers. Is this a good choice? Why not ArrayLists? Why not HashMaps? Why not some other nifty data structure that we might learn about later this semester? This highlights another central concern in good program design: sometimes, we want to experiment with different underlying data structures (to see how they perform in time and space usage). The core ideas in a BankingService, such as logging in and withdrawing money, don't really change. We don't want to have to edit the BankingService code every time we want to try a different data structure, however. Wouldn't it be nice to separate out the core code from the data structure? Interfaces to the rescue. Let's look again at the `login` method. Where has it been written with the explicit assumption that we're storing the data in (Linked) lists? ```=java= public class BankingService { public String login(String custname, String withPwd) { for (Customer cust:customers) { if (cust.name.equals(custname)) { if (cust.checkPwd(withPwd)) { return "Welcome"; } else { return "Try Again"; } } } return "Oops -- don't know this customer"; } } ``` Line 4 is the culprit -- we're using a `for` loop to process the `Customer` data. The for-loop on line 4 would not compile or run if our data were in a hashmap instead. If we want to generalize this code, we first need to ask ourselves what the `for` loop is there to do. Lines 4 and 5 together are trying to find the `Customer` with the given `custname` in the `name` field. This suggests that we could rewrite the `BankingService` code as follows: ```=java= public class BankingService { private Customer findCust(String custname) { for (Customer cust:customers) { if (cust.name.equals(custname)) { return cust; } else { return null; // we'll return to this shortly } } } public String login(String custname, String withPwd) { Customer cust = findCust(custname); if (cust.checkPwd(withPwd)) { return "Welcome"; } else { return "Try Again"; } // next line can't be reached, so remove it // return "Oops -- don't know this customer"; } } ``` Now, `login` says nothing about lists -- it can work with any data structure for which there is a `findCust` method. Now we're really sounding like an interface (we just need a method with a specific name). What if we had an interface like: ```=java interface ICustSet { Customer findCust(String custname); } ``` (here, `ICustSet` stands for "interface for a set of customers") Where do we put this interface though? Should it go on the `BankingService` class? No. The `BankingService` isn't a set of customers. Instead, it needs to take (as an input) a set of customers. In other words, we want the class to look like: ```=java= public class BankingService { ICustSet customers; public BankingService(ICustSet custData) { this.customers = custData; } public String login(String custname, String withPwd) { Customer cust = findCust(custname); ... } } ``` Separately, we create a class that implements `ICustSet`: ```=java= class CustomerList implements ICustSet { LinkedList<Customer> customers; public Customer findCust(String custname) { for (Customer cust:customers) { if (cust.name.equals(custname)) { return cust; } else { return null; // we'll return to this shortly } } } } ``` We could also have a version of this that provides a Customer set via a Hashmap: ```=java= class CustomerHM implements ICustSet { HashMap<String,Customer> customers; public Customer findCust(String custname) { return customers.get(custname); } } ``` Now that the `BankingService` is taking an `ICustSet` as a constructor input, we have to adjust how we set up the service in our `main` method. See lines 3 and 4 of the following code: ```=java= // in a separate Main class public static void main(String[] args) { CustomerList CL = new CustomerList(); BankingService B = new BankingService(CL); Customer kCust = new Customer("kathi", "cs200"); Account kAcct = new Account(100465, kCust, 150); B.addAccount(kAcct); System.out.println("Kathi's balance is " + kAcct.balance); // prints 150 B.withdraw(100465, 30); System.out.println("Kathi's balance is " + kAcct.balance); // prints 120 } ``` Our Banking Service is now agnostic to which data structure is used to manage customers. We could do the same thing with the data structure used to manage accounts. ## Pulling out the User Interface The user interface is another part of the original `BankingService` that we might want to pull out into a separate class to switch out (imagine a web interface vs an audio interface vs a text interface). To pull out the user interface, we make a new class for the interface code. Here, we're calling it `BankingConsole`. We'll move the `loginScreen` method over to this new class: ```=java= import java.util.Scanner; public class BankingConsole { private Scanner keyboard = new Scanner(System.in); public void loginScreen() { System.out.println("Welcome to the Bank. Please log in."); System.out.print("Enter your username: "); String username = keyboard.next(); System.out.print("Enter your password: "); String password = keyboard.next(); this.login(username, password); System.out.println("Thanks for logging in!"); } } ``` IntelliJ flags the call to `this.login` as having an error. Now that we've moved the `loginScreen` out of `BankingService`, the `login` method is no longer in the `this` class. Removing `this` doesn't help: the `login` method is still in the `BankingServices` class. So what do we do? Two suggestions jump to mind: 1. move the `login` method from `BankingService` into `BankingConsole` as well 2. have the `loginScreen` method take a `BankingService` object as input (which we could then use to access the `login` method) The first option ends up not making sense: the `login` method isn't really about the interface, so it doesn't seem to belong in the class for the interface code. More practically, if we move `login`, we'd then run into a similar problem with `findCustomer`, and that definitely isn't related to the user interface. So perhaps we should try the second option. The second option would work. But we actually want to solve this problem slightly differently. It will turn out that many `BankingConsole` methods would need to access methods in `BankingService` (imagine that the user interface gave someone a way to make a withdrawal, for example). Rather than have all of them take a `BankingService` object as input, we can pass a single `BankingService` object to the `BankingConsole` constructor, then use that for all service operations. The code would look as follows: ```=java= import java.util.Scanner; public class BankingConsole { private Scanner keyboard = new Scanner(System.in); BankingService forService; public BankingConsole(BankingService forService) { this.forService = forService; } public void loginScreen() { System.out.println("Welcome to the Bank. Please log in."); System.out.print("Enter your username: "); String username = keyboard.next(); System.out.print("Enter your password: "); String password = keyboard.next(); forService.login(username, password); System.out.println("Thanks for logging in!"); } } ``` # Model-View-Controller With the addition of the `BankingConsole`, we can talk about the overall architecture (configuration and roles) of the application and its classes. We talked about how the classes can be divided into three roles, as shown in the diagram in lecture. - The **view** (`BankingConsole`), which contains the code that interacts with the user (whether text I/O, audio, web interface, etc). The user gives input to the view, which executes commands in the application through the \... - **controller** (`BankingService`), which contains methods for the major operations that the application provides (like logging in, withdrawing funds, etc). Once the controller knows what the user wants to do, it coordinates actual completion of a task by calling methods in the \... - **model** (`Customer`), classes that contain the data and perform operations on the data to fulfill application tasks. This architecture, known as *model-view-controller* is quite common in software engineering. It reinforces the idea that the interface code should be separate from the underlying operations, and that the underlying operations should be expressible against a variety of data structures. The details of the data structures live in their own classes, with fields protected through access modifiers. This enables updating an application with different data details without having to reimplement the core logic.