How do you refactor a God class?
Asked Answered
B

4

29

Does anyone know the best way to refactor a God-object?

Its not as simple as breaking it into a number of smaller classes, because there is a high method coupling. If I pull out one method, i usually end up pulling every other method out.

Bombardier answered 14/2, 2013 at 8:10 Comment(0)
T
42

It's like Jenga. You will need patience and a steady hand, otherwise you have to recreate everything from scratch. Which is not bad, per se - sometimes one needs to throw away code.

Other advice:

  • Think before pulling out methods: on what data does this method operate? What responsibility does it have?
  • Try to maintain the interface of the god class at first and delegate calls to the new extracted classes. In the end the god class should be a pure facade without own logic. Then you can keep it for convenience or throw it away and start to use the new classes only
  • Unit Tests help: write tests for each method before extracting it to assure you don't break functionality
Toronto answered 14/2, 2013 at 15:6 Comment(3)
good advice on the unit testing. But i really need a way of untangling methods really. Thats the crux of the problemBombardier
that heavily depends on the case. There is no one-fits-all recipe, unfortunately (other than "use the appropiate refactoring patterns").Toronto
I like the idea of converting it into a facade. That helps keep the Jenga pieces supporting each other, but also keeps the refactoring moving forward.Hoskins
D
15

I assume "God Object" means a huge class (measured in lines of code).

The basic idea is to extract parts of its functions into other classes.

In order to find those you can look for

  • fields/parameters that often get used together. They might move together into a new class

  • methods (or parts of methods) that use only a small subset of the fields in the class, the might move into a class containing just those field.

  • primitive types (int, String, boolean). They often are really value objects before their coming out. Once they are value object, they often attract methods.

  • look at the usage of the god object. Are there different methods used by different clients? Those might go in separate interfaces. Those intefaces might in turn have separate implementations.

For actually doing these changes you should have some infrastructure and tools at your command:

  • Tests: Have a (possibly generated) exhaustive set of tests ready that you can run often. Be extremely careful with changes you do without tests. I do those, but limit them to things like extract method, which I can do completely with a single IDE action.

  • Version Control: You want to have a version control that allows you to commit every 2 minutes, without really slowing you down. SVN doesn't really work. Git does.

  • Mikado Method: The idea of the Mikado Method is to try a change. If it works great. If not take note what is breaking, add them as dependency to the change you started with. Rollback you changes. In the resulting graph, repeat the process with a node that has no dependencies yet. http://mikadomethod.wordpress.com/book/

Distressed answered 18/3, 2013 at 11:22 Comment(0)
D
5

According to the book "Object Oriented Metrics in Practice" by Lanza and Marinescu, The God Class design flaw refers to classes that tend to centralize the intelligence of the system. A God Class performs too much work on its own, delegating only minor details to a set of trivial classes and using the data from other classes.

The detection of a God Class is based on three main characteristics:

  1. They heavily access data of other simpler classes, either directly or using accessor methods.
  2. They are large and complex
  3. They have a lot of non-communicative behavior i.e., there is a low cohesion between the methods belonging to that class.

Refactoring a God Class is a complex task, as this disharmony is often a cumulative effect of other disharmonies that occur at the method level. Therefore, performing such a refactoring requires additional and more fine-grained information about the methods of the class, and sometimes even about its inheritance context. A first approach is to identify clusters of methods and attributes that are tied together and to extract these islands into separate classes.

Split Up God Class method from the book "Object-Oriented Reengineering Patterns" proposes to incrementally redistribute the responsibilities of the God Class either to its collaborating classes or to new classes that are pulled out of the God Class.

The book "Working Effectively with Legacy Code" presents some techniques such as Sprout Method, Sprout Class, Wrap Method to be able to test the legacy systems that can be used to support the refactoring of God Classes.

What I would do, is to sub-group methods in the God Class which utilize the same class properties as inputs or outputs. After that, I would split the class into sub-classes, where each sub-class will hold the methods in a sub-group, and the properties which these methods utilize.

That way, each new class will be smaller and more coherent (meaning that all their methods will work on similar class properties). Moreover, there will be less dependency for each new class we generated. After that, we can further reduce those dependencies since we can now understand the code better.

In general, I would say that there are a couple of different methods according to the situation at hand. As an example, let's say that you have a god class named "LoginManager" that validates user information, updates "OnlineUserService" so the user is added to the online user list, and returns login-specific data (such as Welcome screen and one time offers)to the client.

So your class will look something like this:

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...
}}

Now we can see that this class will be huge and complex. It is not a God class by book definition yet, since class fields are commonly used among methods now. But for the sake of argument, we can treat it as a God class and start refactoring.

One of the solutions is to create separate small classes which are used as members in the main class. Another thing you could add, could be separating different behaviors in different interfaces and their respective classes. Hide implementation details in classes by making those methods "private". And use those interfaces in the main class to do its bidding.

So at the end, RefactoredLoginManager will look like this:

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

DecryptHandler:

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

}

ValidateHandler:

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

}

Important thing to note here is that the interfaces () only has to include the methods used by other classes. So IValidateHandler looks as simple as this:

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

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...
    }
}

Since both methods are accessed in LoginHandler, interface has to include both methods:

public interface IClientDataSender {
    void sendCustomizedLoginMessage(String userId);

    void sendOneTimeOffer(String userId);
}
Demibastion answered 3/1, 2021 at 16:26 Comment(6)
maybe you can give some suggestions, after you splitted your god classes into more classes (collaborating classes and new classes), how to wire them up?, so how they communicate with each other, where to bring them together and how you can access the new methdos from outside, if they were public in the good class.Breviary
I plan to update my answer in the future when I have more time, but to answer your question, I would say that there are a couple of different methods according to the situation at hand. One of them is to create separate small classes which are used as members in the main class. Another thing you could add could be separating behaviours to different interfaces and their respective classes. Hide implementation details in classes by making those methods private. And use those interfaces in the main class to do its bidding.Demibastion
You might want to re-visit my answer now. It includes an example.Demibastion
thank you, but it was a trivial refactoring, you had in your god class only one method which was used outside, the rest was still internal, if you had different public methods which are refactored into different classes and in the last step you remove the Facade "RefactoredLoginManager", that would be interessting, but not solveable here now^^, maybe i should ask for that a separate question in softwareengineering.stackexchange.comBreviary
In your case, instead of calling "decryptHandler.decryptHashedString(hashedUserId);" , you could still keep the method "decryptHashedString(hashedUserId);" in your class, and inside that method you could call "decryptHandler.decryptHashedString(hashedUserId);". That way, you would not affect other classes that much, and still lighten the load in your class. It is not ideal to have methods just to redirect requests, though.Demibastion
I don't agree much with their 3 bullet points. 1 ok not so bad, but 2 and 3 not so much. 3 cannot be predefined, perhaps a class doing such is fine. These things are often attempted to be predefined as to some manual approach but often they become outdated or were never really useful. Upvote for the answer tho :)Holily
S
1

There are really two topics here:

  • Given a God class, how its members be rationally partitioned into subsets? The fundamental idea is to group elements by conceptual coherency (often indicated by frequent co-usage in client modules) and by forced dependencies. Obviously the details of this are specific to the system being refactored. The outcome is a desired partition (set of groups) of God class elements.

  • Given a desired partition, actually making the change. This is difficult if the code base has any scale. Doing this manually, you are almost forced to retain the God class while you modify its accessors to instead call new classes formed from the partitions. And of course you need to test, test, test because it is easy to make a mistake when manually making these changes. When all accesses to the God class are gone, you can finally remove it. This sounds great in theory but it takes a long time in practice if you are facing thousands of compilation units, and you have to get the team members to stop adding accesses to the God interface while you do this. One can, however, apply automated refactoring tools to implement this; with such a tool you specify the partition to the tool and it then modifies the code base in a reliable way. Our DMS can implement this Refactoring C++ God Classes and has been used to make such changes across systems with 3,000 compilation units.

Simper answered 10/5, 2019 at 16:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.