Chào mừng đến bài đầu tiên trong series về Ruby on Rails Patterns and Anti-patterns. Trong mỗi bài, chúng ta sẽ deep dive vào tất cả các patterns bạn có thể gặp khi làm việc với Rails apps.
Hôm nay, chúng ta sẽ show design pattern là gì và thử explain anti-pattern là gì. Để illustrate tốt hơn, dùng Ruby on Rails framework đã tồn tại khá lâu. Nếu Rails không phải cup of tea của bạn vì lý do gì, đừng lo, ideas (hoặc patterns) mô tả ở đây có thể resonate với technology bạn dùng.
Nhưng trước khi jump vào explain patterns và anti-patterns, làm sao chúng ta đến điểm cần chúng? Tại sao cần tất cả những thứ này cho software? Tại sao cần design solution?
Có, Bạn Là Một Designer
Từ những ngày đầu computer programming, mọi người phải deal với design của programs họ viết. Viết một program (hoặc software) là design một solution cho một problem. Khi viết software, bạn là một designer — feel free append đó vào job title. Design good solutions quan trọng vì software chúng ta viết sẽ được đọc và/hoặc edit bởi others. Cũng, solutions chúng ta tìm ra sẽ được built on bởi others trong tương lai.
Có tất cả này trong mind, generations of engineers bắt đầu thấy similar designs trong code và architecture throughout careers. Mọi người bắt đầu extract và document standard solutions cho problems. Một số nói đó là natural way chúng ta function như humans. Chúng ta thích categorize và tìm patterns trong mọi thứ, và software không ngoại lệ.
Là humans, patterns bắt đầu emerge nhiều hơn khi software engineering phức tạp hơn. Software design patterns phát triển và cement themselves với engineers worldwide. Sách, essays, và talks được given, lan tỏa ideas của well thought out và battle-tested solutions. Những solutions đó save nhiều người time và money, nên hãy đi qua term design pattern, xem nó thực sự là gì.
Design Pattern Là Gì?
Trong software engineering, pattern được mô tả là solution có thể reused để solve common problem. Pattern là thứ considered good practice giữa software engineers. Vì software engineers set chúng, chúng có thể nhanh chóng từ patterns sang opposite — anti-patterns — nhưng sẽ nói sau.
Design pattern chỉ show you the way to solution nhưng không give bạn piece of code ready to plug vào software. Hãy nghĩ pattern như guide để viết well-designed code, nhưng bạn phải tự implement. Using patterns trong day-to-day coding emerge cuối những năm 80, nơi Kent Beck và Ward Cunningham lên idea dùng 'pattern language'.
Ý tưởng pattern languages đến cuối những năm 70 bởi Christopher Alexander trong sách A Pattern Language. Bạn có thể surprised, nhưng sách không về software engineering mà về architecture của buildings. Pattern language là organized và coherent set of patterns, mỗi pattern mô tả một problem và core của solution có thể dùng nhiều cách. Nghe familiar? (Hint: frameworks, another hint: Rails)
Sau đó, design patterns trong software engineering nổi tiếng với large audiences sau cuốn sách huyền thoại Design Patterns bởi Gang Of Four xuất bản 1994. Trong sách, có explanations và definitions của patterns dùng ngày nay — Factory, Singleton, Decorator, chỉ vài cái.
Tuyệt, giờ đã acquainted hoặc refreshed knowledge về design và patterns, hãy tìm hiểu anti-patterns là gì.
Design Anti-Pattern Là Gì?
Nếu nghĩ patterns là good guys, anti-patterns là bad ones. Chính xác hơn, software anti-pattern là pattern có thể commonly used nhưng considered ineffective hoặc counterproductive. Ví dụ điển hình: God objects chứa nhiều functions và dependencies, có thể extract và separate vào different objects.
Common causes của anti-patterns trong code nhiều. Ví dụ tốt: khi good guy (pattern) trở thành bad guy (anti-pattern). Giả sử bạn dùng technology nhất định ở company trước, và gain high competence. Cho ví dụ, dùng Docker. Biết cách efficiently pack applications vào Docker containers, orchestrate chúng trong cloud, pull logs từ cloud. Bất chợt, new job cần ship front end applications. Vì biết nhiều về Docker và ship apps với nó, quyết định đầu tiên là package everything lên và deploy lên cloud.
Nhưng, không biết front end apps không complex ở job hiện tại, và put chúng vào containers có thể không phải effective solution. Ban đầu nghe hay, nhưng sau đó chứng minh counterproductive. Anti-pattern này gọi là "Golden Hammer".
Tóm gọn bằng câu nói: "Nếu bạn có búa, mọi thứ trông như đinh". Nếu bạn thật giỏi Docker và orchestration services, mọi thứ là Docker service để orchestrate trong cloud.
Những thứ này xảy ra và sẽ xảy ra. Good guys thành bad guys, và ngược lại. Nhưng Ruby và Rails fit vào picture như thế nào?
Ruby Trước, Rails Sau
Hầu hết folks introduced to Ruby bằng Ruby on Rails, framework phổ biến build websites nhanh. Tôi cũng như vậy, không sai. Rails based trên well-established software pattern Model-View-Controller, MVC. Nhưng trước khi dive vào MVC pattern trong Rails, một fallacy lớn thường xảy ra là dùng Rails mà không học Ruby properly.
Rails framework từng là go-to khi có idea và muốn build fast. Giờ khác hoàn toàn, Rails vẫn dùng nhưng không mức độ prime. Vì dễ dùng và run, nhiều beginners set out build web apps dùng rails new. Sau đó, problems xảy ra. Là beginner, bạn lured bởi speed và simplicity của Rails, mọi thứ works magically và smoothly lúc đầu. Rồi bạn thấy đã take nhiều 'magic' for granted, và không hiểu gì xảy ra behind the curtain.
Tôi có problem này, và chắc nhiều beginners và advanced beginners suffer từ nó. Bắt đầu với framework trên tay, build trên nó, khi try add something highly custom, bạn không thể, vì used up all magic points từ framework. Lúc đó, phải quay về đầu học basics. Quay về không biggie, xảy ra với best of us. Nhưng problem lớn hơn nếu move on không học essential things, như Ruby. Cuốn sách hay giúp đỡ: The Well-Grounded Rubyist.
Là beginner, không phải đọc từ đầu đến cuối. Nhưng keep bên cạnh để consult nhanh. Tôi không nói suddenly stop đọc whole book, nhưng stop sometimes refresh Ruby basics, có thể open new horizons.
MVC: Bánh Mì Và Bơ Của Rails
OK, nhưng MVC? Model-View-Controller pattern tồn tại từ xưa. Được adopt bởi nhiều frameworks across nhiều languages như Ruby (Rails), Python (Django), Java (Play, Spring MVC). Ý tưởng: separate components mỗi cái làm job:
- Model handle data và business logic.
- View cho presentation của data và user interface.
- Controller tie hai cái kia bằng cách get data từ Model và show View cho user.
Nghe great in theory, và excellent khi logic minimal và website không hold complex logic. Đó là nơi things get tricky, nhưng sẽ nói sau.
MVC spread như wildfire throughout web development community. Thậm chí libraries như React, insanely popular ngày nay, explained as view layer của web app. No other pattern popularized nhiều đến mức cannot be shaken off. Rails thêm Publish-Subscribe với ActionCable, nơi concept của channels được mô tả as controller của MVC pattern.
Nhưng anti-patterns ở đâu, trong pattern widely used này? Hãy xem common anti-patterns cho mỗi phần của MVC.
Model Problems
Khi ứng dụng grow và business logic expand, folks tend to overcrowd models. Constant growth dẫn đến anti-pattern Fat Model.
Pattern nổi tiếng 'Fat Model, Skinny Controller' identified as bad guy bởi một số, good guy bởi số khác. Chúng ta nói having any fat là anti-pattern. Hiểu rõ hơn, vào ví dụ. Imagine streaming service như Spotify hoặc Deezer. Bên trong, model cho songs như này:
class Song < ApplicationRecord
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers
after_update :alert_publisher
def alert_artist_followers
return if unreleased?
artist.followers.each { |follower| follower.notify(self) }
end
def alert_publisher
PublisherMailer.song_email(publisher, self).deliver_now
end
def includes_profanities?
text.scan_for_profanities.any?
end
def user_downloaded?(user)
user.library.has_song?(self)
end
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
Vấn đề với models như này: trở thành dumping ground cho logic khác nhau related to song. Xảy ra khi methods added slowly one-by-one over time. Cả model thì large và complex, splitting logic vào couple other places có thể beneficial in future.
Ngay lập tức, thấy recommended practices model này breaking. Breaking Single Responsibility Principle (SRP). Deal với notifying followers và publisher. Check text cho profanities, có methods export song sang audio formats khác, v.v. Tất cả thêm complexity cho model, không imagine test file cho model này.
Cách refactor model majorly depends on how methods called và used ở nơi khác. Sẽ present general ideas handle这些, bạn chọn fit case bạn nhất.
Callbacks notify followers và publisher có thể extract sang jobs. Jobs sẽ enqueued và logic kept out of model, như sau:
class NotifyFollowers < ApplicationJob
def perform(followers)
followers.each { |follower| follower.notify }
end
end
class NotifyPublisher < ApplicationJob
def perform(publisher, song)
PublisherMailer.song_email(publisher, self).deliver_now
end
end
Jobs chạy riêng trong separate process, away from model. Giờ test job logic separately và chỉ check proper job was enqueued từ model.
Giả sử check profanities và user downloaded song xảy ra ở view part. Có thể dùng Decorator pattern. Popular solution quick start: Draper gem. Với nó, viết decorator như sau:
class SongDecorator < Draper::Decorator
delegate_all
def includes_profanities?
object.text.scan_for_profanities.any?
end
def user_downloaded?(user)
object.user.library.has_song?(self)
end
end
Sau đó, gọi decorate trong controller, ví dụ:
def show
@song = Song.find(params[:id]).decorate
end
Dùng trong views như sau:
<%= @song.includes_profanities? %>
<%= @song.user_downloaded?(user) %>
Nếu không thích dependency, tự roll decorator, nhưng nói ở bài khác. Giờ đã separate majority model concerns, deal với methods finding songs và converting song. Dùng modules separate chúng:
module SongFinders
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
end
module SongConverter
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
Song model sẽ extend SongFinders module, methods available as class methods. Song model include SongConverter, methods available trên model instances.
Tất cả nên make Song model pretty slim và on point:
class Song < ApplicationRecord
extend SongFinders
include SongConverter
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers, if: :published?
after_update :alert_publisher
def alert_artist_followers
NotifyFollowers.perform_later(self)
end
def alert_publisher
NotifyPublisher.perform_later(publisher, self)
end
end
Còn nhiều model anti-patterns hơn, đây chỉ một example. Stay tuned bài khác trong series, đi deep vào more model anti-patterns. Giờ xem view problems.
View Problems
Ngoài model problems, Rails folks sometimes struggle với complexity của views. Ngày xưa, HTML và CSS là kings của view part. Dần dần, JavaScript đến reign, và hầu hết front end viết bằng JavaScript. Rails follow paradigm hơi khác. Thay vì everything in JavaScript trong view, bạn chỉ "sprinkle" JS onto it.
Trong mọi case, deal với HTML, CSS, JS, và Ruby cùng lúc có thể messy. Tricky với building Rails views là domain logic đôi khi tìm thấy trong view. No-no vì break MVC pattern.
Case khác: dùng too much embedded Ruby trong views và partials. Maybe logic có thể vào helper hoặc decorator (còn gọi view model hoặc presenter). Sẽ vào examples trong next posts, stay tuned.
Controller Problems
Rails controllers cũng suffer từ variety problems. Một trong số đó: Fat Controller anti-pattern.
Trước, model béo, nhưng giảm cân, giờ notice controller added extra weight. Thường xảy ra khi business logic put trong Controller, nhưng actual place ở model hoặc elsewhere. Ideas từ large Model section vẫn apply cho controller — extract code to presenters, dùng ActiveRecord callbacks, resort to Service objects.
Một số folks dùng gems như Trailblazer hoặc dry-transaction. Ý tưởng: tạo classes deal với specific transactions. Moving everything out of controller và keeping model skinny, store và test logic trong separate classes, gọi là services, transactions, actions, etc.
Kết Luận
Còn nhiều anti-patterns hơn và hơn nữa solutions cho chúng. Cover everything trong bài này sẽ take too much space và time và làm bài trông fat (như model và controller nói). Follow series, nơi deep dive vào every aspect của MVC pattern trong Rails. Đó, tìm cách deal với famous anti-patterns. Cho đến lúc đó, hy vọng bạn enjoy overview về patterns và anti-patterns và common ones trong Ruby on Rails framework.
Hẹn gặp lại, cheers!
P.S. Nếu muốn đọc bài Ruby Magic ngay khi xuất bản, đăng ký newsletter Ruby Magic để không bỏ lỡ!