# Alternative Classes with Different Interfaces

> Two classes do the same job but expose different method names or signatures, so callers cannot swap one for the other.

- kind: smell
- category: oo-abusers
- canonical: https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces
- inspired-by: https://refactoring.guru/smells/alternative-classes-with-different-interfaces

## Signs

- Two classes have methods that do the same thing under different names: `sendSms(to, text)` and `deliver(message)`.
- Callers contain `if (provider === "a") a.foo() else b.bar()` to bridge the difference.
- The implementations duplicate logic such as retries, rate limiting or formatting.
- One class was written later by someone who did not know the other existed.

## Why it happens

Two teams or two people solve the same problem at different times. A vendor switch or a second payment provider is added next to the first rather than behind a shared contract. Each class gets names that fit the vendor's own API, so the interfaces diverge even though the intent is identical.

## Example

Two SMS providers integrated separately.

```ts title="sms.ts"
class TwilioClient {
  async sendSms(toNumber: string, body: string): Promise<string> { /* returns message SID */ return ""; }
}

class VonageClient {
  async deliver(msg: { recipient: string; text: string; from?: string }): Promise<{ id: string }> {
    return { id: "" };
  }
}

async function sendVerificationCode(phone: string, code: string, region: string) {
  const text = `Your code is ${code}`;
  if (region === "EU") {
    const res = await new VonageClient().deliver({ recipient: phone, text });
    return res.id;
  }
  return new TwilioClient().sendSms(phone, text);
}
```

One interface with matching signatures removes the branch from callers.

```ts title="sms.ts"
interface SmsSender {
  send(to: string, text: string): Promise<string>;
}

class TwilioSender implements SmsSender {
  async send(to: string, text: string): Promise<string> { /* ... */ return ""; }
}

class VonageSender implements SmsSender {
  async send(to: string, text: string): Promise<string> {
    const res = await vonage.deliver({ recipient: to, text });
    return res.id;
  }
}

async function sendVerificationCode(sender: SmsSender, phone: string, code: string) {
  return sender.send(phone, `Your code is ${code}`);
}
```

## Treatment

- Start with [Rename Method](https://aipatterns.dev/refactoring/techniques/rename-method.md) so methods that mean the same thing share a name.
- Align signatures with [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md) and [Parameterize Method](https://aipatterns.dev/refactoring/techniques/parameterize-method.md) until the methods take and return the same shapes.
- If only part of a class matches the other, use [Move Method](https://aipatterns.dev/refactoring/techniques/move-method.md) to relocate behavior so the matching parts line up.
- Once the methods match, use [Extract Interface](https://aipatterns.dev/refactoring/techniques/extract-interface.md) so callers depend on the shared contract.
- If the implementations also share real code, use [Extract Superclass](https://aipatterns.dev/refactoring/techniques/extract-superclass.md) to hold it. Prefer an interface plus a shared helper when the classes wrap different third-party SDKs.
- If one class is a strict subset of the other, delete it and redirect its callers.

## Payoff

- Callers can swap implementations without branching, which makes tests with fakes straightforward.
- Duplicate logic around the call (retry, logging) collapses into one place.
- Adding a third provider means one new class, not another branch in each caller.

## When to ignore

- The classes look alike but serve different domains that will diverge, such as transactional email and marketing email with different compliance rules.
- The classes live in separate libraries you do not control. Wrap them with an [Adapter](https://aipatterns.dev/design-patterns/adapter.md) in your code instead of trying to change them.
- Only one caller uses both, and it is about to be deleted.

## Agent checklist

- [ ] You confirmed the two classes have the same intent and the same failure semantics, not only similar names.
- [ ] Renames are applied across every caller, including dynamic lookups and string references.
- [ ] The extracted interface contains only methods every implementation supports fully.
- [ ] Callers receive the interface through a parameter or constructor rather than constructing a concrete class.
- [ ] Each implementation passes the same contract test suite.
