שלושה דברים שלמדתי מהקוד של Gumroad

12/04/2025

אז Gumroad עברו לקוד פתוח וזו הזדמנות מצוינת ללמוד על מבנה פרויקט ריילס גדול של מערכת אמיתית. אני בטוח שבימים הקרובים אבלה יותר זמן עם הריפו שלהם אבל הנה שלושה דברים מרכזיים שקופצים לעין:

1. מודלים שטוחים, קונסרנים מקוננים

יש המון מודלים בגאמרוד ולמרות זאת כולם באותה תיקיה. מודל מייצג טבלה בבסיס הנתונים ושמירת כל המודלים באותה תיקיה מסדרת את תיקיית models בצורה מקבילה לסכימה וכך קל למצוא דברים. מצד שני מבנה תיקיות במערכת קבצים כן עוזר לנו להתמצא אז את הלוגיקה הם כתבו בקבצי concerns שמסודרים בתיקיות. אין להם בעיה עם Concern-ים שנטענים רק ממודל אחד למשל המודול Follower::AudienceMember שנטען רק מתוך:

class Follower < ApplicationRecord
  include ExternalId
  include TimestampScopes
  include Follower::From
  include Deletable
  include ConfirmedFollowerEvent::FollowerCallbacks
  include Follower::AudienceMember

2. בדיקות בכל מקום

מבדיקה מדגמית כל מודל שנכנסתי אליו הכיל קוד בדיקה, לדוגמה:

# frozen_string_literal: true

require "spec_helper"

RSpec.describe Community do
  subject(:community) { build(:community) }

  describe "associations" do
    it { is_expected.to belong_to(:seller).class_name("User") }
    it { is_expected.to belong_to(:resource) }
    it { is_expected.to have_many(:community_chat_messages).dependent(:destroy) }
    it { is_expected.to have_many(:last_read_community_chat_messages).dependent(:destroy) }
    it { is_expected.to have_many(:community_chat_recaps).dependent(:destroy) }
  end

  describe "validations" do
    it { is_expected.to validate_uniqueness_of(:seller_id).scoped_to([:resource_id, :resource_type, :deleted_at]) }
  end

  describe "#name" do
    it "returns the resource name" do
      community = build(:community, resource: create(:product, name: "Test product"))

      expect(community.name).to eq("Test product")
    end
  end

  describe "#thumbnail_url" do
    it "returns the resource thumbnail url for email" do
      community = build(:community, resource: create(:product))

      expect(community.thumbnail_url).to eq(ActionController::Base.helpers.asset_url("native_types/thumbnails/digital.png"))
    end
  end
end

אני מודה שבחלק מהמקומות הבדיקה דורשת קצת מאמץ בקריאה, לדוגמה הבדיקה הבאה:

it "returns affiliates sorted by # of products" do
  order = [affiliate_user_1, affiliate_user_3, affiliate_user_2]

  expect(seller.direct_affiliates.sorted_by(key: "products", direction: "asc")).to eq(order)
  expect(seller.direct_affiliates.sorted_by(key: "products", direction: "desc")).to eq(order.reverse)
end

מחייבת אותי לגלול לתחילת הקובץ כדי להבין איזה משתמש נוצר עם כמה מוצרים ואישית הייתי מעדיף לראות את ה create צמוד לקוד הבדיקה, אבל בואו - כמות הבדיקות והמגוון מרשימים. יש להם בדיקות מודלים, בדיקות לקונטרולרים ובדיקות מערכת.

3. פרונטאנד נראה מאתגר

גאמרוד משתמשים בריילס עבור השרת וריאקט לצד לקוח. אני לא מצאתי בדיקות לקומפוננטות בצד הפרונטאנד וגם הקוד עצמו היה נראה לי הרבה פחות מסודר: קבצים של מאות ואלפי שורות, פונקציות שמוגדרות בתוך הקומפוננטות, קוד ולוגיקה בתוך JSX, ובאופן כללי הרגשה של פחות תשומת לב.

דוגמה קטנה האפקט הבא שנועד להגדיל גובה של textarea כשהתוכן משתנה:

  React.useEffect(() => {
    if (!ref.current) return;

    ref.current.style.height = "inherit";
    ref.current.style.height = `${ref.current.scrollHeight}px`;
  }, [props.value]);

שהיה נכון יותר לכתוב אותו בתור layout effect או הכי טוב לוותר עליו לגמרי ולהשתמש ב CSS.

או במבט יותר גבוה הבחירה לשמור אוביקט בתוך קונטקסט למשל ב DomainSettings.ts שלהם:

type DomainSettings = {
  scheme: string;
  appDomain: string;
  rootDomain: string;
  shortDomain: string;
  discoverDomain: string;
  thirdPartyAnalyticsDomain: string;
};

const Context = React.createContext<DomainSettings | null>(null);

שאומרת שכל פעם שאיזשהו מפתח באוביקט ישתנה כל הקומפוננטות שמקשיבות גם למפתחות אחרים יתרנדרו מחדש, מה שפוגע בביצועים.

סך הכל כל הכבוד לגאמרוד על השיתוף והפתיחות. מעבר לקוד פתוח פותח הזדמנויות לקהילה לגדול ולמפתחים אחרים ללמוד ובמיוחד כשמדובר על מערכת פרודקשן אמיתית. זה הקוד בגיטהאב שתוכלו גם להנות וללמוד:

https://github.com/antiwork/gumroad