Getting your Trinity Audio player ready...
|
In this blog, we’re gonna look into what are Software Designs and design patterns, what’s the difference between the design patterns and anti-patterns that we can have in the Ruby on Rails application Models. We’ll also look into one of the far most important patterns, MVC in a web application, and how to use it wisely in a Rails application. We’ll be covering most of the commonly occurring anti-patterns that can be there in the models and we can delegate using some of the important design patterns and principles.
Design – A Software Design?
A procedure to create a specification of the Software Artifacts that facilitates coders to implement the Software. It assists to implement the Software and avoids uncertainty. Strictly illustrates individual modules or components rather than the entire feature. It’s one of the most vital and initial phases of the Secure Software Development Life Cycle (SSDLC).
It answers the question HOW?????
Now you might doubt what exactly is the key distinction between Design and Architecture.
Design
Concentrates on the code-level sketch.
Architecture
Focus on the skeleton or blueprint and high-level infrastructure.
Design Patterns
The pattern is a reusable solution to solve a common problem. It’s always a good practice among developers. It will guide you to the solution but not the exact code. Design patterns are a Guide for writing well-designed code. These are incomplete chunks so it’s hard to directly inject them into the code. Instead, they’re templates, a description of how to solve a problem.
“Changeability is the only design metric that matters; code that’s easy to change is well-designed.”
Sandi Metz
Why use them?
Design Patterns make your code more manageable, scalable, and easily testable. It breaks into modules, many times the issues that you might encounter at a very later stage of development are identifiable right at the beginning of the development when we use design patterns. They make your life easy. It also helps in communicating with other developers.
Types
- Active Record
- Adapter
- Bridge
- Builder
- Chain of Responsibility
- Circuit Breaker
- Command
- Decorator
- Delegation
- Factory
- Gateway
- Handle
- MVC
- Null Object
- Presentation
- Process Objects
- Proxy
- Rails Presenter
- Repository
- Singleton
- Special Case
- Template Method
- Visitor
- Interactions
Anti-Patterns
A Software Anti-pattern is a pattern that is ineffective or unethical. Solving a problem in an unrecommended fashion without following any underlying standards or best practices to achieve functionality.
MVC
Model
Handles datum and business logic
View
Presentation of the data and the UI
Controller
Binds both by fetching data from the Model and forecasting it on the Views
Mostly identifiable anti-patterns on the Rails application Models
God Object FAT Overweight MODEL
An object that instances a mass of unique, has a lot of non-relatable or miscellaneous methods, or combinations of both. As an application grows and business logic expands, folks tend to overcrowd their models. Constant growth will result in an anti-pattern called the Fat Model. Practically, as long as the application grows, there’ll be exponential growth in the Model if unfollowed.
Single Responsibility Principle – SRP
In general, a computer-programming principle depicts that each module, class, or function in a computer program ought to have responsibility over a single point of interest of its functionality, and it must wrap itself. All of that module, class, or function’s services should be almost in parallel with its own responsibility.
Let’s Go..
Let’s see some Practical Examples
Let’s take a Video Streaming Service for an instance. On a basic video streaming service, the following are the required feature that users love.
- Notify subscribers and posters when a new video on upload
- Check for Profinates
- Video Downloads
- Export videos in varying formats
- DATA Handling
I’ve created a Streaming application for the above to explain the above use cases. The first most thing the developer will do to achieve this is to create a model like the one below. Here’s a model named Video at has its own definitions for associations to have the RDBMS interrelated and also has validations on the input. Callbacks to listen to the change event on the database and perform actions appropriately. Also, we have a few methods defined to fulfill the case-based logic.
class Video < ActiveRecord::Base
belongs_to :category
belongs_to :channel
belongs_to :poster
has_one :text
has_many :downloads
validates :channel_id, presence: true
validates :poster_id, presence: true
after_update :alert_channel_subscribers
after_update :alert_poster
def alert_channel_subscribers
return if unreleased?
channel.subscribers.each { |subscriber| subscriber.notify(self) }
end
def alert_poster
PosterMailer.video_email(poster, self).deliver_now
end
def includes_profanities?
text.scan_for_profanities.any?
end
def user_downloaded?(user)
user.library.has_video?(self)
end
def find_posted_from_channel_with_categories
# ...
end
def find_posted_with_categories
# ...
end
def to_mov
# ...
end
def to_mp4
# ...
end
def to_wmv
# ...
end
def to_avi
# ...
end
def to_mkv
# ...
end
end
You’ll be landing up with such a model which is an anti-pattern as it’s doing more than one of its purposes and behaviors.
How to eradicate it?
#1 Notify subscribers and posters
Let’s take the first use case to notify subscribers and posters. In most of the services, the notification systems never work in real-time. But whenever there’s a change in data identified on the event callbacks are available in Rails and later we can perform any action based on the trigger.
Here if you look in the below code snippet, the callbacks running to notify both posters and subscribers but in real-time and not in the background.
after_update :alert_channel_subscribers
after_update :alert_poster
def alert_channel_subscribers
return if unreleased?
channel.subscribers.each { |subscriber| subscriber.notify(self) }
end
def alert_poster
PosterMailer.video_email(poster, self).deliver_now
end
What to do? – The answer is Jobs
Delegating it to Background Jobs by creating 3 different jobs for posters, and subscribers and refreshing the videos list on demand. The queuing mechanism can make use of the Delayed Jobs or Sidekiq based on the requirements and needs.
Notify Subscribers
class NotifySubscribers < ApplicationJob
def perform(subscribers)
subscribers.each { |subscriber| subscriber.notify }
end
end
Notify Posters
class NotifyPoster < ApplicationJob
def perform(poster, video)
PosterMailer.video_email(poster, self).deliver_now
end
end
Refresh Videos
class VideoRefreshJob < ApplicationJob
def perform
videos = Video.where(status: :posted)
# ...
end
end
Once you’ve moved your code from the real-time event callback model code to the respective Jobs and the model will look kinda similar to below. On the change event, jobs can handle them. Your model now has fewer lines of code.
class VideoUpdated < ActiveRecord::Base
############# Callbacks ##############
after_update :alert_channel_subscribers, if: :posted?
after_update :alert_poster
############# Methods ##############
def alert_channle_subscribers
NotifySubscribers.perform_later(self)
end
def alert_poster
NotifyPoster.perform_later(poster, self)
end
end
#2 Check if includes profanities and downloaded.
Coming to the second use case to check if the video content has any profanities or if the user had already downloaded the video to highlight them on the view page. To achieve this, the developer would have written code in the model with methods to check the above cases. And call the method from the view or wherever required complicating the model thereby polluting and introducing anti-patterns.
def includes_profanities?
text.scan_for_profanities.any?
end
def user_downloaded?(user)
user.library.has_video?(self)
end
Mitigation!
To make sure this doesn’t happen, we have a beautiful pattern called the Decorator pattern which you can follow. Below I’ve moved the helper methods for the UI to the decorator with which the decorator itself by handling the data manipulation as required. We also have a gem named Draper which supports the decorator pattern. Once you’ve defined the decorators, you can call them on the controllers like below.
class VideoDecorator < Draper::Decorator
delegate_all
def includes_profanities?
object.text.scan_for_profanities.any?
end
def user_downloaded?(user)
object.user.library.has_video?(self)
end
end
# show.html.erb
<div id="more_tables" class="more_tables">
<div class="row">
<div class="col-md-12 col-md-offset-0">
<%= @video.includes_profanities? %>
<%= @video.user_downloaded?(user) %>
</div>
</div>
</div>
In the view page of each video, we’ve used the default decorate method on the active record object or the collection. The video object has its decorative methods included with which you can call them to have it work.
#3 Export videos in varying formats
The third one is with the downloading or exporting of videos of different formats. There are a few methods in the model which take care of converting the format from one to the other. Here you can see multiple methods doing their job and polluting the model causing the anti-pattern to occur.
def to_mov
# ...
end
def to_mp4
# ...
end
def to_wmv
# ...
end
def to_avi
# ...
end
def to_mkv
# ...
end
end
The right way!
There are several other patterns to remove this anti-pattern from the model. A notable way is to move this conversion logic to a model Converter concern (Mixin) and also it depends on the usage like below. Extending the concern to the Video Model, the video record is usable on the converter methods after initialization.
class Video::VideoConverter
attr_reader :video
def initialize(video)
@video = video
end
def to_mov
# ...
end
def to_mp4
# ...
end
def to_wmv
# ...
end
def to_avi
# ...
end
def to_mkv
# ...
end
end
class VideoUpdated < ActiveRecord::Base
# Call as below @video.converter.to_mp3 or self.converter.to_mp3
def converter
VideoConverter.new(self)
end
end
Now if you see to convert the format there’s a single method in the model which calls the converter concern to convert videos of any format required.
#4 <DATA>
This is one of the most important factors when it comes to data-intensive applications. Let’s say there’s a requirement to retrieve videos based on the channel, poster, and posted date. For this to happen, a developer will fetch the data required using the query written on a service or a model method like the one below.
class VideoReportService
def fetch_videos_from_channel(channel_id)
videos = Video.where(status: :posted)
.where(channel_id: channel_id)
.order(:title)
# ...
end
end
Service now has the queries and is not reusable. To have this in a more optimized way, we can make use of the Scopes to define data that are pretty identifiable and straightforward.
############# Scopes ##############
scope :post, -> { where(posted: true) }
scope :by_channel, ->(channel_id) { where(channel_id: channel_id) }
scope :sorted_by_title, { order(:title) }
scope :sorted_by_release_date, { order(:release_date) }
class VideoReportService
def fetch_videos_from_channel(channel_id)
videos = Video.posted.by_channel(channel_id).sorted_by_title # Scopes
end
end
Here I’ve filtered the videos by the posted date, and channel and sorted them by title and release dates respectively, and is reusable. We see a lot of scopes written thereby again polluting the model and more complex scopes will also introduce anti-patterns into the model. We do have furthermore ways to have it optimized. Instead of having them as defined named scopes, you can move them to finders defined for each of the models.
I have written a finder method where you’ll have explicit functions to fetch or find data required like below.
Finder!
module Video::VideoFinder
def find_posted_from_channel(channel_id)
Video.posted.by_channel(channel_id).sorted_by_title
end
def find_posted
Video.posted.sorted_by_title
end
def find_latest_release_by_channel
find_posted_from_channel.sorted_by_release_date
end
end
class VideoReportService
include Video::VideoFinder
def fetch_videos_from_channel(channel_id)
videos = Video::Finders.find_posted_from_channel_with_categories(channel_id) # Finder Object
end
end
Here the finder method fetches channel-specific videos using reusable scopes. Even instead of the defined scopes, we can also add raw SQL queries or a Rails way of fetching records from the database using Active Record. This reduces the size of the model thereby replacing an anti-pattern.
Repository
There is also an option that you can opt for the Repository pattern for the same to move the scope of data fetching out of your model. It’s more similar to dirty monkey patching. You’ll have repositories defined for each model and the repository will have its active record methods defined. Here you can see that this method is the default find the method and we’re overriding the method of the active record. Still, we’ll make use of the same super method inside our method.
class VideoRepository
class << self
def find(id)
Video.find(id)
rescue ActiveRecord::RecordNotFound => e
raise RecordNotFoundError, e
end
def destroy(id)
find(id).destroy
end
def recently_posted_by_channel(channel_id)
Video.where(posted: true)
.where(channel_id: channel_id)
.order(:release_date)
end
end
end
class VideoReportService
include Video::VideoFinder
def fetch_videos_from_channel(channel_id)
videos = VideoRepository.recently_posted_by_channel(channel_id) # Repository Pattern
end
Once you move all the default methods to a repository you will be pretty sure to identify most of the corner cases and the exceptions that might come up and can be handled in the repository and need not worry about the AR throwing exceptions implicitly. You can also write specific test cases so that you can improve your coverage and avoid breakages on any corner case.
We can also have custom finder methods which would be more readable when in a Repository as it can have any data manipulation code and you can make use of them wherever and whenever required.
Tada!!
These are some of the possible ways of removing the anti-pattern from the Model file and once you’ve designed a code in such a way to follow the most possible design patterns look like below.
class Video < ActiveRecord::Base
############# Dependencies ##############
extend Video::VideoFinder
include Video::VideoConverter
############# Associations ##############
belongs_to :category
belongs_to :channel
belongs_to :poster
has_one :text
has_many :downloads
############# Scopes ##############
scope :posted, -> { where(posted: true) }
scope :by_channel, ->(channel_id) { where(channel_id: channel_id) }
scope :sorted_by_title, { order(:title) }
scope :sorted_by_release_date, { order(:release_date) }
############# Validations ##############
validates :channel_id, presence: true
validates :poster_id, presence: true
############# Callbacks ##############
after_update :alert_channel_subscribers, if: :posted?
after_update :alert_poster
############# Methods ##############
def alert_channel_subscribers
NotifySubscribers.perform_later(self)
end
def alert_poster
NotifyPoster.perform_later(poster, self)
end
# Call as below @video.converter.to_mp3 or self.converter.to_mp3
def converter
VideoConverter.new(self)
end
end
Now you’ll have a more readable code and the test cases that you’ll have to add will also be pretty lesser with improved coverage. The main problem will be when it comes to writing test cases for a file that has 1000 lines of code for instance the spec file would be either double or triple times that of the source file.
In the meanwhile, as the application grows it becomes more tedious to understand and debug the code in case of issues. Delegating code from the Model and moving it to appropriate files using design patterns also reduces the scope for testing and we’ll be pretty sure where this issue would have happened when you debug in case of an issue.
You can further move associations, validations, callbacks, and scopes to a separate file and just include them in the model which reduces the scope of the model file. Let’s say more than one model is dependent on the other and makes use of some similar associations, we can reuse this file at any cost to avoid duplications.
Patterns or principles used so far
- Service Objects
- Jobs
- Mixins
- Decorators
- DRY – Don’t Repeat Yourself
- SRP – Single Responsibility Principle
- Repository
- Data vs schema migrations
Pros
- Code Modularised for easy understanding of the workflow
- You can easily test the functionality as you’ve followed SRP which will help reduce the number of bugs
- Explicit unit tests for each module
- Debugging is easier as you’ll know where exactly this might have happened
- You’ll have very minimal changes to make if there requires any functionality change
- Impacts will be very less
Cons
- Creation of a lot of files
- Adding data workflow diagrams and set as a process to understand the code
- Require external libraries like gems
To conclude…
Hope you’ve learned some of the patterns that you can use to have your model code neat, clean and futuristic. I’ve almost covered the important design patterns but still, there are numerous useful patterns that you can transform into your code. Make sure to use them with care to attain the most out of them.
Get to know about Rently at https://use.rently.com/
To learn more about Engineering topics visit – https://engineering.rently.com