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.

design patterns

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

Leave a Reply

Login with