DEV Community

Rohit Gavali
Rohit Gavali

Posted on

What I Learned After Letting Different AI Models Refactor the Same Function

I had a function that bothered me. Not broken—just inelegant. 200 lines of nested conditionals handling user permissions across three different access levels with special cases for admin overrides and temporary grants.

It worked. Tests passed. But every time I looked at it, I knew it could be better.

So I did something unusual. I asked five different AI models to refactor it. Same function, same context, same instruction: "Make this better."

What I got back revealed something fundamental about how different AI systems think about code—and exposed assumptions I didn't know I was making about what "better" even means.

The Function That Started It

The original code looked like this (simplified for clarity):

function checkPermission(user, resource, action) {
  if (user.role === 'admin') {
    return true;
  }

  if (user.temporaryGrants) {
    const grant = user.temporaryGrants.find(g => 
      g.resource === resource && 
      g.action === action &&
      new Date() < new Date(g.expiresAt)
    );
    if (grant) return true;
  }

  if (user.role === 'editor') {
    if (action === 'read' || action === 'write') {
      if (resource.type === 'document' || resource.type === 'draft') {
        if (resource.ownerId === user.id || resource.sharedWith?.includes(user.id)) {
          return true;
        }
      }
    }
    if (action === 'read' && resource.public) {
      return true;
    }
  }

  if (user.role === 'viewer') {
    if (action === 'read') {
      if (resource.public || resource.sharedWith?.includes(user.id)) {
        return true;
      }
    }
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

Functional, but the nested conditionals obscured the actual permission logic. Each model saw this differently.

What Claude Focused On

When I fed this to Claude Opus 4.6, it took a strategy-first approach. It didn't just refactor—it restructured around permission strategies.

Claude's version introduced a permission strategy pattern:

const permissionStrategies = {
  admin: () => true,

  temporaryGrant: (user, resource, action) => {
    return user.temporaryGrants?.some(grant =>
      grant.resource === resource &&
      grant.action === action &&
      !isExpired(grant.expiresAt)
    ) ?? false;
  },

  editor: (user, resource, action) => {
    const canEdit = ['read', 'write'].includes(action) &&
      ['document', 'draft'].includes(resource.type) &&
      (resource.ownerId === user.id || resource.sharedWith?.includes(user.id));

    const canReadPublic = action === 'read' && resource.public;

    return canEdit || canReadPublic;
  },

  viewer: (user, resource, action) => {
    return action === 'read' && 
      (resource.public || resource.sharedWith?.includes(user.id));
  }
};

function checkPermission(user, resource, action) {
  const strategies = [
    () => user.role === 'admin' && permissionStrategies.admin(),
    () => permissionStrategies.temporaryGrant(user, resource, action),
    () => permissionStrategies[user.role]?.(user, resource, action)
  ];

  return strategies.some(strategy => strategy());
}
Enter fullscreen mode Exit fullscreen mode

What struck me: Claude optimized for conceptual clarity. Each permission type became explicit. The code was longer, but the logic was clearer. If someone asked "how do editor permissions work?", you could point to a single function.

But there was a tradeoff. The strategy pattern added abstraction overhead. For a function this size, was the pattern worth it? Claude thought in terms of extensibility and maintainability. It assumed this code would grow.

What Gemini Prioritized

Gemini 3.1 Pro took a completely different approach. It focused on data-driven configuration.

Instead of encoding permission logic in code, Gemini extracted it into a declarative structure:

const permissionRules = {
  admin: { allowAll: true },

  editor: {
    read: {
      documentTypes: ['document', 'draft'],
      conditions: ['isOwner', 'isSharedWith', 'isPublic']
    },
    write: {
      documentTypes: ['document', 'draft'],
      conditions: ['isOwner', 'isSharedWith']
    }
  },

  viewer: {
    read: {
      conditions: ['isPublic', 'isSharedWith']
    }
  }
};

const conditions = {
  isOwner: (user, resource) => resource.ownerId === user.id,
  isSharedWith: (user, resource) => resource.sharedWith?.includes(user.id),
  isPublic: (user, resource) => resource.public
};

function checkPermission(user, resource, action) {
  // First check temporary grants
  if (hasValidTemporaryGrant(user, resource, action)) {
    return true;
  }

  const rules = permissionRules[user.role];
  if (!rules) return false;
  if (rules.allowAll) return true;

  const actionRules = rules[action];
  if (!actionRules) return false;

  // Check document type if specified
  if (actionRules.documentTypes && 
      !actionRules.documentTypes.includes(resource.type)) {
    return false;
  }

  // Check if any condition is satisfied
  return actionRules.conditions.some(conditionName =>
    conditions[conditionName](user, resource)
  );
}
Enter fullscreen mode Exit fullscreen mode

Gemini was optimizing for configurability. Want to add a new role? Update the config. Change permission logic? Modify the rules object. The code itself barely needs to change.

This approach would shine in systems where permissions evolve frequently or need to be managed by non-developers. But it introduced indirection—to understand what permissions a viewer has, you need to trace through the config object and condition functions.

What GPT Emphasized

GPT-5.4 went minimal. It focused on reducing cognitive load through simplification and early returns.

function checkPermission(user, resource, action) {
  // Admin access
  if (user.role === 'admin') return true;

  // Temporary grants
  if (hasActiveTemporaryGrant(user, resource, action)) return true;

  // Public read access
  if (action === 'read' && resource.public) return true;

  // Shared resource access
  const hasSharedAccess = resource.sharedWith?.includes(user.id);

  // Editor permissions
  if (user.role === 'editor') {
    const isEditableType = ['document', 'draft'].includes(resource.type);
    const isOwner = resource.ownerId === user.id;
    const canModify = ['read', 'write'].includes(action);

    return canModify && isEditableType && (isOwner || hasSharedAccess);
  }

  // Viewer permissions
  if (user.role === 'viewer') {
    return action === 'read' && hasSharedAccess;
  }

  return false;
}

function hasActiveTemporaryGrant(user, resource, action) {
  return user.temporaryGrants?.some(grant =>
    grant.resource === resource &&
    grant.action === action &&
    new Date() < new Date(grant.expiresAt)
  ) ?? false;
}
Enter fullscreen mode Exit fullscreen mode

GPT extracted one helper function and flattened the rest. It optimized for readability through simplicity. No patterns, no config objects, just straightforward conditional logic with good naming.

This version was easiest to read linearly. But it wouldn't scale well. Adding a fourth role means adding another conditional block. The logic is inline, which makes it clear but also harder to reuse or test in isolation.

What The Differences Revealed

Each model made implicit assumptions about what "better" meant:

Claude assumed the code would grow. It optimized for future extensibility even though the current requirements didn't demand it. The strategy pattern adds complexity now to make changes easier later.

Gemini assumed the logic would change frequently. It separated logic from code, optimizing for configurability. This is brilliant if permissions need to be modified by non-developers or if you're building a multi-tenant system where each tenant defines their own rules.

GPT assumed simplicity was the highest virtue. It reduced abstraction, making the code as straightforward as possible. This works great for stable, well-understood requirements that won't grow much.

None of these approaches is objectively better. They're optimized for different futures.

The Assumptions I Didn't Know I Had

Watching AI models refactor the same code exposed my own biases.

I initially favored Claude's approach because I value extensibility. I've been burned by rigid code that became painful to extend. But that's my history, not necessarily this code's future.

The Gemini approach made me uncomfortable because I've seen over-engineered configuration systems that became harder to understand than code. But I've also seen systems where separating logic from code was exactly right.

The GPT approach felt too simple at first. Then I realized that was internalized complexity bias—the assumption that good code must involve some sophisticated abstraction. Sometimes the simple solution is actually the right one.

What This Means For Using AI To Refactor

Different AI models have different philosophies about code quality, and those philosophies reflect different tradeoffs:

Some models optimize for future flexibility. They'll add abstractions that make the code more complex now but easier to extend later. Great if you're building something that will evolve. Overkill if you're not.

Some models optimize for separation of concerns. They'll extract configuration, create clear boundaries, and make components testable in isolation. Valuable for complex systems. Unnecessary overhead for simple ones.

Some models optimize for immediate clarity. They'll keep things simple and readable even if it means sacrificing some extensibility. Perfect for stable code. Limiting for code that needs to grow.

When you ask an AI to refactor code, you're not just getting a technical transformation—you're getting a philosophy about what makes code good. Understanding which philosophy fits your actual needs is more important than accepting whatever the AI suggests.

How I Actually Use Multiple Models Now

I don't ask one AI to refactor and accept the result. I ask several and compare their approaches.

Using platforms like Crompt AI that let you work with multiple AI models side-by-side, I can see different perspectives on the same code simultaneously. Not to find the "right" answer, but to understand the tradeoffs.

When refactoring now, I ask myself:

  • How likely is this code to change?
  • What kind of changes will it face?
  • Who will maintain it?
  • What's the cost of added abstraction?
  • What's the cost of missing abstraction?

Then I look at which AI approach optimizes for my actual constraints, not theoretical best practices.

Sometimes I take Claude's strategy pattern because I know the permission system will grow. Sometimes I take GPT's simplicity because the requirements are stable and the team values clarity. Sometimes I take Gemini's config-driven approach because permissions actually do need to be managed separately from code.

And sometimes I take elements from multiple approaches, using the AI suggestions as a menu of options rather than a prescription.

The Pattern Recognition Problem

The most interesting thing I learned: AI models are pattern matchers, and they match different patterns.

Claude sees permission code and matches it to strategy patterns it's seen work well in large systems. Gemini sees permission code and matches it to configuration-driven systems that provide flexibility. GPT sees permission code and matches it to straightforward implementations that prioritize readability.

None of them asked about my specific constraints. They can't—they don't know if this is a startup prototype that will change daily or a stable enterprise system that will run unchanged for years.

This is why blindly accepting AI refactoring suggestions is dangerous. The AI is optimizing for patterns it's seen succeed in its training data, not for your specific context.

What You Should Do

Next time you're tempted to ask AI to refactor your code:

Ask multiple models. Compare their approaches. Notice what each one optimizes for. Then make a conscious decision about which tradeoffs align with your actual needs.

Don't treat AI as an oracle that knows the "right" way to structure code. Treat it as a source of different perspectives on what "better" could mean.

The value isn't in getting one perfect refactoring. It's in seeing multiple valid approaches and understanding the philosophical differences between them.

Your code's future depends on constraints the AI doesn't know: how often requirements change, who maintains the code, how the system will evolve. Choose the approach that fits your actual constraints, not the one that sounds most sophisticated.

Sometimes that means taking the simple version and resisting the urge to over-engineer. Sometimes it means accepting extra abstraction because you know complexity is coming. Sometimes it means splitting the difference.

The AI gives you options. The judgment about which option fits your context? That's still on you.

-ROHIT

Top comments (0)