Tuesday, December 30, 2025

Clean Code Chapter 3: Functions

 

Clean Code Chapter 3: Functions

Writing Functions That Tell a Story

In Chapter 3 of Clean Code, Robert C. Martin lays out a compelling vision: functions should be small, do one thing, and read like well-written prose. This chapter transformed how I think about structuring code, and the principles apply beautifully to both Rails backends and React frontends.

Let's explore the key ideas with practical examples.


Small!

Martin's first rule of functions is that they should be small. His second rule? They should be smaller than that.

Functions should rarely be more than 20 lines. Ideally, they should be 5-10 lines. This isn't arbitrary. Small functions are easier to understand, test, and maintain.

Rails Example

Before: A bloated controller action

def create
  @user = User.new(user_params)
  
  if @user.save
    if params[:subscribe_to_newsletter]
      newsletter = Newsletter.find_by(name: 'weekly')
      if newsletter
        Subscription.create(user: @user, newsletter: newsletter)
        NewsletterMailer.welcome_email(@user).deliver_later
      end
    end
    
    if @user.referral_code.present?
      referrer = User.find_by(referral_code: @user.referral_code)
      if referrer
        Credit.create(user: referrer, amount: 10, reason: 'referral')
        Credit.create(user: @user, amount: 5, reason: 'referred')
        ReferralMailer.successful_referral(referrer, @user).deliver_later
      end
    end
    
    redirect_to dashboard_path, notice: 'Welcome!'
  else
    render :new
  end
end

After: Small, focused functions

def create
  @user = User.new(user_params)
  
  if @user.save
    handle_newsletter_subscription
    process_referral
    redirect_to dashboard_path, notice: 'Welcome!'
  else
    render :new
  end
end

private

def handle_newsletter_subscription
  return unless params[:subscribe_to_newsletter]
  
  NewsletterSubscriptionService.subscribe(@user, 'weekly')
end

def process_referral
  return unless @user.referral_code.present?
  
  ReferralService.process(@user, @user.referral_code)
end

React Example

Before: A component doing too much

function UserDashboard({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [orders, setOrders] = useState<Order[]>([]);
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        return fetch(`/api/users/${userId}/orders`);
      })
      .then(res => res.json())
      .then(data => {
        setOrders(data);
        return fetch(`/api/users/${userId}/notifications`);
      })
      .then(res => res.json())
      .then(data => {
        setNotifications(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <Spinner />;

  return (
    <div>
      <h1>Welcome, {user?.name}</h1>
      <div className="stats">
        <div>Orders: {orders.length}</div>
        <div>Unread: {notifications.filter(n => !n.read).length}</div>
      </div>
      <ul>
        {orders.map(order => (
          <li key={order.id}>
            Order #{order.id} - ${order.total} - {order.status}
          </li>
        ))}
      </ul>
    </div>
  );
}

After: Composed from smaller pieces

function UserDashboard({ userId }: { userId: string }) {
  const { user, orders, notifications, loading } = useDashboardData(userId);

  if (loading) return <Spinner />;

  return (
    <div>
      <WelcomeHeader userName={user?.name} />
      <DashboardStats orderCount={orders.length} notifications={notifications} />
      <OrderList orders={orders} />
    </div>
  );
}

function DashboardStats({ 
  orderCount, 
  notifications 
}: { 
  orderCount: number; 
  notifications: Notification[];
}) {
  const unreadCount = notifications.filter(n => !n.read).length;
  
  return (
    <div className="stats">
      <div>Orders: {orderCount}</div>
      <div>Unread: {unreadCount}</div>
    </div>
  );
}

Do One Thing

Functions should do one thing. They should do it well. They should do it only.

But how do you know if a function does "one thing"? Martin offers a useful test: if you can extract another function from it with a name that isn't merely a restatement of its implementation, it's doing more than one thing.

Rails Example

Before: A method doing multiple things

def process_order(order)
  # Validate inventory
  order.line_items.each do |item|
    product = item.product
    if product.inventory_count < item.quantity
      raise InsufficientInventoryError, "Not enough #{product.name}"
    end
  end
  
  # Calculate totals
  subtotal = order.line_items.sum { |item| item.price * item.quantity }
  tax = subtotal * order.tax_rate
  shipping = calculate_shipping(order)
  total = subtotal + tax + shipping
  
  # Update order
  order.update!(
    subtotal: subtotal,
    tax: tax,
    shipping: shipping,
    total: total,
    status: 'confirmed'
  )
  
  # Send notifications
  OrderMailer.confirmation(order).deliver_later
  InventoryService.reserve_items(order)
end

After: Each function does one thing

def process_order(order)
  validate_inventory(order)
  finalize_totals(order)
  confirm_order(order)
  send_notifications(order)
end

private

def validate_inventory(order)
  InventoryValidator.validate!(order)
end

def finalize_totals(order)
  OrderTotalCalculator.calculate!(order)
end

def confirm_order(order)
  order.update!(status: 'confirmed')
end

def send_notifications(order)
  OrderNotifier.send_confirmation(order)
  InventoryService.reserve_items(order)
end

React Example

Before: A handler doing multiple things

function handleSubmit(event: FormEvent) {
  event.preventDefault();
  
  // Validation
  const errors: string[] = [];
  if (!formData.email.includes('@')) {
    errors.push('Invalid email');
  }
  if (formData.password.length < 8) {
    errors.push('Password too short');
  }
  if (formData.password !== formData.confirmPassword) {
    errors.push('Passwords do not match');
  }
  
  if (errors.length > 0) {
    setErrors(errors);
    return;
  }
  
  // Transform data
  const payload = {
    email: formData.email.toLowerCase().trim(),
    password: formData.password,
    marketingOptIn: formData.newsletter,
  };
  
  // Submit
  setSubmitting(true);
  fetch('/api/register', {
    method: 'POST',
    body: JSON.stringify(payload),
  })
    .then(res => res.json())
    .then(data => {
      localStorage.setItem('token', data.token);
      navigate('/dashboard');
    })
    .catch(err => setErrors([err.message]))
    .finally(() => setSubmitting(false));
}

After: Separated concerns

function handleSubmit(event: FormEvent) {
  event.preventDefault();
  
  const validationErrors = validateRegistrationForm(formData);
  if (validationErrors.length > 0) {
    setErrors(validationErrors);
    return;
  }
  
  submitRegistration(formData);
}

function validateRegistrationForm(data: RegistrationFormData): string[] {
  const errors: string[] = [];
  
  if (!isValidEmail(data.email)) errors.push('Invalid email');
  if (!isValidPassword(data.password)) errors.push('Password too short');
  if (!passwordsMatch(data.password, data.confirmPassword)) {
    errors.push('Passwords do not match');
  }
  
  return errors;
}

async function submitRegistration(data: RegistrationFormData) {
  setSubmitting(true);
  
  try {
    const payload = buildRegistrationPayload(data);
    const response = await registerUser(payload);
    handleSuccessfulRegistration(response);
  } catch (err) {
    setErrors([err.message]);
  } finally {
    setSubmitting(false);
  }
}

One Level of Abstraction per Function

Functions should maintain a consistent level of abstraction. Mixing high-level concepts with low-level details creates cognitive dissonance.

Rails Example

Before: Mixed abstraction levels

def onboard_new_customer(customer_params)
  # High level: create customer
  customer = Customer.create!(customer_params)
  
  # Low level: SQL for finding default plan
  default_plan = Plan.where(active: true)
                     .where('price_cents > 0')
                     .order(:price_cents)
                     .first
  
  # High level: create subscription
  subscription = customer.subscriptions.create!(plan: default_plan)
  
  # Low level: date calculation
  trial_end = Time.current + 14.days
  trial_end = trial_end.end_of_day
  trial_end = trial_end.in_time_zone(customer.timezone)
  
  subscription.update!(trial_ends_at: trial_end)
  
  # High level: send welcome
  CustomerMailer.welcome(customer).deliver_later
end

After: Consistent abstraction

def onboard_new_customer(customer_params)
  customer = create_customer(customer_params)
  start_trial_subscription(customer)
  send_welcome_email(customer)
  customer
end

private

def create_customer(params)
  Customer.create!(params)
end

def start_trial_subscription(customer)
  plan = Plan.default_starter_plan
  trial_end = TrialPeriod.calculate_end_date(customer.timezone)
  
  customer.subscriptions.create!(
    plan: plan,
    trial_ends_at: trial_end
  )
end

def send_welcome_email(customer)
  CustomerMailer.welcome(customer).deliver_later
end

React Example

Before: Mixed abstractions in a component

function CheckoutPage() {
  const cart = useCart();
  
  // High level
  const handleCheckout = async () => {
    // Low level: manual localStorage manipulation
    const savedAddress = localStorage.getItem('shipping_address');
    const address = savedAddress ? JSON.parse(savedAddress) : null;
    
    // Low level: manual API construction
    const response = await fetch('/api/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
      },
      body: JSON.stringify({
        items: cart.items.map(i => ({ id: i.productId, qty: i.quantity })),
        shipping: address,
      }),
    });
    
    // High level
    if (response.ok) {
      cart.clear();
      navigate('/confirmation');
    }
  };
  
  return <CheckoutForm onSubmit={handleCheckout} />;
}

After: Consistent high-level abstraction

function CheckoutPage() {
  const cart = useCart();
  const { createOrder } = useOrders();
  const { shippingAddress } = useShippingAddress();
  
  const handleCheckout = async () => {
    const order = await createOrder({
      items: cart.items,
      shippingAddress,
    });
    
    if (order) {
      cart.clear();
      navigate('/confirmation');
    }
  };
  
  return <CheckoutForm onSubmit={handleCheckout} />;
}

Use Descriptive Names

Don't be afraid to make a name long. A long descriptive name is better than a short enigmatic name.

The name should describe what the function does. If you struggle to name it, that's a sign it might be doing too much.

Rails Example

Before: Vague names

def process(user)
  # What does this process?
end

def handle_data(params)
  # Handle how?
end

def do_stuff(order)
  # Very helpful...
end

def check(item)
  # Check what exactly?
end

After: Names that explain intent

def send_password_reset_email(user)
end

def normalize_address_params(params)
end

def apply_promotional_discount(order)
end

def inventory_sufficient_for?(item)
end

React Example

Before: Unclear names

function handle(e) { }
function update(data) { }
function process(items) { }
function getData() { }
function Component1() { }

After: Self-documenting names

function handleEmailChange(event: ChangeEvent<HTMLInputElement>) { }
function updateUserProfile(profileData: ProfileUpdate) { }
function calculateCartSubtotal(lineItems: CartItem[]) { }
function fetchActiveSubscriptions() { }
function SubscriptionPlanSelector() { }

Function Arguments

The ideal number of arguments is zero. Next comes one, then two. Three arguments should be avoided where possible. More than three requires special justification.

Rails Example

Before: Too many arguments

def create_shipment(order_id, carrier, service_level, weight, dimensions, 
                    signature_required, insurance_amount, saturday_delivery)
  # ...
end

# Calling code
create_shipment(
  order.id, 'fedex', 'overnight', 2.5, [10, 8, 4],
  true, 500.00, false
)

After: Using an argument object

def create_shipment(shipment_request)
  carrier = shipment_request.carrier
  # ...
end

# With a value object or struct
shipment_request = ShipmentRequest.new(
  order: order,
  carrier: 'fedex',
  service_level: 'overnight',
  package: Package.new(weight: 2.5, dimensions: [10, 8, 4]),
  options: ShippingOptions.new(
    signature_required: true,
    insurance_amount: 500.00,
    saturday_delivery: false
  )
)

create_shipment(shipment_request)

React Example

Before: Props explosion

function ProductCard({
  name,
  price,
  originalPrice,
  imageUrl,
  rating,
  reviewCount,
  inStock,
  onAddToCart,
  onAddToWishlist,
  onQuickView,
  showRating,
  showWishlist,
  variant,
}: ProductCardProps) {
  // 13 props!
}

After: Grouped into logical objects

interface ProductCardProps {
  product: Product;
  actions: ProductActions;
  display?: DisplayOptions;
}

function ProductCard({ product, actions, display = defaultDisplay }: ProductCardProps) {
  const { name, price, imageUrl } = product;
  const { onAddToCart, onAddToWishlist } = actions;
  const { showRating, variant } = display;
  
  // Much cleaner
}

// Usage
<ProductCard 
  product={product}
  actions={{ onAddToCart, onAddToWishlist, onQuickView }}
  display={{ showRating: true, variant: 'compact' }}
/>

Have No Side Effects

Side effects are lies. Your function promises to do one thing, but it also does other hidden things.

Rails Example

Before: Hidden side effect

def authenticate(username, password)
  user = User.find_by(username: username)
  return false unless user
  
  if user.valid_password?(password)
    # Hidden side effect! The method name suggests it only authenticates
    session[:user_id] = user.id
    user.update!(last_login_at: Time.current)
    true
  else
    false
  end
end

After: Explicit about what it does

def authenticate(username, password)
  user = User.find_by(username: username)
  return nil unless user&.valid_password?(password)
  
  user
end

def sign_in(user)
  session[:user_id] = user.id
  record_login(user)
end

def record_login(user)
  user.update!(last_login_at: Time.current)
end

# Usage is now explicit
if user = authenticate(username, password)
  sign_in(user)
  redirect_to dashboard_path
end

React Example

Before: Sneaky side effects

function formatCurrency(amount: number): string {
  // Side effect: logging
  console.log(`Formatting: ${amount}`);
  
  // Side effect: analytics
  analytics.track('currency_formatted', { amount });
  
  // Side effect: caching in global state
  window.__lastFormattedAmount = amount;
  
  return `$${amount.toFixed(2)}`;
}

After: Pure function

function formatCurrency(amount: number): string {
  return `$${amount.toFixed(2)}`;
}

// If you need tracking, make it explicit
function formatAndTrackCurrency(amount: number): string {
  analytics.track('currency_formatted', { amount });
  return formatCurrency(amount);
}

Command Query Separation

Functions should either do something (command) or answer something (query), but not both.

Rails Example

Before: Mixed command and query

def set_and_return_status(order, new_status)
  order.update!(status: new_status)
  order.status  # Returns something
end

# Confusing usage
if set_and_return_status(order, 'shipped') == 'shipped'
  # Did it change? Was it already shipped?
end

After: Separate command and query

# Command: changes state, returns nothing meaningful
def update_status(order, new_status)
  order.update!(status: new_status)
end

# Query: returns information, changes nothing
def current_status(order)
  order.status
end

# Clear usage
update_status(order, 'shipped')
if current_status(order) == 'shipped'
  notify_customer(order)
end

React Example

Before: Mutation returns value

function toggleAndGetVisibility(): boolean {
  setIsVisible(prev => !prev);
  return !isVisible; // This might not be what you expect due to async state
}

After: Separated

// Command
function toggleVisibility(): void {
  setIsVisible(prev => !prev);
}

// Query (derived state)
const isCurrentlyVisible = isVisible;

// Or use a callback pattern if you need the new value
function toggleVisibility(onToggled?: (newValue: boolean) => void): void {
  setIsVisible(prev => {
    const newValue = !prev;
    onToggled?.(newValue);
    return newValue;
  });
}

Prefer Exceptions to Returning Error Codes

Returning error codes means the caller must deal with the error immediately. Exceptions let you separate the happy path from error handling.

Rails Example

Before: Error codes

def create_account(params)
  return :invalid_email unless valid_email?(params[:email])
  return :email_taken if User.exists?(email: params[:email])
  return :weak_password unless strong_password?(params[:password])
  
  user = User.create!(params)
  :success
end

# Calling code becomes a mess
result = create_account(params)
case result
when :success then redirect_to dashboard_path
when :invalid_email then flash[:error] = "Invalid email"
when :email_taken then flash[:error] = "Email already registered"
when :weak_password then flash[:error] = "Password too weak"
end

After: Exceptions

def create_account(params)
  validate_email!(params[:email])
  ensure_email_available!(params[:email])
  validate_password_strength!(params[:password])
  
  User.create!(params)
end

# Clean calling code
begin
  create_account(params)
  redirect_to dashboard_path
rescue InvalidEmailError => e
  flash[:error] = e.message
  render :new
rescue EmailTakenError => e
  flash[:error] = "Email already registered"
  render :new
rescue WeakPasswordError => e
  flash[:error] = e.message
  render :new
end

React/TypeScript Example

Before: Error returns

type Result = { success: true; data: User } | { success: false; error: string };

async function fetchUser(id: string): Promise<Result> {
  try {
    const response = await api.get(`/users/${id}`);
    return { success: true, data: response.data };
  } catch {
    return { success: false, error: 'Failed to fetch user' };
  }
}

// Awkward usage
const result = await fetchUser(id);
if (result.success) {
  setUser(result.data);
} else {
  setError(result.error);
}

After: Let exceptions flow (with proper boundaries)

async function fetchUser(id: string): Promise<User> {
  const response = await api.get(`/users/${id}`);
  return response.data;
}

// With an error boundary or try/catch at the appropriate level
function UserProfile({ userId }: { userId: string }) {
  const { data: user, error, isLoading } = useQuery(
    ['user', userId],
    () => fetchUser(userId)
  );
  
  if (error) return <ErrorDisplay error={error} />;
  if (isLoading) return <Spinner />;
  
  return <ProfileCard user={user} />;
}

Don't Repeat Yourself (DRY)

Duplication is the root of all evil in software. Every piece of knowledge should have a single, unambiguous representation in the system.

Rails Example

Before: Duplicated logic

class OrdersController < ApplicationController
  def create
    @order = Order.new(order_params)
    
    # Calculate total
    subtotal = @order.line_items.sum { |i| i.price * i.quantity }
    discount = @order.coupon&.calculate_discount(subtotal) || 0
    tax = (subtotal - discount) * 0.08
    @order.total = subtotal - discount + tax
    
    @order.save!
  end
end

class Order < ApplicationRecord
  def recalculate_total
    # Same logic repeated!
    subtotal = line_items.sum { |i| i.price * i.quantity }
    discount = coupon&.calculate_discount(subtotal) || 0
    tax = (subtotal - discount) * 0.08
    self.total = subtotal - discount + tax
    save!
  end
end

After: Single source of truth

class OrderTotalCalculator
  def initialize(order)
    @order = order
  end
  
  def calculate
    subtotal - discount + tax
  end
  
  def subtotal
    @order.line_items.sum { |i| i.price * i.quantity }
  end
  
  def discount
    @order.coupon&.calculate_discount(subtotal) || 0
  end
  
  def tax
    (subtotal - discount) * TAX_RATE
  end
  
  private
  
  TAX_RATE = 0.08
end

# Used everywhere
@order.total = OrderTotalCalculator.new(@order).calculate

React Example

Before: Copy-pasted validation

function RegistrationForm() {
  const validateEmail = (email: string) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  };
  // ...
}

function ProfileForm() {
  const validateEmail = (email: string) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  };
  // ...
}

function InviteForm() {
  const validateEmail = (email: string) => {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  };
  // ...
}

After: Shared utility

// utils/validation.ts
export function isValidEmail(email: string): boolean {
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return EMAIL_REGEX.test(email);
}

// Or as a custom hook for form integration
export function useEmailValidation() {
  const validate = useCallback((email: string) => {
    if (!email) return 'Email is required';
    if (!isValidEmail(email)) return 'Invalid email format';
    return undefined;
  }, []);
  
  return { validate };
}

Conclusion

Martin's principles for writing clean functions are timeless. Whether you're building Rails APIs or React interfaces, the rules remain the same:

  1. Keep functions small and focused
  2. Make them do one thing well
  3. Maintain consistent abstraction levels
  4. Choose names that reveal intent
  5. Minimize arguments
  6. Avoid side effects
  7. Separate commands from queries
  8. Use exceptions over error codes
  9. Eliminate duplication

The goal isn't to follow these rules dogmatically, but to write code that communicates clearly. When your functions are small, well-named, and focused, your codebase becomes a joy to work in rather than a chore to maintain.


This is part of a series on Clean Code principles with Rails and React examples. See also: Chapter 1: Clean Code and Chapter 2: Meaningful Names.

No comments:

Post a Comment

Clean Code Chapter 3: Functions

  Clean Code Chapter 3: Functions Writing Functions That Tell a Story In Chapter 3 of Clean Code , Robert C. Martin lays out a compelling ...