你如何重构神班?

时间:2013-02-14 08:10:45

标签: class refactoring anti-patterns god-object

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

它不像将它分成许多较小的类那么简单,因为它有一个高方法耦合。如果我拿出一种方法,我通常会把所有其他方法拉出来。

4 个答案:

答案 0 :(得分:33)

就像Jenga。你需要耐心和稳定的手,否则你必须从头开始重新创造一切。这本身并不坏 - 有时需要丢掉代码。

其他建议:

  • 在拉出方法之前先想一想:这个方法运行的是什么数据?它有什么责任?
  • 首先尝试维护神类的接口,并将调用委托给新提取的类。最后,上帝阶级应该是一个没有自己逻辑的纯粹外观。然后你可以保留它以方便使用或丢弃它并开始只使用新类
  • 单元测试帮助:在提取每个方法之前为每个方法编写测试以确保不破坏功能

答案 1 :(得分:12)

我认为“上帝对象”意味着一个庞大的阶级(用代码行来衡量)。

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

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

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

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

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

  • 看看神对象的用法。是否有不同的客户使用不同的方法?那些可能会进入单独的界面。这些接口可能反过来又有单独的实现。

要实际执行这些更改,您应该拥有一些基础架构和工具:

  • 测试:准备好(可能生成的)详尽的测试集,以便您可以经常运行。如果没有测试你做的改变要非常小心。我这样做,但将它们限制为像抽取方法这样的东西,我可以用一个IDE动作完全完成。

  • 版本控制:您希望拥有一个允许您每2分钟提交一次的版本控制,而不会让您失望。 SVN并没有真正起作用。 Git确实。

  • Mikado方法:Mikado方法的想法是尝试改变。如果它工作得很好。如果没有注意到什么是破坏,请将它们作为依赖项添加到您开始的更改中。回滚你的变化。在结果图中,使用尚无依赖关系的节点重复该过程。 http://mikadomethod.wordpress.com/book/

答案 2 :(得分:1)

根据 Lanza 和 Marinescu 的“Object Oriented Metrics in Practice”一书,God Class 设计缺陷是指倾向于集中系统智能的类。 God Class 自己做太多工作,只将次要的细节委托给一组琐碎的类,并使用其他类的数据。

神级的检测基于三个主要特征:

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

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

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

“Working Effectively with Legacy Code”一书介绍了一些技术,例如 Sprout 方法、Sprout 类、Wrap 方法,以便能够测试可用于支持 God 类重构的遗留系统。

我要做的是对 God 类中的方法进行分组,这些方法使用相同的类属性作为输入或输出。之后,我会将类拆分为子类,每个子类将保存一个子组中的方法以及这些方法使用的属性。

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

总的来说,我会说根据手头的情况有几种不同的方法。例如,假设您有一个名为“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);

}

在线用户服务通知程序:

public class OnlineUserServiceNotifier implements 
    IOnlineUserServiceNotifier {

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

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

ClientDataSender:

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);
}

答案 3 :(得分:0)

这里确实有两个主题:

  • 给定一个上帝阶级,如何将其成员合理地划分为子集?基本思想是通过概念上的一致性(通常由客户端模块中频繁的共同使用来表示)和强制依赖性来对元素进行分组。显然,此操作的详细信息特定于重构的系统。结果是神类元素的理想划分(组的集合)。

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