zoom_outLarge Class
The Large Class is a situation in where a class becomes excessively large and complex, often containing too many responsibilities and methods. This code smell can lead to various issues and challenges in maintaining, understanding, and extending the codebase.
Low Cohesion-large classes tend to have low cohesion, meaning that they handle multiple unrelated tasks or responsibilities. This violates the Single Responsibility Principle (SRP), making the class harder to understand and maintain.
High Coupling-large classes often have high coupling with other classes. Since they contain many methods and variables, they may rely heavily on other classes, leading to tight dependencies and making it difficult to isolate and test individual components. error-prone.
Readability and Understandability-large classes are challenging to read and understand. Developers may struggle to comprehend the entire class, leading to increased cognitive load and making it harder to identify bugs or make changes.
Maintainability-maintaining a large class becomes increasingly difficult over time. Adding new features or fixing bugs may require navigating through a vast amount of code, increasing the risk of introducing errors or unintentional side effects.
Testing Complexity-testing a large class comprehensively is complex and time-consuming. Writing test cases to cover all possible scenarios and edge cases becomes challenging, leading to inadequate test coverage and potentially missing critical bugs.
Code Duplication-large classes are prone to code duplication. Similar functionality may be implemented in different parts of the class, leading to redundancy and making it harder to maintain consistency.
Scalability-large classes hinder the scalability of the codebase. As the size of the class grows, it becomes increasingly challenging to add new functionality without further bloating the class, exacerbating the existing problems.
The example below contains the code that is responsible for creating new users.
In this state, the class responsibility is to connect to the API, retrieve user data from them, and then create new users based on this data.
class Users::Create
SOME_APP_URL = "https://api.someapp.io".freeze
def call
users_from_api.map do |user_params|
user = User.new
user.name = "#{user_params[:first_name]} #{user_params[:last_name}"
user.email = user_params[:email]
user.devices = Device.where(id: user[:id])
user.save!
end
end
def find_devices(user_id)
device_ids = user_devices(user_id).pluck(:id)
Device.where(id: device_ids)
end
def users_from_api
HTTParty.get("#{SOME_APP_URL}/users" ,headers: {
Authorization: "Bearer #{access_token}",
'Content-Type': 'application/json'
})
end
def user_devices(id)
HTTParty.get("#{SOME_APP_URL}/user/#{id}/devices" ,headers: {
Authorization: "Bearer #{access_token}",
'Content-Type': 'application/json'
})
end
def access_token
@access_token ||= HTTPart.post("#{SOME_APP_URL}/token")
end
end
This class itself is not very large yet, but as programmers trying to predict the future, we see that in the future, when we add additional functionalities, it may become too large, so we will try to split it into two smaller classes.
All functionalities related to handling and sending queries to the API will be separated into a separate class.
class SomeApp::API
SOME_APP_URL = "https://api.someapp.io".freeze
def users
api_client "/users"
end
def user_devices(id)
api_client "/users/#{id}/devices"
end
private
def api_client(endpoint)
HTTParty.get("#{SOME_APP_URL}#{endpoint}", headers:)
end
def headers
{
Authorization: "Bearer #{access_token}",
'Content-Type': 'application/json'
}
end
def access_token
@access_token ||= HTTPart.post("#{SOME_APP_URL}/token")
end
end
Thanks to this, we will be able to remove all methods related to API support and the class will be able to focus only on creating users.
class Users::Create
attr_reader :api_client
def initialize
@api_client = SomeApp::API.new
end
def call
users_from_api.map do |user_params|
user = User.new
user.name = "#{user_params[:first_name]} #{user_params[:last_name}"
user.email = user_params[:email]
user.save!
end
end
private
def users
api_client.users
end
def user_device(user)
api.client.user_device(user[:id])
end
def create_user(params)
user = User.new
user.name = "#{user_params[:first_name]} #{user_params[:last_name}"
user.email = user_params[:email]
user.devices = Device.where(id: user
user.save!
end
end
Pros & Cons
Improved Readability-breaking down a large class into smaller components makes the codebase more readable and understandable. Each component has a clear responsibility, making it easier for developers to comprehend and maintain the code.
Enhanced Maintainability-smaller classes are easier to maintain than large ones. When a class is focused on a single responsibility, making changes or adding new features becomes simpler and less error-prone.
Better Testability-smaller classes are typically easier to test. Unit testing becomes more straightforward because each class can be tested in isolation, leading to improved test coverage and more robust code.
Increased Reusability-refactoring a large class into smaller, cohesive components promotes code reusability. These components can be reused in different parts of the application, reducing code duplication and improving modularity.
Easier Collaboration-smaller classes encourage collaboration among team members. Developers can work on different components independently, leading to better teamwork and more efficient code reviews.
Increased Number of Classes-refactoring a large class may result in a higher number of smaller classes. While this can improve code organization, it may also lead to an increased cognitive load when navigating the codebase.
Potential Over-Abstraction-breaking down a large class into smaller components can sometimes lead to over-abstraction. Developers must strike a balance between creating smaller, focused classes and avoiding excessive complexity.
Dependencies Between Classes-refactoring a large class may reveal dependencies between different components. Managing these dependencies and ensuring proper communication between classes can be challenging, especially in complex systems.
Codebase Fragmentation-refactoring a large class may fragment the codebase, making it harder to follow the flow of execution. Developers may need to navigate between multiple classes to understand the entire system, which can increase the complexity of the codebase.
Refactoring Overhead-refactoring a large class requires time and effort. It may disrupt ongoing development tasks and introduce new bugs if not done carefully. Additionally, refactoring may require updating existing tests and documentation to reflect the changes.
Potential Performance Impact-introducing more classes and method calls through refactoring may have a slight performance impact, although this is usually negligible in most applications.