25、代码设计最佳实践

代码设计最佳实践

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
依赖注入 合理使用依赖注入提高代码质量 在线拍卖系统中的服务类和数据类
封装 隐藏实现细节,减少耦合 消息服务的包私有访问
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值