abstract class BaseService {
protected logger = new Logger();
protected cache = new RedisCache();
protected db = getDb();
protected async cached<T>(key: string, fn: () => Promise<T>) { /* ... */ }
protected paginate(q: Query, page: number) { /* ... */ }
}
class InvoiceService extends BaseService { /* uses only db */ }
This is a "god base class". It uses inheritance only to share helpers, and it causes several problems:
- Every service gets everything.
InvoiceServiceneeds onlydb, but it still connects to Redis. - Hidden dependencies. The constructor shows nothing. You must read the base class to know what a service uses.
- Hard to test.
new Logger()andgetDb()run inside the base, so you can't pass in fakes. - Fragile. One change to
BaseServicetouches every service.
Refactor with composition:
- Turn each helper into its own small class:
Logger,Cache,Paginator. - Inject only what each service needs:
constructor(private db: Db, private logger: Logger) {}. - Turn pure helpers like
paginateinto plain exported functions. - Move one service at a time. Delete
BaseServicewhen nothing extends it.
After this, each constructor is an honest list of dependencies. Tests can pass small fakes.
- A base class used only to share helpers gives every subclass dependencies it does not need.
- Dependencies hidden in a base class are not visible in the constructor.
- Helpers created inside the base class cannot be replaced in tests.
- Extract each helper and inject only what each service uses.
- Refactor one service at a time until the base class can be deleted.