403 lines
15 KiB
Plaintext
403 lines
15 KiB
Plaintext
[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<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. |