罗伯特·C·马丁(Robert C. Martin)在他的《清洁代码》一书中指出,函数只能做一件事,而不能混用不同级别的抽象。因此,我开始怀疑下面的代码是否满足这些要求(我认为答案是否定的)。以下哪一个代码片段更好?

片段1

public void run() {
    isRunning = true;

    try {
        serverSocket = new ServerSocket(port);
    }
    catch (IOException | IllegalArgumentException e) {
        System.out.println("Could not bind socket to given port number.");
        System.out.println(e);
        System.exit(1);
    }

    while(isRunning) {
        Socket clientSocket = null;

        try {
            clientSocket = serverSocket.accept();
        }
        catch (IOException e) {
            logEvent("Could not accept incoming connection.");
        }

        Client client = new Client(clientSocket, this);
        connectedClients.put(client.getID(), client);
        Thread clientThread = new Thread(client);
        clientThread.start();
        logEvent("Accepted new connection from " + clientSocket.getRemoteSocketAddress().toString());
        client.send("@SERVER:HELLO");
    }
}


片段2

public void run() {
    isRunning = true;

    createServerSocket();

    while(isRunning) {
        Socket clientSocket = null;
        clientSocket = acceptConnection();
        addClient(clientSocket);
    }
}

private void createServerSocket() {
    try {
        serverSocket = new ServerSocket(port);
    }
    catch (IOException | IllegalArgumentException e) {
        System.out.println("Could not bind socket to given port number.");
        System.out.println(e);
        System.exit(1);
    }
}

private Socket acceptConnection() {
    try {
        Socket clientSocket = serverSocket.accept();
    }
    catch (IOException e) {
        logEvent("Could not accept incoming connection.");
    }
    return clientSocket;
}

private void addClient(Socket clientSocket) {
    Client client = new Client(clientSocket, this);
    connectedClients.put(client.getID(), client);
    Thread clientThread = new Thread(client);
    clientThread.start();
    logEvent("Accepted new connection from " + clientSocket.getRemoteSocketAddress().toString());
    client.send("@SERVER:HELLO");
}


但是,将这样的简单代码分成多个片段似乎是一个过大的选择(尽管它更具可读性-是吗?)。此外,createServerSockets有副作用(Sys.exit),而addClient一次执行太多操作。有什么建议吗?

#1 楼

第二个更容易阅读,它表达了开发人员的意图。乍一看,我(例如,作为维护人员或同一团队中的另一个开发人员)只想快速了解代码,而不关心细节。 (例如,代码是如何创建服务器套接字的。)第二个代码正好说明了这一点。

可以消除System.exit的副作用:抛出带有适当消息的异常(并且不要忘记确定原因)。然后使用run方法处理异常。我经常看到如下包装纸:

public void run() {
    try {
        doRun();
    } catch (SomeException e) {
        // exception handling here
    }
}


这通常使doRun()更简单易读。

其他一些注意事项:




Socket clientSocket = null;
clientSocket = acceptConnection();


可以写为

Socket clientSocket = acceptConnection();


I' d修改createServerSocket()方法以返回服务器套接字,并修改acceptConnection()方法以使其具有ServerSocket参数。它将消除两种方法之间的时间耦合。不可能以错误的顺序调用它们。
我最近没有编写任何线程代码,但是我认为为每个客户创建一个单独的线程不会很好。您可能要在此处使用执行程序/线程池。


评论


\ $ \ begingroup \ $
谢谢-我喜欢您提出的代码。现在看来已经很明显了,但是我之前并没有提出这个想法。我想我也应该将addClient拆分为更多方法。
\ $ \ endgroup \ $
–cafe_
2014年1月26日17:13

\ $ \ begingroup \ $
@ prometh07:是的,很好。我将从registerNewClient()和sendInitialMessage()方法开始。
\ $ \ endgroup \ $
–palacsint
2014年1月26日17:20

\ $ \ begingroup \ $
由于clientSocket不能在其他任何地方使用(即,变量的范围如此之短),所以我什至不理会它。为什么不只是addClient(acceptConnection())?
\ $ \ endgroup \ $
–约书亚·泰勒(Joshua Taylor)
2014年1月27日,0:01

\ $ \ begingroup \ $
1)是的。 2)嗯... serverSocket是成员变量。它不在run()方法外部使用,因此您(和rolfl)就在这里。 3)该代码仅用于教育目的,因此我怀疑一次可以有5个以上的用户(这是一个聊天应用程序-我将在稍后的主要帖子中对其进行更精确的描述)。
\ $ \ endgroup \ $
–cafe_
14年1月27日,0:07

\ $ \ begingroup \ $
@JoshuaTaylor:我也很好。两个可能的缺点:读者不知道acceptConnection()的返回类型;在调试时使用step in会有点困难。
\ $ \ endgroup \ $
–palacsint
2014年1月27日在8:53

#2 楼

您对new ServerSocket(port)的异常处理可能会更好。我不喜欢将System.exit(1)埋在辅助函数中的方式。捕获异常的方式应使其绕过clientSocket -accepting循环。类似地,如果serverSocket.accept()抛出异常,则将其记录下来,但是无论如何会继续尝试创建客户端线程。相反,创建客户端线程的代码应位于try块内,以便该异常将导致其被跳过。

这是修复异常处理的初步重新安排。

public void run() {    
    try {
            serverSocket = new ServerSocket(port);

            isRunning = true;
            while (isRunning) {
                    try {
                            Socket clientSocket = serverSocket.accept();
                            Client client = new Client(clientSocket, this);
                            connectedClients.put(client.getID(), client);
                            Thread clientThread = new Thread(client);
                            clientThread.start();
                            logEvent("Accepted new connection from " + clientSocket.getRemoteSocketAddress().toString());
                            // The "hello" should be sent by the clientThread code instead
                            // client.send("@SERVER:HELLO");
                    } catch (IOException e) {
                            logEvent("Could not accept incoming connection.");
                    }
            }
    } catch (IOException | IllegalArgumentException e) {
            System.out.println("Could not bind socket to given port number.");
            System.out.println(e);
            System.exit(1);
    }
}


如果您想减轻这种方法的负担,可以创建“客户端任务”以放入“客户端处理池”中。那么此run()方法可能是非常通用的。

如上所述,client.send("@SERVER:HELLO")属于clientThread代码,而不属于此循环。

#3 楼

我不喜欢任何一种解决方案(或当前建议的答案)。在运行ServerSocket的任何应用程序中,我相信套接字管理都应该在Main线程上完成。这样可以避免出现System.exit(1)的确切情况。服务器应用程序的基本模式是:

public static void main(String[] args) {
    // sort out the arguments, if any.
    int port = 12345; // default, args can override

    // open the ServerSocket to ensure you can reserve the port befor you do expensive setup.
    try (ServerSocket serverSocket = createServerSocket(port)) {
        // Initialize the system
        setup();

        listenForClients(serverSocket);

    } catch (IOException ioe) {
        // deal with exception
        // can do System.exit(1) in the main thread to set exit codes...
        // but my personal preference is to throw an exception from the main method.
        // whihc exits with 1, and still does a clean shutdown.
    }  
}


ClientThread设置有些错误。如果希望客户机线程在主线程停止时死亡,则应将它们设置为守护程序线程:

Thread clientThread = new Thread(client);
clientThread.setDaemon(true);


当所有非守护程序都终止时,Java系统将终止线程停止。如果您唯一的非守护程序线程是主线程(它始终是非守护程序),那么当主线程结束时系统将干净地退出。方式...包括具有终止变量(例如AtomicBoolean isRunning = new AtomicBoolean(true)),任何线程都可以将其设置为false,主线程将定期检查...(使用Selector和ServerSocketChannel-一个高级概念)

底线是您没有正确设置Daemon线程,因此,需要重量级的System.exit(1)。您的ServerSocket管理也应该在主线程上(或者可以通过从主线程派生它,并让主线程正常退出,使其在唯一的非守护进程线程上进行管理。)

评论


\ $ \ begingroup \ $
我将在以后添加GUI,所以我认为我需要这个单独的线程。
\ $ \ endgroup \ $
–cafe_
2014年1月26日19:34