Reputation: 2229
I have 2 different types of users in my app: LoggedInUser
and GuestUser
They both have same functionalities such as createNewOrder
and cancelOrder
(the implementation should be different) but LoggedInUser
can do much more such as WriteReview
,FavouriteProduct
etc.
I want a design pattern for these types of users, so that I don't check which type of user my application has at the moment and I just call the method without knowing the actual type of the user.
Is it even possible to do this? Which design pattern is the most suitable?
What I have done so far is creating an abstract class called User
which has the similar functionalities:
abstract class User{
void createOrder()
void cancelOrder()
}
and also a class for LoggedInUser
and a class for GuestUser
:
class LoggedInUser extends User {
//some instance variables
void favouriteProduct(String id) {
//...
}
void writeReview(Review review) {
//...
}
@override
void cancelOrder(){
//...
}
@override
void cancelOrder(){
//...
}
}
class GuestUser extends User {
//some instance variables
@override
void cancelOrder(){
//...
}
@override
void cancelOrder(){
//...
}
}
Thanks.
Upvotes: 0
Views: 1078
Reputation: 3707
If you are absolutely dead set against have to check which type of user you have, and want to treat them the same way no matter what (I'm not sure why you would want that), you can add default implementations of the favouriteProduct
and writeReview
methods in your abstract User
class. These default implementations would be used when calling them on GuestUser
instances, while you would override them in your LoggedInUser
. I think this is not a great design.
public class UserExample
{
public static void main(String[] args)
{
User loggedInUser = new LoggedInUser();
User guestUser = new GuestUser();
loggedInUser.favouriteProduct("123");
guestUser.favouriteProduct("123");
Review review = new Review();
review.text = "This product is amazing.";
loggedInUser.writeReview(review);
guestUser.writeReview(review);
}
}
abstract class User
{
abstract void createOrder();
abstract void cancelOrder();
public void favouriteProduct(String id)
{
System.out.printf("UserAbstract::favouriteProduct(%s)%n", id);
}
public void writeReview(Review review)
{
System.out.printf("UserAbstract::writeReview%n\t%s%n", review.text);
}
}
class LoggedInUser extends User
{
//some instance variables
@Override
public void favouriteProduct(String id)
{
System.out.printf("LoggedInUser::favouriteProduct(%s)%n", id);
}
@Override
public void writeReview(Review review)
{
System.out.printf("LoggedInUser::writeReview%n\t%s%n", review.text);
}
@Override
public void createOrder()
{
//...
}
@Override
public void cancelOrder()
{
//...
}
}
class GuestUser extends User
{
//some instance variables
@Override
public void createOrder()
{
//...
}
@Override
public void cancelOrder()
{
//...
}
}
class Review
{
public String text;
}
You would probably be better off moving the favouriteProduct
and writeReview
methods to a service class. Those methods could accept instances that implement the User
interface, then decide how to handle instances of LoggedInUsers
vs GuestUsers
differently. This allows you to pass users around pretty generically and only branch when absolutely necessary. You can ignore GuestUsers
in the service methods, throw an exception, redirect to a login, whatever you want. IMO that kind of logic doesn't belong in your business objects.
public class UserExample2
{
public static void main(String[] args)
{
LoggedInUser loggedInUser = new LoggedInUser();
GuestUser guestUser = new GuestUser();
UserService.favouriteProduct(loggedInUser, "123");
UserService.favouriteProduct(guestUser, "123");
Review review = new Review();
review.text = "This product is amazing.";
UserService.writeReview(loggedInUser, review);
UserService.writeReview(guestUser, review);
}
}
interface User
{
void createOrder();
void cancelOrder();
}
class LoggedInUser implements User
{
//some instance variables
public void createOrder()
{
//...
}
public void cancelOrder()
{
//...
}
}
class GuestUser implements User
{
//some instance variables
public void createOrder()
{
//...
}
public void cancelOrder()
{
//...
}
}
class UserService
{
public static void favouriteProduct(LoggedInUser user, String id)
{
System.out.printf("UserService::favouriteProduct LoggedInUser favourites %s%n", id);
}
public static void favouriteProduct(GuestUser user, String id)
{
System.out.printf("UserService::favouriteProduct GuestUser favourites %s%n", id);
}
public static void writeReview(LoggedInUser user, Review review)
{
System.out.printf("UserService::writeReview LoggedInUser writes%n\t%s%n", review.text);
}
public static void writeReview(GuestUser user, Review review)
{
System.out.printf("UserService::writeReview GuestUser writes%n\t%s%n", review.text);
}
}
class Review
{
public String text;
}
[Edit]
Here is a more thorough example of how to solve this kind or problem. This more clearly illustrates how polymorphism and inheritance can be used to implement common functionality across different types of sessions in a typical e-commerce cart scenario.
import java.util.*;
public class UserExample3
{
public static void main(String[] args)
{
// Set up our user with test data IRL this would be retrieved from authentication mechanism
Address userAddress = new Address("1313 Mockingbird Lane", "London", "GB", "12345");
User loggedInUser = new User(23, "Alice", userAddress);
// Set up a session for our user
UserSession userSession = new UserSession(loggedInUser);
// Set up a guest session
GuestSession guestSession = new GuestSession();
// Add items - action happens in the abstract class
userSession.addOrderItem("123", 2);
guestSession.addOrderItem("456", 3);
// Favourite items - action happens in the concrete classes
userSession.favouriteProduct("123");
guestSession.favouriteProduct("456");
// List items via the cart service, requires instances of Session interface
CartService.listCartItems(userSession);
CartService.listCartItems(guestSession);
// Write reviews via the Review service, requires instances of Session interface
Review userReview = ReviewService.writeReview(userSession, "123", "This product is amazing");
System.out.println(userReview);
Review guestReview = ReviewService.writeReview(guestSession, "456", "This product is pants");
System.out.println(guestReview);
// Create orders via the order service, requires instances of Session interface
Order userOrder = null;
try
{
userOrder = OrderService.createOrderFromSession(userSession);
System.out.println(userOrder);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
// Guest checkout requires us to collect an address (user already has address saved in their profile)
Address guestAddress = new Address("1060 W. Addison", "Chicago", "US", "12345");
guestSession.setAddress(guestAddress);
Order guestOrder = null;
try
{
guestOrder = OrderService.createOrderFromSession(guestSession);
System.out.println(guestOrder);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
// Estimate shipping charges via cart service
double userEstimate = CartService.estimateShipping(userSession);
System.out.printf("User shipping estimate: %.2f%n", userEstimate);
double guestEstimate = CartService.estimateShipping(guestSession);
System.out.printf("Guest shipping estimate: %.2f%n", guestEstimate);
// Convert a guest session to a user session via CartService - requires concrete classes
UserSession newUserSession = new UserSession(loggedInUser);
CartService.populateUserSessionCartFromGuestSession(newUserSession, guestSession);
/*
The guest session items are now in the user session
and the guest session favorites have been added to the user
Requires instances of Session interface
*/
CartService.listCartItems(newUserSession);
System.out.println(loggedInUser);
// Empty the cart via the cart service
CartService.emptyCart(newUserSession);
CartService.listCartItems(newUserSession);
// Cancel an order via the order service - will be null if validation failed
if(userOrder != null)
{
OrderService.cancelOrder(userOrder);
System.out.println(userOrder);
}
// Complete an order via the order service - will be null if validation failed
if(guestOrder != null)
{
OrderService.completeOrder(guestOrder);
System.out.println(guestOrder);
}
}
}
/**
* Do cart things
*/
class CartService
{
/**
* Print the line items in a cart
* <p>
* This is an example of polymorphism - any instance that implements the
* Session interface will work here
*
* @param session instance of interface Session
*/
public static void listCartItems(Session session)
{
System.out.println("Order items:");
for (Map.Entry<String, Integer> currItem : session.getOrderItems().entrySet())
{
System.out.printf("\t%s: %d%n", currItem.getKey(), currItem.getValue());
}
}
/**
* Populate a UserSession with data collected in a guest session
* Used when a user begins their order while unauthenticated, the logs in prior to completing the order
*
* @param userSession Instance of UserSession
* @param guestSession Instance of GuestSession
*/
public static void populateUserSessionCartFromGuestSession(UserSession userSession, GuestSession guestSession)
{
userSession.getOrderItems().putAll(guestSession.getOrderItems());
userSession.getUser().getFavouriteProducts().addAll(guestSession.getFavouriteProducts());
guestSession.clearOrderItems();
}
/**
* Clear the items from a cart
* <p>
* This is an example of polymorphism - any instance that implements the
* Session interface will work here
*
* @param session Instance of Session interface
*/
public static void emptyCart(Session session)
{
session.clearOrderItems();
}
public static double estimateShipping(Session session)
{
Map<String, Double> perItemChargesByCountry = new HashMap<>();
perItemChargesByCountry.put("US", 1.23);
perItemChargesByCountry.put("GB", 1.05);
String countryCode = session.getAddress().getCountry();
Double perItemCharge = perItemChargesByCountry.get(countryCode);
int numItems = 0;
for (Map.Entry<String, Integer> currLineItem : session.getOrderItems().entrySet())
{
numItems += currLineItem.getValue();
}
return numItems * perItemCharge;
}
}
/**
* Do order things
*/
class OrderService
{
/**
* Create an order from the line items in a cart
* <p>
* This is an example of polymorphism - any instance that implements the
* Session interface will work here
* <p>
* If the session is an instance of UserSession, data available only on
* those instances will be set on the order record
*
* @param session Instance of Session interface
* @return Order
*/
public static Order createOrderFromSession(Session session) throws Exception
{
Order order = new Order();
order.setStatus(Order.Status.PENDING);
order.setAddress(session.getAddress());
order.getItems().putAll(session.getOrderItems());
if (session instanceof UserSession)
{
order.setUser(((UserSession) session).getUser());
}
if(!order.validate())
{
throw new Exception("Something is wrong with the order");
}
return order;
}
/**
* Cancel an order by changing its status
*
* @param order Order
*/
public static void cancelOrder(Order order)
{
order.setStatus(Order.Status.CANCELLED);
}
/**
* Complete an order by changing its status
*
* @param order Order
*/
public static void completeOrder(Order order)
{
order.setStatus(Order.Status.COMPLETE);
}
}
/**
* Do review things
*/
class ReviewService
{
/**
* Create a product review
* <p>
* This is an example of polymorphism - any instance that implements the
* Session interface will work here
* <p>
* If the session is an instance of UserSession, data available only on
* those instances will be set on the review record
*
* @param session Session
* @param productId String
* @param text String content of review
* @return Review
*/
public static Review writeReview(Session session, String productId, String text)
{
Review review = new Review();
if (session instanceof UserSession)
{
review.setUser(((UserSession) session).getUser());
}
review.setProductId(productId);
review.setText(text);
return review;
}
}
/**
* Describe the contract that all session classes should adhere to
*/
interface Session
{
Address getAddress();
Map<String, Integer> getOrderItems();
void setOrderItems(Map<String, Integer> orderItems);
void addOrderItem(String productId, int quantity);
void removeOrderItem(String productId);
void clearOrderItems();
void favouriteProduct(String productId);
}
/**
* Implement functionality common to all concrete session classes
*/
abstract class SessionAbstract
{
private Map<String, Integer> orderItems = new HashMap<>();
public Map<String, Integer> getOrderItems()
{
return orderItems;
}
public void setOrderItems(Map<String, Integer> orderItems)
{
this.orderItems = orderItems;
}
public void addOrderItem(String productId, int quantity)
{
if (orderItems.containsKey(productId))
{
quantity += orderItems.get(productId);
}
orderItems.put(productId, quantity);
}
public void removeOrderItem(String productId)
{
orderItems.remove(productId);
}
public void clearOrderItems()
{
orderItems.clear();
}
}
/**
* Concrete session class for authenticated users
*/
class UserSession extends SessionAbstract implements Session
{
private final User user;
public UserSession(User user)
{
this.user = user;
}
public User getUser()
{
return user;
}
@Override
public Address getAddress()
{
return user.getAddress();
}
public void favouriteProduct(String productId)
{
user.addFavouriteProduct(productId);
}
}
/**
* Concrete session class for guest users
*
* We need to have an instance of Address and collection of favourite items
* here since we don't have a user instance
*/
class GuestSession extends SessionAbstract implements Session
{
private Address address;
Set<String> favouriteProducts = new TreeSet<>();
public Address getAddress()
{
return address;
}
public void setAddress(Address address)
{
this.address = address;
}
public Set<String> getFavouriteProducts()
{
return favouriteProducts;
}
public void favouriteProduct(String productId)
{
favouriteProducts.add(productId);
}
}
class Address
{
String street;
String city;
String country;
String postCode;
public Address(String street, String city, String country, String postCode)
{
this.street = street;
this.city = city;
this.country = country;
this.postCode = postCode;
}
public String getStreet()
{
return street;
}
public void setStreet(String street)
{
this.street = street;
}
public String getCity()
{
return city;
}
public void setCity(String city)
{
this.city = city;
}
public String getCountry()
{
return country;
}
public void setCountry(String country)
{
this.country = country;
}
public String getPostCode()
{
return postCode;
}
public void setPostCode(String postCode)
{
this.postCode = postCode;
}
@Override
public String toString()
{
return "Address{" + "street='" + street + '\'' + ", city='" + city + '\'' + ", country='" + country + '\'' + ", postCode='" + postCode + '\'' + '}';
}
}
class User
{
Integer id;
String name;
Address address;
Set<String> favouriteProducts = new TreeSet<>();
public User(Integer id, String name, Address address)
{
this.id = id;
this.name = name;
this.address = address;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public Address getAddress()
{
return address;
}
public void setAddress(Address address)
{
this.address = address;
}
public Set<String> getFavouriteProducts()
{
return favouriteProducts;
}
public void setFavouriteProducts(Set<String> favouriteProducts)
{
this.favouriteProducts = favouriteProducts;
}
public void addFavouriteProduct(String productId)
{
favouriteProducts.add(productId);
}
@Override
public String toString()
{
return "User{" + "id=" + id + ", name='" + name + '\'' + ", address=" + address + ", favouriteProducts=" + favouriteProducts + '}';
}
}
class Order
{
public enum Status
{
PENDING, CANCELLED, COMPLETE
}
private Status status;
private User user;
private Address address;
private Map<String, Integer> items = new HashMap<>();
public User getUser()
{
return user;
}
public void setUser(User user)
{
this.user = user;
}
public Address getAddress()
{
return address;
}
public void setAddress(Address address)
{
this.address = address;
}
public Map<String, Integer> getItems()
{
return items;
}
public void setItems(Map<String, Integer> items)
{
this.items = items;
}
public Status getStatus()
{
return status;
}
public void setStatus(Status status)
{
this.status = status;
}
public boolean validate()
{
boolean valid = true;
if(items.isEmpty())
{
valid = false;
}
else if(address == null)
{
valid = false;
}
else if(status == null)
{
valid = false;
}
return valid;
}
@Override
public String toString()
{
return "Order{" + "status=" + status + ", user=" + user + ", address=" + address + ", items=" + items + '}';
}
}
class Review
{
private String productId;
private String text;
private User user;
public String getProductId()
{
return productId;
}
public void setProductId(String productId)
{
this.productId = productId;
}
public String getText()
{
return text;
}
public void setText(String text)
{
this.text = text;
}
public User getUser()
{
return user;
}
public void setUser(User user)
{
this.user = user;
}
@Override
public String toString()
{
String name = (user != null) ? user.getName() : "Anonymous Coward";
return "Review{" + "productId='" + productId + '\'' + ", text='" + text + '\'' + ", user=" + name + '}';
}
}
Upvotes: 1