[TOC] # 解决思路 面向对象设计问题要求求职者设计出类和方法,以实现技术问题或描述真实生活中的对象。可以按照以下步骤来进行解决: ## 1. 处理不明确的地方 面向对象设计问题往往会故意放些烟幕弹,意在检验你是武断臆测,还是提出问题以厘清问题。毕竟,开发人员要是没弄清楚自己要开发什么,就直接挽起袖子开始编码,只会浪费公司的财力物力,还可能造成更严重的后果。 碰到面向对象设计问题时,你应该先问清楚,谁是使用者、他们将如何使用。对某些问题,你甚至还要问清楚“5W1H”,也就是 Who(谁)、What(什么)、Where(哪里)、When(何时)Why(为什么)、How(如何)。举个例子,假设面试官让你描述咖啡机的面向对象设计。这个问题看似简单明了,其实不然。这台咖啡机可能是一款工业型机器,设计用来放在大餐厅里,每小时要服务几百位顾客,还要能制作 10 种不同口味的咖啡。又或者,它可能是设计给老年人使用的简易咖啡机,只要能制作简单的黑咖啡就行。这些用例将大大影响你的设计。 ## 2. 定义核心对象 比如,假设要为一家餐馆进行面向对象设计。那么,核心对象可能包括员工、服务员、厨师、餐桌、顾客、订单、菜。 ## 3. 分析对象关系 可以利用 UML 类图来分析。其中, - 服务员和厨师都继承自员工类; - 一个服务员要为多个餐桌的顾客提供服务; - 一个餐桌可以有很多顾客; - 一个餐桌可以有一个订单; - 一个订单有多个菜。 - 一个厨师要处理多个订单; ![](index_files/97c1d566-ae3a-4ddd-ba86-e8378e26d49b.png) ## 4. 研究对象的动作 可以用时序图来找出对象的动作。 多个顾客坐到一个餐桌,服务员来到餐桌前提供服务,并等待顾客填写订单。订单填写完成后,交给厨师处理订单,并等待厨师处理完成,完成之后服务员将上菜。 ![](index_files/4f901988-481c-48bf-a313-35778e2211bc.png) # 示例 ## 1. 聊天服务器 题目描述:请描述该如何设计一个聊天服务器。要求给出各种后台组件、类和方法的细节,并说明其中最难解决的问题会是什么。 设计聊天服务器是项大工程,绝非一次面试就能完成。毕竟,就算一整个团队,也要花费数月乃至好几年才能打造出一个聊天服务器。作为求职者,你的工作是专注解决该问题的某个方面,涉及范围要够广,又要够集中,这样才能在一轮面试中搞定。它不一定要与真实情况一模一样,但也应该忠实反映出实际的实现。 这里我们会把注意力放在用户管理和对话等核心功能:添加用户、创建对话、更新状态,等等。考虑到时间和空间有限,我们不会探讨这个问题的联网部分,也不描述数据是怎么真正推送到客户端的。 另外,我们假设“好友关系”是双向的,如果你是我的联系人之一,那就表示我也是你的联系人之一。我们的聊天系统将支持群组聊天和一对一(私密)聊天,但并不考虑语音聊天、视频聊天或文件传输。 ### 1.1 需要支持的特定动作 这也有待你跟面试官探讨,下面列出几点想法。 - 显示在线和离线状态。 - 添加请求(发送、接受、拒绝)。 - 更新状态信息。 - 发起私聊和群聊。 - 在私聊和群聊中添加新信息。 这只是一部分列表,如果时间有富余,还可以多加一些动作。 ### 1.2 核心组件 这个系统可能由一个数据库、一组客户端和一组服务器组成。我们的面向对象设计不会包含这些部分,不过可以讨论一下系统的整体概览。 数据库将用来存放更持久的数据,比如用户列表或聊天对话的备份。 SQL 数据库应该是不错的选择,或者,如果可扩展性要求更高,可以选用 BigTable 或其他类似的系统。 对于客户端和服务器之间的通信,使用 XML 应该也不错。尽管这种格式不是最紧凑的(你也应该向面试官指出这一点),它仍是很不错的选择,因为不管是计算机还是人类都容易辨识。使用 XML 可以让程序调试起来更轻松,这一点非常重要。 服务器由一组机器组成,数据会分散到各台机器上,这样一来,我们可能就必须从一台机器跳到另一台机器。如果可能的话,我们会尽量在所有机器上复制部分数据,以减少查询操作的次数。在此,设计上有个重要的限制条件,就是必须防止出现单点故障。例如,如果一台机器控制所有用户的登录,那么,只要这一台机器断网,就会造成数以百万计的用户无法登录。 ### 1.3 关键对象和方法 代码参考:[Github](https://github.com/careercup/ctci/tree/master/java/Chapter%208/Question8_7) 系统的关键对象包括用户、对话和状态消息等,我们已经实现了 UserManagement 类。要是更关注这个问题的联网方面或其他组件,我们就可能转而深入探究那些对象。 ```java public class UserManager { private static UserManager instance; private HashMap usersById = new HashMap(); private HashMap usersByAccountName = new HashMap(); private HashMap onlineUsers = new HashMap(); public static UserManager getInstance() { if (instance == null) { instance = new UserManager(); } return instance; } public void addUser(User fromUser, String toAccountName) { User toUser = usersByAccountName.get(toAccountName); AddRequest req = new AddRequest(fromUser, toUser, new Date()); toUser.receivedAddRequest(req); fromUser.sentAddRequest(req); } public void approveAddRequest(AddRequest req) { req.status = RequestStatus.Accepted; User from = req.getFromUser(); User to = req.getToUser(); from.addContact(to); to.addContact(from); } public void rejectAddRequest(AddRequest req) { req.status = RequestStatus.Rejected; User from = req.getFromUser(); User to = req.getToUser(); from.removeAddRequest(req); to.removeAddRequest(req); } public void userSignedOn(String accountName) { User user = usersByAccountName.get(accountName); if (user != null) { user.setStatus(new UserStatus(UserStatusType.Available, "")); onlineUsers.put(user.getId(), user); } } public void userSignedOff(String accountName) { User user = usersByAccountName.get(accountName); if (user != null) { user.setStatus(new UserStatus(UserStatusType.Offline, "")); onlineUsers.remove(user.getId()); } } } ``` 在 User 类中, receivedAddRequest 方法会通知用户 B(User B),用户 A(User A)请求加他 为 好 友 。 用 户 B 会 接 受 或 拒 绝 该 请 求 ( 通 过 UserManager.approveAddRequest 或 rejectAddRequest), UserManager 则负责将用户互相添加到对方的通讯录中。 当 UserManager 要将 AddRequest 加入用户 A 的请求列表时,会调用 User 类的 sentAddRequest 方法。综上,整个流程如下。 1. 用户 A 点击客户端软件上的“添加用户”,发送给服务器。 2. 用户 A 调用 requestAddUser(User B)。 3. 步骤 2 的方法会调用 UserManager.addUser。 4. UserManager 会调用 User A.sentAddRequest 和 User B.receivedAddRequest。 ```java public class User { private int id; private UserStatus status = null; private HashMap privateChats = new HashMap(); private ArrayList groupChats = new ArrayList(); private HashMap receivedAddRequests = new HashMap(); private HashMap sentAddRequests = new HashMap(); private HashMap contacts = new HashMap(); private String accountName; private String fullName; public User(int id, String accountName, String fullName) { this.accountName = accountName; this.fullName = fullName; this.id = id; } public boolean sendMessageToUser(User toUser, String content) { PrivateChat chat = privateChats.get(toUser.getId()); if (chat == null) { chat = new PrivateChat(this, toUser); privateChats.put(toUser.getId(), chat); } Message message = new Message(content, new Date()); return chat.addMessage(message); } public boolean sendMessageToGroupChat(int groupId, String content) { GroupChat chat = groupChats.get(groupId); if (chat != null) { Message message = new Message(content, new Date()); return chat.addMessage(message); } return false; } public void setStatus(UserStatus status) { this.status = status; } public UserStatus getStatus() { return status; } public boolean addContact(User user) { if (contacts.containsKey(user.getId())) { return false; } else { contacts.put(user.getId(), user); return true; } } public void receivedAddRequest(AddRequest req) { int senderId = req.getFromUser().getId(); if (!receivedAddRequests.containsKey(senderId)) { receivedAddRequests.put(senderId, req); } } public void sentAddRequest(AddRequest req) { int receiverId = req.getFromUser().getId(); if (!sentAddRequests.containsKey(receiverId)) { sentAddRequests.put(receiverId, req); } } public void removeAddRequest(AddRequest req) { if (req.getToUser() == this) { receivedAddRequests.remove(req); } else if (req.getFromUser() == this) { sentAddRequests.remove(req); } } public void requestAddUser(String accountName) { UserManager.getInstance().addUser(this, accountName); } public void addConversation(PrivateChat conversation) { User otherUser = conversation.getOtherParticipant(this); privateChats.put(otherUser.getId(), conversation); } public void addConversation(GroupChat conversation) { groupChats.add(conversation); } public int getId() { return id; } public String getAccountName() { return accountName; } public String getFullName() { return fullName; } } ``` Conversation 类实现为一个抽象类,因为所有 Conversation 不是 GroupChat 就是 PrivateChat,同时每个类各有自己的功能。 ```java public abstract class Conversation { protected ArrayList participants = new ArrayList(); protected int id; protected ArrayList messages = new ArrayList(); public ArrayList getMessages() { return messages; } public boolean addMessage(Message m) { messages.add(m); return true; } public int getId() { return id; } } ``` ```java public class GroupChat extends Conversation { public void removeParticipant(User user) { participants.remove(user); } public void addParticipant(User user) { participants.add(user); } } ``` ```java public class PrivateChat extends Conversation { public PrivateChat(User user1, User user2) { participants.add(user1); participants.add(user2); } public User getOtherParticipant(User primary) { if (participants.get(0) == primary) { return participants.get(1); } else if (participants.get(1) == primary) { return participants.get(0); } return null; } } ``` ```java public class Message { private String content; private Date date; public Message(String content, Date date) { this.content = content; this.date = date; } public String getContent() { return content; } public Date getDate() { return date; } } ``` AddRequest 和 UserStatus 两个类比较简单,功能不多,主要用来将数据聚合在一起,方便其他类使用。 ```java public class AddRequest { private User fromUser; private User toUser; private Date date; RequestStatus status; public AddRequest(User from, User to, Date date) { fromUser = from; toUser = to; this.date = date; status = RequestStatus.Unread; } public RequestStatus getStatus() { return status; } public User getFromUser() { return fromUser; } public User getToUser() { return toUser; } public Date getDate() { return date; } } ``` ```java public class UserStatus {  private String message;  private UserStatusType type;  public UserStatus(UserStatusType type, String message) {  this.type = type;  this.message = message;  }    public UserStatusType getStatusType() {  return type;  }    public String getMessage() {  return message;  } } ``` ```java public enum UserStatusType { Offline, Away, Idle, Available, Busy } ``` ```java public enum RequestStatus { Unread, Read, Accepted, Rejected } ``` ### 4. 最难解决或最有意思的问题 **问题 1:如何确定某人在线** 虽然希望用户在退出时通知我们,但即便如此也无法确切知道状态。例如,用户的网络连接可能断开了。为了确定用户何时退出,或许可以试着定期询问客户端,以确保它仍然在线。 **问题 2:如何处理冲突的信息** 部分信息存储在计算机内存中,部分则存储在数据库里。如果两者不同步有冲突,那会出什么问题?哪一部分是“正确的”? **问题 3:如何才能让服务器在任何负载下都能应付自如** 前面我们设计聊天服务器时并没怎么考虑可扩展性,但在实际场景中必须予以关注。我们需要将数据分散到多台服务器上,而这又要求我们更关注数据的不同步。 **问题 4:如何预防拒绝服务攻击** 客户端可以向我们推送数据——若它们试图向服务器发起拒绝服务(DOS)攻击,怎么办?该如何预防? # 参考资料 - GAYLELEAKMANNMCDOWELL. 程序员面试金典 [M]. 人民邮电出版社 , 2013.