Code smellObject-Orientation Abusers
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.
Signs
- Two classes have methods that do the same thing under different names:
sendSms(to, text)anddeliver(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.
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.
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 so methods that mean the same thing share a name.
- Align signatures with Add Parameter and Parameterize Method until the methods take and return the same shapes.
- If only part of a class matches the other, use Move Method to relocate behavior so the matching parts line up.
- Once the methods match, use Extract Interface so callers depend on the shared contract.
- If the implementations also share real code, use Extract Superclass 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 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.
Treatments
- Rename MethodChange a method's name to say what it does, using a forwarding alias when callers cannot all be updated at once.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Add ParameterGive a method a new parameter when it needs data it cannot reach, migrating callers through a temporary overload or default value.
- Parameterize MethodMerge several methods that do the same thing with different literal values into one method that takes the value as a parameter.
- Extract SuperclassCreate a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
- Extract InterfaceDeclare the subset of a class's methods that clients actually use as an interface, so clients depend on the contract instead of the class.
See also
- Duplicate CodeThe same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
- Refused BequestA subclass inherits methods or data it does not want, overriding them to throw or do nothing, so the hierarchy lies about what it can do.
- Switch StatementsThe same switch or if-chain over a type code is repeated across the codebase, so adding one case means editing every copy.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.