403 lines
15 KiB
Plaintext
403 lines
15 KiB
Plaintext
![]() |
[TOC]
|
|||
|
|
|||
|
# 解决思路
|
|||
|
|
|||
|
面向对象设计问题要求求职者设计出类和方法,以实现技术问题或描述真实生活中的对象。可以按照以下步骤来进行解决:
|
|||
|
|
|||
|
## 1. 处理不明确的地方
|
|||
|
|
|||
|
面向对象设计问题往往会故意放些烟幕弹,意在检验你是武断臆测,还是提出问题以厘清问题。毕竟,开发人员要是没弄清楚自己要开发什么,就直接挽起袖子开始编码,只会浪费公司的财力物力,还可能造成更严重的后果。
|
|||
|
|
|||
|
碰到面向对象设计问题时,你应该先问清楚,谁是使用者、他们将如何使用。对某些问题,你甚至还要问清楚“5W1H”,也就是 Who(谁)、What(什么)、Where(哪里)、When(何时)Why(为什么)、How(如何)。举个例子,假设面试官让你描述咖啡机的面向对象设计。这个问题看似简单明了,其实不然。这台咖啡机可能是一款工业型机器,设计用来放在大餐厅里,每小时要服务几百位顾客,还要能制作 10 种不同口味的咖啡。又或者,它可能是设计给老年人使用的简易咖啡机,只要能制作简单的黑咖啡就行。这些用例将大大影响你的设计。
|
|||
|
|
|||
|
## 2. 定义核心对象
|
|||
|
|
|||
|
比如,假设要为一家餐馆进行面向对象设计。那么,核心对象可能包括员工、服务员、厨师、餐桌、顾客、订单、菜。
|
|||
|
|
|||
|
## 3. 分析对象关系
|
|||
|
|
|||
|
可以利用 UML 类图来分析。其中,
|
|||
|
|
|||
|
- 服务员和厨师都继承自员工类;
|
|||
|
- 一个服务员要为多个餐桌的顾客提供服务;
|
|||
|
- 一个餐桌可以有很多顾客;
|
|||
|
- 一个餐桌可以有一个订单;
|
|||
|
- 一个订单有多个菜。
|
|||
|
- 一个厨师要处理多个订单;
|
|||
|
|
|||
|

|
|||
|
|
|||
|
## 4. 研究对象的动作
|
|||
|
|
|||
|
可以用时序图来找出对象的动作。
|
|||
|
|
|||
|
多个顾客坐到一个餐桌,服务员来到餐桌前提供服务,并等待顾客填写订单。订单填写完成后,交给厨师处理订单,并等待厨师处理完成,完成之后服务员将上菜。
|
|||
|
|
|||
|

|
|||
|
|
|||
|
# 示例
|
|||
|
|
|||
|
## 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<Integer, User> usersById = new HashMap<Integer, User>();
|
|||
|
private HashMap<String, User> usersByAccountName = new HashMap<String, User>();
|
|||
|
private HashMap<Integer, User> onlineUsers = new HashMap<Integer, User>();
|
|||
|
|
|||
|
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<Integer, PrivateChat> privateChats = new HashMap<Integer, PrivateChat>();
|
|||
|
private ArrayList<GroupChat> groupChats = new ArrayList<GroupChat>();
|
|||
|
private HashMap<Integer, AddRequest> receivedAddRequests = new HashMap<Integer, AddRequest>();
|
|||
|
private HashMap<Integer, AddRequest> sentAddRequests = new HashMap<Integer, AddRequest>();
|
|||
|
|
|||
|
private HashMap<Integer, User> contacts = new HashMap<Integer, User>();
|
|||
|
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<User> participants = new ArrayList<User>();
|
|||
|
protected int id;
|
|||
|
protected ArrayList<Message> messages = new ArrayList<Message>();
|
|||
|
|
|||
|
public ArrayList<Message> 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.
|