本文所讨论的话题
通常在一个业务系统中会有各种不同的角色,而在系统的若干功能模块中,这些角色所能看到的数据是不一样的。那么在程序中如何处理类似问题会更优呢,本文想通过一个简单的场景来和大家再次探讨一下如何用OO来改善我们的系统。
场景
系统中目前有三种角色,超级管理员、管理员、普通用户。有一个功能是显示书籍列表,每一种角色所能看到的书籍是不同的。这里面会有一些规则,但是这些规则不是文本所要讨论的重点。
最初的实现
通常我们最快能想到的思路是,先创建两个类,用户类User、书籍类Book。在User类中我们创建了判断用户角色类型的方法。
package fangwei.solution1.user.domain;
public class User {
public static final int ROLE_SUPER_ADMIN = 1;
public static final int ROLE_ADMIN = 2;
public static final int ROLE_COMMON_USER = 3;
private Integer roleId;
public void setRoleId(Integer roleId) {
this.roleId = roleId;
}
public Integer getRoleId() {
return roleId;
}
public boolean isSuperAdmin() {
return this.roleId==ROLE_SUPER_ADMIN;
}
public boolean isAdmin() {
return this.roleId==ROLE_ADMIN;
}
public boolean isCommonUser() {
return this.roleId==ROLE_COMMON_USER;
}
}
package fangwei.solution1.book.domain;
public class Book {
}
然后用分层的方式去处理问题,先创建service层的接口。为了处理不同角色的情况,我们传入了一个user对象。
package fangwei.solution1.book.service;
import java.util.List;
import fangwei.solution1.book.domain.Book;
import fangwei.solution1.user.domain.User;
public interface BookService {
public List<Book> listBook(User user);
}
实现service层的接口,同时我们需要创建dao层的接口
package fangwei.solution1.book.service;
import java.util.List;
import fangwei.solution1.book.dao.BookDao;
import fangwei.solution1.book.domain.Book;
import fangwei.solution1.user.domain.User;
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public List<Book> listBook(User user){
List<Book> result = null;
if(user.isSuperAdmin()){
result = bookDao.selectBookList4SuperAdmin();
}else if(user.isAdmin()){
result = bookDao.selectBookList4Admin();
}else if (user.isCommonUser()) {
result = bookDao.selectBookList4CommonUser();
}
return result;
}
}
package fangwei.solution1.book.dao;
import java.util.List;
import fangwei.solution1.book.domain.Book;
public interface BookDao {
public List<Book> selectBookList4SuperAdmin();
public List<Book> selectBookList4Admin();
public List<Book> selectBookList4CommonUser();
}
实现dao层的接口,为了本文的讨论,我们加入了log语句,而并没有真正去编写实现代码
package fangwei.solution1.book.dao;
import org.apache.log4j.Logger;
import java.util.List;
import fangwei.solution1.book.domain.Book;
public class BookDaoImpl implements BookDao {
private static final Logger logger = Logger.getLogger(BookDaoImpl.class);
public List<Book> selectBookList4SuperAdmin() {
logger.debug("selectBookList4SuperAdmin");
return null;
}
public List<Book> selectBookList4Admin() {
logger.debug("selectBookList4Admin");
return null;
}
public List<Book> selectBookList4CommonUser() {
logger.debug("selectBookList4CommonUser");
return null;
}
}
好了,下面我们来编写一个测试,来看service层的实现是否正确
package fangwei.solution1.book.service;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import fangwei.solution1.book.dao.BookDaoImpl;
import fangwei.solution1.book.service.BookServiceImpl;
import fangwei.solution1.user.domain.User;
public class TestBookServiceImpl {
private BookServiceImpl bookService;
@Before
public void setUp() throws Exception {
bookService = new BookServiceImpl();
bookService.setBookDao(new BookDaoImpl());
}
@After
public void tearDown() throws Exception {
}
@Test
public void testListBook() {
User user = new User();
user.setRoleId(User.ROLE_SUPER_ADMIN);
bookService.listBook(user);
user.setRoleId(User.ROLE_ADMIN);
bookService.listBook(user);
user.setRoleId(User.ROLE_COMMON_USER);
bookService.listBook(user);
}
}
运行这个测试,输出结果如下
selectBookList4SuperAdmin
selectBookList4Admin
selectBookList4CommonUser
说明这个实现是正确的。
问题在哪里
在一个业务系统中类似上面的场景会有很多,也就是说我们会在service层的若干实现方法中使用if...else if...else if...用来处理不同角色的情况。如果角色类型是不会增加的,那么上面的实现没有任何问题。但是很可惜,客户在使用系统一段时间后提出要增加新的角色类型,当然新角色所能看到的书籍列表也是有别于之前角色的:(
这个时候,我们痛苦的发现,我们需要满系统去找使用if...else if...else if...用来处理不同角色的地方,然后再加入一个else if。有人会说,这没什么啊,我们的团队是按功能模块划分来开发的,每个人挨个service类去加就好了。但是我当时的想法是,一定有比这更好的设计。
另一种实现
我们再回过头去看service层的listBook方法,其实我们要做的事只有一件——查询书籍列表,只是因为角色这个因素导致出现了分支结构。那么我们可以将这个变化抽象出来,使我们在查询书籍列表时不需要关心角色这个因素。下面是另一种实现的service层接口,同时我们抽象出了新的接口BookBiz
package fangwei.solution2.book.service;
import java.util.List;
import fangwei.solution2.book.domain.Book;
import fangwei.solution2.book.domain.BookBiz;
public interface BookService {
public List<Book> listBook(BookBiz bookBiz);
}
package fangwei.solution2.book.domain;
import java.util.List;
import fangwei.solution2.book.domain.Book;
public interface BookBiz {
public List<Book> listBook();
}
这样,我们在实现service层接口的时候就不需要考虑角色因素了
package fangwei.solution2.book.service;
import java.util.List;
import fangwei.solution2.book.domain.Book;
import fangwei.solution2.book.domain.BookBiz;
public class BookServiceImpl implements BookService {
public List<Book> listBook(BookBiz bookBiz) {
return bookBiz.listBook();
}
}
那么我们如何使用BookBiz这个接口呢,我们可以创建三个新的用户类分别对应于场景中三种不同的角色,然后让他们实现BookBiz这个接口。为了测试的简单,我们直接依赖了dao层的实现BookDaoImpl,在实际当中可以使用spring等ioc容器在运行期注入。
package fangwei.solution2.user.domain;
import java.util.List;
import fangwei.solution2.book.dao.BookDao;
import fangwei.solution2.book.dao.BookDaoImpl;
import fangwei.solution2.book.domain.Book;
import fangwei.solution2.book.domain.BookBiz;
public class SuperAdminUser extends User implements BookBiz {
BookDao bookDao = new BookDaoImpl();
public List<Book> listBook() {
return bookDao.selectBookList4SuperAdmin();
}
}
package fangwei.solution2.user.domain;
import java.util.List;
import fangwei.solution2.book.dao.BookDao;
import fangwei.solution2.book.dao.BookDaoImpl;
import fangwei.solution2.book.domain.Book;
import fangwei.solution2.book.domain.BookBiz;
public class AdminUser extends User implements BookBiz {
BookDao bookDao = new BookDaoImpl();
public List<Book> listBook() {
return bookDao.selectBookList4Admin();
}
}
package fangwei.solution2.user.domain;
import java.util.List;
import fangwei.solution2.book.dao.BookDao;
import fangwei.solution2.book.dao.BookDaoImpl;
import fangwei.solution2.book.domain.Book;
import fangwei.solution2.book.domain.BookBiz;
public class CommonUser extends User implements BookBiz {
BookDao bookDao = new BookDaoImpl();
public List<Book> listBook() {
return bookDao.selectBookList4CommonUser();
}
}
依然需要写一个测试来验证我们的设计
package fangwei.solution2.book.service;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import fangwei.solution2.book.domain.BookBiz;
import fangwei.solution2.book.service.BookService;
import fangwei.solution2.book.service.BookServiceImpl;
import fangwei.solution2.user.domain.AdminUser;
import fangwei.solution2.user.domain.CommonUser;
import fangwei.solution2.user.domain.SuperAdminUser;
public class TestBookServiceImpl {
private BookService bookService;
@Before
public void setUp() throws Exception {
bookService = new BookServiceImpl();
}
@After
public void tearDown() throws Exception {
}
@Test
public void testListBook() {
BookBiz bookBiz = new SuperAdminUser();
bookService.listBook(bookBiz);
bookBiz = new AdminUser();
bookService.listBook(bookBiz);
bookBiz = new CommonUser();
bookService.listBook(bookBiz);
}
}
运行测试的输出结果同第一种实现
selectBookList4SuperAdmin
selectBookList4Admin
selectBookList4CommonUser
上层如何调用
通常我们都会将User对象放入HttpSession对象中,所以要使用此种实现,我们需要在用户登录成功后,根据不同的角色创建不同的用户类放入HttpSession对象中。登录的代码在这里就略过了,下面给出场景的一种action层实现及测试代码,重点是对HttpSession对象的操作,框架是次要因素
package fangwei.solution2.book.action;
import java.util.List;
import java.util.Map;
import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.interceptor.SessionAware;
import com.opensymphony.xwork2.ActionSupport;
import fangwei.solution2.book.domain.Book;
import fangwei.solution2.book.domain.BookBiz;
import fangwei.solution2.book.service.BookService;
public class BookAction extends ActionSupport implements SessionAware{
private static final long serialVersionUID = 2744158510001123482L;
private List<Book> bookList;
private Map<String, Object> session;
private BookService bookService;
public void setBookList(List<Book> bookList) {
this.bookList = bookList;
}
public List<Book> getBookList() {
return bookList;
}
public void setSession(Map<String, Object> session) {
this.session = session;
}
public void setBookService(BookService bookService) {
this.bookService = bookService;
}
@Action(value="/book/listBook",
results = {@Result(name=SUCCESS,location="/WEB-INF/jsp/book/listBook.jsp")}
)
public void listBook() {
BookBiz bookBiz = (BookBiz) session.get("user");
bookList = bookService.listBook(bookBiz);
}
}
package fangwei.solution2.book.action;
import static org.junit.Assert.*;
import java.util.HashMap;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import fangwei.solution2.book.service.BookServiceImpl;
import fangwei.solution2.user.domain.AdminUser;
import fangwei.solution2.user.domain.CommonUser;
import fangwei.solution2.user.domain.SuperAdminUser;
import fangwei.solution2.user.domain.User;
public class TestBookAction {
private BookAction bookAction;
@Before
public void setUp() throws Exception {
bookAction = new BookAction();
bookAction.setBookService(new BookServiceImpl());
}
@After
public void tearDown() throws Exception {
}
@Test
public void testListBook() {
Map<String, Object> session = new HashMap<String, Object>();
bookAction.setSession(session);
User user = new SuperAdminUser();
session.put("user", user);
bookAction.listBook();
user = new AdminUser();
session.put("user", user);
bookAction.listBook();
user = new CommonUser();
session.put("user", user);
bookAction.listBook();
}
}
好处在哪里
再回到第一种实现提出的问题,现在如果我们需要增加新的角色类型,就可以创建一个新的用户类XXXUser,然后实现BookBiz等需要的业务接口就可以了。在这种设计下,我们再也不需要满系统去增加else if了。同样的一句代码bookService.listBook(bookBiz);在运行期会有不同的执行效果,这就是OO的多态之一。有人会说,看起来service层的实现没啥用了,只是一个facade了。其实不然,BookBiz抽象出来的是由于角色因素导致的变化,我们依然可以在service中编写所有角色共有的后续业务逻辑,而不必在每个BookBiz的实现中去重复。
package fangwei.solution2.book.service;
import java.util.List;
import fangwei.solution2.book.domain.Book;
import fangwei.solution2.book.domain.BookBiz;
public class BookServiceImpl implements BookService {
public List<Book> listBook(BookBiz bookBiz) {
List<Book> listBook = bookBiz.listBook();
//后续的业务逻辑
//...
return listBook;
}
}
疑惑依然存在
第二种实现隔离了由于角色因素导致的变化,使我们能够更方便的增加新的角色类型,但是当需要隔离的变化多了以后会怎么样呢。我们的BookBiz接口中的方法会越来越多,类似BookBiz的接口会越来越多,但是我们的每一种用户类如SuperAdminUser只有一个,那么最后我们的SuperAdminUser类实现的接口会越来越多,即实现的接口方法会越来越多,这个类会不断的膨胀下去导致维护困难。。。这种情况,我们又该如何应对呢?一定还有更优的设计?本文抛砖引玉,旨在了解大家在遇到类似问题时如何用OO的思想去分析解决。