• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

Stop Overengineering in the Name of Clean Architecture

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
1,481
Баллы
155
Clean Architecture is a great concept. It’s meant to help you write maintainable, modular, and scalable software.

But too many developers treat it like a religion. They follow it blindly, stuffing projects with unnecessary layers, abstractions, and patterns. All in the name of “clean code.”

Let’s be honest, Clean Architecture is often overused, not because the ideas are bad, but because people overengineer the implementation.

A Quick Joke (That Some Devs Write Unironically)


Here’s an example of multiplying two numbers implemented with an absurd number of patterns:


// overengineered-multiplier.ts

// Interface
interface IMultiplier {
multiply(a: number, b: number): number;
}

// Singleton
class MultiplierService implements IMultiplier {
private static instance: MultiplierService;

private constructor() {}

static getInstance(): MultiplierService {
if (!MultiplierService.instance) {
MultiplierService.instance = new MultiplierService();
}
return MultiplierService.instance;
}

multiply(a: number, b: number): number {
return a * b;
}
}

// Adapter
interface IMultiplierInput {
x: number;
y: number;
}

class InputAdapter {
constructor(private readonly rawInput: [number, number]) {}

adapt(): IMultiplierInput {
return { x: this.rawInput[0], y: this.rawInput[1] };
}
}

// Decorator
class LoggingMultiplierDecorator implements IMultiplier {
constructor(private readonly inner: IMultiplier) {}

multiply(a: number, b: number): number {
console.log(`Logging: Multiplying ${a} * ${b}`);
const result = this.inner.multiply(a, b);
console.log(`Logging: Result is ${result}`);
return result;
}
}

// Proxy
class MultiplierProxy implements IMultiplier {
constructor(private readonly target: IMultiplier) {}

multiply(a: number, b: number): number {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Invalid input types');
}
return this.target.multiply(a, b);
}
}

// Abstract Factory
interface IMultiplierFactory {
create(): IMultiplier;
}

class RealMultiplierFactory implements IMultiplierFactory {
create(): IMultiplier {
const singleton = MultiplierService.getInstance();
const withLogging = new LoggingMultiplierDecorator(singleton);
const withProxy = new MultiplierProxy(withLogging);
return withProxy;
}
}

// Usage
const rawInput: [number, number] = [6, 9];
const adapted = new InputAdapter(rawInput).adapt();

const factory = new RealMultiplierFactory();
const multiplier = factory.create();

const result = multiplier.multiply(adapted.x, adapted.y);
console.log(`Final Result: ${result}`);

All of this just to calculate 6 * 9. This is what happens when design patterns become cosplay.

The Real Problem: Over-Abstraction


Clean Architecture promotes decoupling. That’s good. But too many devs interpret that as "abstract everything."

Example:


interface IUserRepository {
findById(id: string): Promise<UserDTO>;
}

class UserRepositoryImpl implements IUserRepository {
constructor(private db: PrismaClient) {}

async findById(id: string): Promise<UserDTO> {
return await this.db.user.findUnique({ where: { id } });
}
}

Then you add a use case on top:


class GetUserByIdUseCase {
constructor(private repo: IUserRepository) {}

async execute(id: string): Promise<UserDTO> {
return this.repo.findById(id);
}
}

All of this for one database call. This isn’t clean it’s waste.

Common Overengineering Patterns

1. Interfaces and Implementations for Everything


Blindly creating an interface and a class for every service adds friction without real value.

Use it when:

  • You actually have multiple implementations
  • You’re building a plugin system or SDK

Don’t use it when:

  • You only have one implementation
  • You’re doing it "just in case"
2. Use Case Classes for Simple Logic


class CreatePostUseCase {
execute(input: CreatePostDTO): Promise<PostDTO> {
return this.postRepo.create(input);
}
}

This is a one-line call. Wrapping it in a class adds nothing. Use a service or method directly unless business logic really requires separation.

3. DTO Explosion


type Post = { id: string; title: string; content: string };
type CreatePostDTO = { title: string; content: string };
type PostResponseDTO = { id: string; title: string; content: string };

If the structure is the same across layers, reuse the object. You don’t need a new DTO for every transition unless there’s a reason.

4. Domain Models with No Behavior


class User {
constructor(public id: string, public name: string) {}
}

If your "domain entity" is just a data wrapper, it’s not a domain model. Add behavior or don’t abstract.

5. Dependency Injection Overkill


@Module({
providers: [
{ provide: IUserService, useClass: UserServiceImpl },
{ provide: IUserRepository, useClass: UserRepositoryImpl }
]
})

DI containers are great. But if you’re not benefiting from polymorphism or dynamic injection, just instantiate the class.

What You Should Do Instead

  • Start simple. Build complexity only when needed.
  • Abstract with intent. Not everything needs to be swappable.
  • Refactor later. Let the structure grow organically as the app gets more complex.
  • Optimize for clarity. Not for pleasing architecture diagrams.
Clean Code Is Not More Code


Good code is:

  • Easy to read
  • Easy to test
  • Easy to change

It’s not defined by how many layers, decorators, or design patterns it uses.

Final Thoughts


Clean Architecture should serve your code, not the other way around.

It’s a tool, not a rulebook. Use it to solve complexity not to create it.

TLDR

Avoid ThisDo This Instead
Interface and Impl for everythingUse one class until you actually need two
Use cases for CRUD logicUse simple services or methods
Separate DTOs for same shapesReuse types where possible
DI for everythingInstantiate directly when practical
Layers for layers’ sakeLet structure grow with real needs

Build software, not monuments.


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу