Class 存储所有必需对象是正确的方法吗?

Class that stores all essential objects is the right way to go?

问题是面向对象编程中的设计问题,我正在尝试寻找或想到最佳解决方案, 并不确定我目前想到的解决方案。

问题:

我写了一个名为 ComponentsHub 的 class,它的想法是在其中存储对 运行 程序必不可少的所有主要对象。每个对象都是静态的、最终的和 public。我使用 public 访问修饰符来简化通过静态导入访问对象或仅静态访问对象的过程。

package com.rtransfer.net.components;

import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Logger;

import com.rtransfer.net.system.StorageManager;

public class ComponentsHub {

    public static final Logger logger;
    
    public static final Server server;
        
    public static final StorageManager storageManager;

    public static final SecurityManager securityManager;
    
    public static final RequestForwarder requestForwarder;
    
    public static final Authenticator authenticator;
    
    public static final Uploader uploader;

    public static final ConnectionHandler connectionHandler;

    public static final Listener listener;
    
    static {
        logger = Logger.getLogger("rtransfer.net");
        
        try {
            FileHandler handler = new FileHandler("logs/logs.txt");
            logger.addHandler(handler);
        } catch (SecurityException | IOException e) {
            e.printStackTrace();
        }       
        server = new Server();
            
        storageManager = new StorageManager();

        securityManager = new SecurityManager();
        
        requestForwarder = new RequestForwarder();
        
        authenticator = new Authenticator();
        
        uploader = new Uploader();

        connectionHandler = new ConnectionHandler();

        listener = new Listener();
    }
}

很明显这个 code/design 存在一些问题(希望你能告诉我其中的一些问题)。例如,不分离关注点、难以测试、不可扩展等等。我不太适应这种架构,我想我创造了某种“上帝Class”。

我想到的解决方案:

我知道这是设计面向对象系统时很常见的问题类型。我想解决这个问题,这样我就不必在软件的进一步开发中生活在噩梦中。

我提出的方案是否解决了部分问题?有其他选择吗?如果您能向我推荐合适的设计模式或其他能让我解决这个问题的技术,我会很高兴。我应该如何以及在何处创建对象并允许相当简单地访问它们?


评论和回答结果:

正如其中一条评论所建议的那样,我实现了 ServiceLocator,它看起来比之前的想法要好得多。现在知道是不是Anti-pattern的争论不休,也明白它的缺点是什么,但是如果没有相关的框架,会很困难很累(其中一个答案也有提到)使用传统的依赖注入。

public class InitialContext {
    
    public ServiceComponent lookup(String serviceName) {
        if (serviceName.equalsIgnoreCase(Listener.class.getSimpleName())) {
            return new Listener();
        } else if (serviceName.equalsIgnoreCase(ConnectionHandler.class.getSimpleName())) {
            return new ConnectionHandler();
        } else if (serviceName.equalsIgnoreCase(Uploader.class.getSimpleName())) {
            return new Uploader();
        } else if (serviceName.equalsIgnoreCase(Authenticator.class.getSimpleName())) {
            return new Authenticator();
        } else if (serviceName.equalsIgnoreCase(RequestForwarder.class.getSimpleName())) {
            return new RequestForwarder();
        } else if (serviceName.equalsIgnoreCase(SecurityManager.class.getSimpleName())) {
            return new SecurityManager();
        } else if (serviceName.equalsIgnoreCase(StorageManager.class.getSimpleName())) {
            return new StorageManager();
        } else if (serviceName.equalsIgnoreCase(Server.class.getSimpleName())) {
            return new Server();
        } else {
            return null;
        }
    }
}
public class ServiceCache {

    private Hashtable<String, ServiceComponent> services;
    
    public ServiceCache() {
        services = new Hashtable<>();
    }
    
    public ServiceComponent getService(Class<?> service) {
        return services.get(service.getSimpleName());
    }
    
    public void addService(ServiceComponent newService) {
        services.put(newService.getClass().getSimpleName(), newService);
    }
}
package com.rtransfer.net.components;

import com.rtransfer.utils.InitialContext;
import com.rtransfer.utils.ServiceCache;

public class ServiceLocator {

    private static ServiceCache cache = new ServiceCache();
    
    public static ServiceComponent getService(Class<?> serviceClass) {
        ServiceComponent service = cache.getService(serviceClass);
        
        if (service != null)
            return service;
        InitialContext context = new InitialContext();
        
        service = context.lookup(serviceClass.getSimpleName());
        cache.addService(service);
        
        return service;
    }
}

所以最后我决定使用依赖注入

无论您如何构造此单例对象,它仍然是一个单例(或单例集合)。它会在您的 class 之间产生意想不到的相互依赖关系,并使它们难以孤立地进行测试或推理。

放入搜索引擎的神奇短语是“依赖注入”。这个术语带有很多包袱,人们有时会把它与复杂的框架联系起来,但其核心原则非常简单:与其主动寻找依赖关系,每个 class 应该要求代码 使用 class 提供依赖项。

不是这个:

class Dog {
    void bark() {
        SoundManager.emitSound("woof");
    }
}

但是这个:

class Dog {
    private final SoundManager soundManager;

    public Dog(SoundManager soundManager) {
        this.soundManager = soundManager;
    }

    void bark() {
        this.soundManager.emitSound("woof");
    }
}

并且这个原则沿着依赖链上升:如果 Kennel 想要创建一个 Dog 对象,它还需要从 请求一个 SoundManager 来电者。

您可以想象传递所有这些构造函数参数会变得乏味,尤其是当您的代码库增长时。这就是框架有用的地方。

正如你所说,这在 OOP 中是一个很常见的问题,你提出的想法是个好主意,但实施起来很差,原因有很多,

你在这里试图做的是Context And Dependency Injection的概念。 您想要一个 Class 可供 供应 实例供您随时随地使用:

SrorageManager storageManager = ComponentsHub.getStorageManager();

但是你需要考虑一些重要的方面,从我的脑海中:

  1. 可变性
  2. 线程安全

当然还有 关注分离。 其中一些 Classes 可能受制于多个线程,有些可能是单例,有些可能只是实用程序 类.

所以我先求婚; 接口

分离:定义你的合同。


安全上下文

public interface SecurityContext {
    public SecurityManager getSecurityManager();
    public Authemticator getAuthenticator();
}

网络环境

public interface WebContext {
    public Uploader getUploader();
    public RequestForwarder getRequestForwarder();
    public Server getServer();
    .
    .
    .
}

也许还有数据或持久性上下文。


线程安全


如果您正在考虑能够支持多线程,您需要考虑一种控制和同步的方式,使用 java API 锁或使用 syncronized 关键字,或者也许隔离一些线程本地工作,具体取决于您的情况。


可变性


谁可以更改上下文的状态?上下文是创建为每个应用程序一个还是每个线程(请求)一个,例如数据库事务。

您是否希望所有内容都来自应用程序并且永远不会在运行时更改,例如,也许数据库凭据已更改,也许您想实施另一个身份验证器,那些 类 应该是动态的,而不是静态和上下文可以灵活地为您提供不同的实现。


为了总结我我认为你应该能够做的是:

  1. 在上下文之外的字段中创建 类 的 factories/suppliers/instances 让我们调用那些 dependencies
  2. 初始化上下文,使用构建器并注入所需的依赖项new PersistanceContextBuilder.withStorageManager(new FileSystemStorageManager).build();
  3. 想办法为应用程序的其余部分提供这些上下文,最好是创建同样分离并依赖于不同上下文的服务或控制器。

根据您的情况,您手头可能有也可能没有问题,可能 问题可以用“单例实例”这个总称来概括。

单例实例受到大量无端滥用。几乎整个编程社区都认为它们很糟糕。但是单身人士是一种完全有效的设计模式,从您构建问题的方式来看,我建议您继续使用它们。我的座右铭是,如果你想要能叫、能游、能飞的东西,但每个人都告诉你鸭子不好,谁在乎每个人怎么想!

单例是你的应用程序最多只能有一个的东西的体现。例如,一个主窗体或一组登录凭据 - 两者都是单例实例。您在所有应用程序中都需要单例。

但是,我建议您不要将所有单例放在一个 basket/class 中。除了违反单一职责原则外,随着越来越多的程序全局状态被放入此包罗万象中,它最终会变得庞大而笨拙。这通常是 Singletons 和 IMO 发生的事情,这就是为什么他们如此讨厌。

相反,将您的单例设计得好像它们不是。

  1. 为每个单例创建一个接口。
    这会让你 根据需要轻松替换单例的实现。
  2. 在靠近每个接口的地方有 setter/getter 个方法。
    这将允许您轻松地替换启动和访问每个单例的方式,也许稍后用服务管理器替换它。
  3. 确保每个接口都位于包含实现的项目的单独项目中。这将防止您意外地创建对单例的某些实现细节的依赖。

其他答案似乎提出了范围广泛的问题,其中大部分与如何容纳 Singleton 实例的设计选择完全无关。从简单开始的最好的部分是您可以在每个问题出现时对其进行处理。
有线程问题?将有问题的单身人士包裹在锁中。
它只是一个单例,因为它构建起来很昂贵?将其更改为对象池。
你想模拟依赖注入?更改访问器以使用线程局部变量。
您想同时登录多个用户吗?更改您的访问者以查看用户凭据。

您可以做的另一件事是在 class 中使用访问器方法,为界面中的每个函数创建静态版本。这些静态函数中的每一个都应该通过访问器方法执行, 例如

public class Database
{
   public static DatabaseInterface instance() { return instance_; }
   public static boolean connect()  {return instance().connect(); }
}

这样您就可以从代码中删除所有访问器调用。

永远记住,最重要的是代码的可读性。