代码设计最佳实践
1. 线程安全与依赖注入
在多线程环境中,保证对象的安全发布和安全连接是至关重要的。
1.1 安全发布
在多线程编程中,仅保证操作顺序是不足以实现安全发布的,这会引发可见性问题。例如,线程A更新了哈希表,但线程B可能看到的是
null
值。这是因为在缺乏足够同步的情况下,JVM 不保证线程间字段的可见性。
以下是一个不安全发布的示例:
public class MoreUnsafePublication {
private EmailDatabase service;
public MoreUnsafePublication(EmailDatabase service) {
this.service = service;
}
public void read() {
System.out.println("Dhanji's email address really is "
+ service.get("Dhanji"));
}
}
在这个例子中,由于缺乏同步,调用
read()
方法的线程可能无法保证
service
依赖的可用性,容易导致
NullPointerException
。
解决这个问题的一个简单方法是将字段声明为
final
。
final
字段能保证对所有相关线程的安全可见性,因为这些字段通常在构造函数中设置,一旦构造函数完成,它们对所有线程都是可见的。
以下是安全发布的示例:
public class SafePublication {
private final EmailDatabase service;
public SafePublication(EmailDatabase service) {
this.service = service;
}
public void read() {
System.out.println("Dhanji's email address really is "
+ service.get("Dhanji"));
}
}
即使你认为实例只会在一个线程中使用,声明字段为
final
也是一个好习惯,因为这能明确表达意图。
1.2 安全连接
线程间的可见性问题通常在单例模式中出现,因为只有单例才会被两个或更多线程共享。如果能保证对象引用在构造过程中不逸出,
final
字段是解决这个问题的安全方案。
许多依赖注入器可以在单例构造过程中提供额外的同步帮助。例如,PicoContainer 可以设置为锁定模式,确保对象创建在同步状态下进行:
MutablePicoContainer injector = new DefaultPicoContainer();
injector.as(LOCK, CACHE).addComponent(MyObject.class);
MyObject obj1 = injector.getComponent(MyObject.class);
MyObject obj2 = injector.getComponent(MyObject.class);
这相当于以下代码:
public class SynchronizedPublication {
private Lock lock = new ReentrantLock();
private MyObject obj;
public void constructObj() {
lock.lock();
try {
obj = new MyObject();
} finally {
lock.unlock();
}
//other threads can safely access obj's fields now
}
}
Guice 也为其所有单例提供了类似的保证。然而,我们不应仅仅依赖注入器来实现安全连接,设计类时应考虑在多线程环境中表现良好,这样无论是否有库的帮助,代码都是安全的。
以下是一个尝试修改
final
字段的示例,会导致编译失败:
public class EarlyWarning {
private final ImmutableDependency dep;
public EarlyWarning(ImmutableDependency dep) {
this.dep = dep;
}
public void setImmutableDependency(ImmutableDependency newDep) {
this.dep = newDep;
}
}
编译结果:
Information:Compilation completed with 1 error and 0 warnings
Information:1 error
Information:0 warnings
EarlyWarning.java
Error:Error:line (9)cannot assign a value to final variable dep
使用
@Immutable
、
@ThreadSafe
、
@NotThreadSafe
和
@GuardedBy
等线程安全注解可以进一步明确行为意图。
2. 对象与设计
在决定哪些类的对象应该使用依赖注入进行管理时,这是一个棘手的问题。对于新手来说,过度使用或谨慎使用依赖注入都不是明智的选择。
2.1 数据与服务
以一个虚构的在线拍卖行为例,有三个主要服务:
-
AuctionManager
:管理投标和拍卖状态
-
ItemManager
:管理列表项和描述
-
UserManager
:管理用户账户和历史
以下是
UserManager
的示例代码:
public class UserManager {
private final UserDao userDao;
public UserManager(UserDao userDao) {
this.userDao = userDao;
}
public void createNewUser(User user) {
validate(user);
userDao.save(user);
}
public void updatePassword(long userId,
String password) {
User user = userDao.read(userId);
user.setPassword(password);
validate(user);
userDao.save(user);
}
public void deactivate(long userId) {
User user = userDao.read(userId);
user.deactivate();
userDao.save(user);
}
...
}
UserManager
依赖于
UserDao
来读写用户数据。而
User
类是一个数据模型类:
public class User {
private long userId;
private String name;
private String password;
private boolean active = true;
public User(long userId, String name) {
this.userId = userId;
this.name = name;
}
public long getUserId() {
return userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isActive() {
return active;
}
public void deactivate() {
this.active = false;
}
}
User
类的字段大多是可变的,且没有真正的依赖结构,因此不适合通过依赖注入来实例化,更适合手动创建和管理。
以下是一个新用户注册页面的示例:
@RequestScoped
public class NewUserPage {
private final UserManager userManager;
@Inject
public NewUserPage(UserManager userManager) {
this.userManager = userManager;
}
public HTML registerNewUser(String name) {
userManager.createNewUser(new User(nextUserId(), name));
...
}
}
2.2 更好的封装
封装是面向对象编程的核心原则之一,它通过将组件内的信息隐藏,避免语义的意外泄露。
以下是一个消息服务的示例:
public interface Messager {
void send(Message msg);
}
public class Emailer implements Messager {
public void send(Message msg) {
//send message via email...
}
}
如果不小心泄露了抽象,可能会导致问题。例如:
public interface Messager {
void send(EmailMessage msg);
}
public class Emailer implements Messager {
public void send(EmailMessage msg) {
//send message via email...
}
}
这会导致新的消息服务(如使用 Jabber 协议的服务)无法正确实现接口。
正确的做法是恢复原始实现:
public interface Messager {
void send(Message msg);
}
public class Emailer implements Messager {
public void send(Message msg) {
EmailMessage email = convert(msg);
//send message via email...
}
}
这样就可以轻松创建和替换不同的消息服务。
在 Java 中,可以利用包私有访问来进一步封装实现细节。例如:
package example.messaging;
public interface Messager {
void send(Message msg);
}
class JabberMessager implements Messager {
private final JabberTransport transport;
private final JabberMessageConverter converter;
public void send(Message msg) {
...
}
}
class JabberTransport {
...
}
class JabberMessageConverter {
...
}
通过这种方式,只有服务接口和配置被暴露,实现细节被隐藏,减少了紧密耦合的可能性。
以下是使用 Guice 配置消息服务的示例:
public class MessagingModule extends AbstractModule {
@Override
protected void configure() {
bind(Messager.class).to(JabberMessager.class);
}
}
客户端可以通过接口使用消息服务:
public class MessageClient {
private final Messager messager;
public MessageClient(Messager messager) {
this.messager = messager;
}
public void go() {
messager.send(new Message("Dhanji", "Hi there!"));
}
}
总结
在多线程编程和依赖注入中,我们需要注意对象的安全发布和安全连接,合理选择使用依赖注入的对象,并利用封装原则减少代码的耦合度。通过这些最佳实践,可以提高代码的安全性和可维护性。
流程图
graph LR
A[开始] --> B[对象创建]
B --> C{是否单例}
C -- 是 --> D[考虑安全连接]
C -- 否 --> E[正常创建]
D --> F{是否使用依赖注入}
F -- 是 --> G[使用注入器同步]
F -- 否 --> H[手动同步]
G --> I[对象使用]
H --> I
E --> I
I --> J[结束]
表格
| 概念 | 说明 | 示例 |
|---|---|---|
| 安全发布 | 确保对象对所有线程可见 |
SafePublication
类
|
| 安全连接 | 解决单例线程可见性问题 | PicoContainer 锁定模式 |
| 数据与服务 | 区分数据类和服务类 |
User
类和
UserManager
类
|
| 封装 | 隐藏实现细节,减少耦合 | 消息服务示例 |
代码设计最佳实践(续)
3. 依赖注入的使用决策
在实际开发中,决定何时使用依赖注入是一个具有挑战性的问题。过度使用或谨慎使用都可能带来问题,尤其是在大型和复杂的代码库中。
一般来说,可以遵循以下经验法则:
-
服务或动作组件
:将这些组件交给依赖注入器管理,因为它们通常具有复杂的依赖关系,使用依赖注入可以提高代码的灵活性和可测试性。
-
数据模型类
:对于那些没有复杂依赖关系,且主要用于存储数据的类,建议手动创建和管理,因为它们通常不需要依赖注入的特性。
例如,在前面提到的在线拍卖系统中,
AuctionManager
、
ItemManager
和
UserManager
是服务类,适合使用依赖注入;而
User
类是数据模型类,适合手动创建。
4. 封装的进一步应用
封装不仅可以用于隐藏实现细节,还可以用于控制组件的访问权限和行为。
4.1 包私有访问的优势
在 Java 中,包私有访问(即没有访问修饰符)可以限制类的可见性,只有同一个包内的类才能访问这些类。这种访问控制可以有效地隐藏实现细节,减少外部代码对内部实现的依赖。
例如,在消息服务的示例中,将
JabberMessager
、
JabberTransport
和
JabberMessageConverter
类声明为包私有:
package example.messaging;
public interface Messager {
void send(Message msg);
}
class JabberMessager implements Messager {
private final JabberTransport transport;
private final JabberMessageConverter converter;
public void send(Message msg) {
...
}
}
class JabberTransport {
...
}
class JabberMessageConverter {
...
}
这样,外部代码只能通过
Messager
接口来使用消息服务,而无法直接访问
JabberMessager
的实现细节。
4.2 使用依赖注入器进行封装
依赖注入器可以帮助我们更好地利用包私有访问。通过在每个包中暴露服务接口和配置,隐藏实现细节,我们可以实现更高层次的封装。
例如,使用 Guice 配置消息服务:
public class MessagingModule extends AbstractModule {
@Override
protected void configure() {
bind(Messager.class).to(JabberMessager.class);
}
}
客户端代码只需要依赖
Messager
接口,而不需要关心具体的实现类:
public class MessageClient {
private final Messager messager;
public MessageClient(Messager messager) {
this.messager = messager;
}
public void go() {
messager.send(new Message("Dhanji", "Hi there!"));
}
}
这种方式使得客户端代码与消息服务的实现细节解耦,提高了代码的可维护性和可扩展性。
5. 线程安全注解的作用
线程安全注解如
@Immutable
、
@ThreadSafe
、
@NotThreadSafe
和
@GuardedBy
可以帮助开发者更好地理解和维护代码。
-
@Immutable:表示类是不可变的,即一旦创建,其状态不能被修改。这种类通常是线程安全的,因为它们不需要同步机制。 -
@ThreadSafe:表示类在多线程环境下是安全的,可以被多个线程同时访问而不会出现问题。 -
@NotThreadSafe:表示类在多线程环境下是不安全的,需要额外的同步机制来保证线程安全。 -
@GuardedBy:表示某个字段或方法需要通过指定的锁来保护,以确保线程安全。
例如,以下是一个使用
@ThreadSafe
注解的示例:
@ThreadSafe
public class ThreadSafeCounter {
private final AtomicInteger counter = new AtomicInteger(0);
public int incrementAndGet() {
return counter.incrementAndGet();
}
}
这个注解向其他开发者表明
ThreadSafeCounter
类在多线程环境下是安全的。
6. 总结与建议
在代码设计中,我们应该遵循以下原则:
-
线程安全
:确保对象的安全发布和安全连接,使用
final
字段和线程安全注解来提高代码的线程安全性。
-
依赖注入
:合理选择使用依赖注入的对象,将服务和动作组件交给依赖注入器管理,手动创建和管理数据模型类。
-
封装
:利用封装原则隐藏实现细节,减少代码的耦合度,使用包私有访问和依赖注入器来实现更高层次的封装。
通过遵循这些原则,可以提高代码的安全性、可维护性和可扩展性。
流程图
graph LR
A[代码设计] --> B[线程安全]
A --> C[依赖注入]
A --> D[封装]
B --> E[安全发布]
B --> F[安全连接]
C --> G[服务组件]
C --> H[数据模型类]
D --> I[包私有访问]
D --> J[依赖注入器封装]
E --> K[使用final字段]
F --> L[注入器同步]
G --> M[提高灵活性]
H --> N[手动创建]
I --> O[隐藏实现细节]
J --> P[解耦代码]
表格
| 原则 | 说明 | 示例 |
|---|---|---|
| 线程安全 | 确保对象在多线程环境下的安全 |
ThreadSafeCounter
类
|
| 依赖注入 | 合理使用依赖注入提高代码质量 | 在线拍卖系统中的服务类和数据类 |
| 封装 | 隐藏实现细节,减少耦合 | 消息服务的包私有访问 |
超级会员免费看

被折叠的 条评论
为什么被折叠?



