26

有谁知道重构上帝对象的最佳方法?

它不像将其分解为许多较小的类那么简单,因为存在高度的方法耦合。如果我退出一种方法,我通常最终会退出所有其他方法。

4

4 回答 4

38

就像积木一样。您将需要耐心和稳定的手,否则您必须从头开始重新创建所有内容。这本身还不错 - 有时需要丢弃代码。

其他建议:

  • 抽出方法前想一想:这个方法对什么数据进行操作?它有什么责任?
  • 首先尝试维护上帝类的接口,并将调用委托给新提取的类。归根结底,上帝类应该是一个没有自己逻辑的纯门面。然后你可以为了方便而保留它或扔掉它并开始只使用新的类
  • 单元测试帮助:在提取每个方法之前为每个方法编写测试,以确保您不会破坏功能
于 2013-02-14T15:06:37.563 回答
15

我假设“上帝对象”是指一个巨大的类(以代码行衡量)。

基本思想是将其部分功能提取到其他类中。

为了找到那些你可以寻找的

  • 经常一起使用的字段/参数。他们可能会一起搬进一个新班级

  • 仅使用类中字段的一小部分子集的方法(或方法的一部分),可能会移动到仅包含这些字段的类中。

  • 原始类型(int、String、boolean)。他们在出来之前往往是真正有价值的对象。一旦它们成为价值对象,它们通常会吸引方法。

  • 看神物的用法。不同的客户使用不同的方法吗?这些可能会进入单独的界面。这些接口可能又具有单独的实现。

为了实际进行这些更改,您应该有一些基础设施和工具可供您使用:

  • 测试:准备好一组(可能生成的)详尽的测试,您可以经常运行这些测试。对未经测试的更改要格外小心。我做了这些,但将它们限制在提取方法之类的东西上,我可以用一个 IDE 操作完全完成。

  • 版本控制:您希望有一个版本控制,允许您每 2 分钟提交一次,而不会真正减慢您的速度。SVN 并没有真正起作用。吉特可以。

  • 天皇法:天皇法的理念是尝试改变。如果效果很好。如果不注意发生了什么问题,请将它们作为依赖项添加到您开始的更改中。回滚您的更改。在结果图中,使用尚无依赖关系的节点重复该过程。http://mikadommethod.wordpress.com/book/

于 2013-03-18T11:22:23.973 回答
2

根据 Lanza 和 Marinescu 的《实践中的面向对象度量》一书,God Class 设计缺陷是指倾向于集中系统智能的类。一个上帝类自己完成了太多的工作,只将次要的细节委托给一组琐碎的类,并使用来自其他类的数据。

上帝类的检测基于三个主要特征:

  1. 它们直接或使用访问器方法大量访问其他更简单类的数据。
  2. 它们庞大而复杂
  3. 它们有很多非交流行为,即属于该类的方法之间的内聚度很低。

重构上帝类是一项复杂的任务,因为这种不和谐通常是在方法级别发生的其他不和谐的累积效应。因此,执行这样的重构需要关于类方法的额外和更细粒度的信息,有时甚至需要关于其继承上下文的信息。第一种方法是识别捆绑在一起的方法和属性的集群,并将这些岛提取到单独的类中。

《面向对象的再造模式》一书中的Split Up God Class 方法建议将God Class 的职责逐步重新分配给它的协作类或从God Class 中拉出的新类。

《Working Effectively with Legacy Code》一书介绍了一些技术,例如 Sprout Method、Sprout Class、Wrap Method,以便能够测试可用于支持 God Classes 重构的遗留系统。

我要做的是对上帝类中的方法进行分组,这些方法使用与输入或输出相同的类属性。之后,我会将类拆分为子类,其中每个子类将包含子组中的方法以及这些方法使用的属性。

这样,每个新类将更小且更连贯(这意味着它们的所有方法都将适用于相似的类属性)。此外,我们生成的每个新类的依赖性都会减少。在那之后,我们可以进一步减少这些依赖,因为我们现在可以更好地理解代码。

一般来说,我会说根据手头的情况有几种不同的方法。例如,假设您有一个名为“LoginManager”的上帝类,它验证用户信息,更新“OnlineUserService”以便将用户添加到在线用户列表中,并返回特定于登录的数据(例如欢迎屏幕和一次提供)给客户。

所以你的课程看起来像这样:

import java.util.ArrayList;
import java.util.List;

public class LoginManager {

public void handleLogin(String hashedUserId, String hashedUserPassword){
    String userId = decryptHashedString(hashedUserId);
    String userPassword = decryptHashedString(hashedUserPassword);

    if(!validateUser(userId, userPassword)){ return; }

    updateOnlineUserService(userId);
    sendCustomizedLoginMessage(userId);
    sendOneTimeOffer(userId);
}

public String decryptHashedString(String hashedString){
    String userId = "";
    //TODO Decrypt hashed string for 150 lines of code...
    return userId;
}

public boolean validateUser(String userId, String userPassword){
    //validate for 100 lines of code...
    
    List<String> userIdList = getUserIdList();
    
    if(!isUserIdValid(userId,userIdList)){return false;}
    
    if(!isPasswordCorrect(userId,userPassword)){return false;}
    
    return true;
}

private List<String> getUserIdList() {
    List<String> userIdList = new ArrayList<>();
    //TODO: Add implementation details
    return userIdList;
}

private boolean isPasswordCorrect(String userId, String userPassword) {
    boolean isValidated = false;
    //TODO: Add implementation details
    return isValidated;
}

private boolean isUserIdValid(String userId, List<String> userIdList) {
    boolean isValidated = false;
    //TODO: Add implementation details
    return isValidated;
}

public void updateOnlineUserService(String userId){
    //TODO updateOnlineUserService for 100 lines of code...
}

public void sendCustomizedLoginMessage(String userId){
    //TODO sendCustomizedLoginMessage for 50 lines of code...

}

public void sendOneTimeOffer(String userId){
    //TODO sendOneTimeOffer for 100 lines of code...
}}

现在我们可以看到这个类将是巨大而复杂的。它还不是书籍定义的上帝类,因为类字段现在在方法中普遍使用。但是为了论证,我们可以把它当成神类,开始重构。

解决方案之一是创建单独的小类,用作主类中的成员。您可以添加的另一件事可能是在不同的接口及其各自的类中分离不同的行为。通过使这些方法“私有”来隐藏类中的实现细节。并在主类中使用这些接口来进行竞标。

所以最后,RefactoredLoginManager 将如下所示:

public class RefactoredLoginManager {
    IDecryptHandler decryptHandler;
    IValidateHandler validateHandler;
    IOnlineUserServiceNotifier onlineUserServiceNotifier;
    IClientDataSender clientDataSender;

public void handleLogin(String hashedUserId, String hashedUserPassword){
        String userId = decryptHandler.decryptHashedString(hashedUserId);
        String userPassword = decryptHandler.decryptHashedString(hashedUserPassword);

        if(!validateHandler.validateUser(userId, userPassword)){ return; }

        onlineUserServiceNotifier.updateOnlineUserService(userId);

        clientDataSender.sendCustomizedLoginMessage(userId);
        clientDataSender.sendOneTimeOffer(userId);
    }
}

解密处理程序:

public class DecryptHandler implements IDecryptHandler {

    public String decryptHashedString(String hashedString){
        String userId = "";
        //TODO Decrypt hashed string for 150 lines of code...
        return userId;
    }

}
 
public interface IDecryptHandler {

    String decryptHashedString(String hashedString);

}

验证处理程序:

public class ValidateHandler implements IValidateHandler {
    public boolean validateUser(String userId, String userPassword){
        //validate for 100 lines of code...

        List<String> userIdList = getUserIdList();

        if(!isUserIdValid(userId,userIdList)){return false;}

        if(!isPasswordCorrect(userId,userPassword)){return false;}

        return true;
    }

    private List<String> getUserIdList() {
        List<String> userIdList = new ArrayList<>();
        //TODO: Add implementation details
        return userIdList;
    }

    private boolean isPasswordCorrect(String userId, String userPassword) 
    {
        boolean isValidated = false;
        //TODO: Add implementation details
        return isValidated;
    }

    private boolean isUserIdValid(String userId, List<String> userIdList) 
    {
        boolean isValidated = false;
        //TODO: Add implementation details
        return isValidated;
    }

}

这里要注意的重要一点是,interfaces() 只需要包含其他类使用的方法。所以 IValidateHandler 看起来就像这样简单:

public interface IValidateHandler {

    boolean validateUser(String userId, String userPassword);

}

OnlineUserServiceNotifier:

public class OnlineUserServiceNotifier implements 
    IOnlineUserServiceNotifier {

    public void updateOnlineUserService(String userId){
        //TODO updateOnlineUserService for 100 lines of code...
    }

}
 
public interface IOnlineUserServiceNotifier {
    void updateOnlineUserService(String userId);
}

客户端数据发送器:

public class ClientDataSender implements IClientDataSender {

    public void sendCustomizedLoginMessage(String userId){
        //TODO sendCustomizedLoginMessage for 50 lines of code...

    }

    public void sendOneTimeOffer(String userId){
        //TODO sendOneTimeOffer for 100 lines of code...
    }
}

由于这两种方法都在 LoginHandler 中访问,因此接口必须包含这两种方法:

public interface IClientDataSender {
    void sendCustomizedLoginMessage(String userId);

    void sendOneTimeOffer(String userId);
}
于 2021-01-03T16:26:52.363 回答
1

这里实际上有两个主题:

  • 给定一个上帝类,如何将其成员合理划分为子集?基本思想是通过概念一致性(通常通过客户端模块中的频繁共同使用)和强制依赖来对元素进行分组。显然,这方面的细节是特定于要重构的系统的。结果是上帝类元素的所需分区(一组组)。

  • 给定所需的分区,实际进行更改。如果代码库有任何规模,这很困难。手动执行此操作,您几乎被迫保留 God 类,同时修改其访问器以调用由分区形成的新类。当然,您需要测试、测试、测试,因为手动进行这些更改时很容易出错。当对 God 类的所有访问权限都消失时,您终于可以将其删除。这在理论上听起来不错,但如果您面对数千个编译单元,则在实践中需要很长时间,并且您必须让团队成员在您执行此操作时停止添加对上帝界面的访问。但是,可以应用自动重构工具来实现这一点;使用这样的工具,您可以指定该工具的分区,然后它会以可靠的方式修改代码库。重构 C++ God Classes,并已被用于跨具有 3,000 个编译单元的系统进行此类更改。

于 2019-05-10T16:46:15.890 回答