Как стать автором
Обновить

(Не)рушимые законы крутого кода: Law of Demeter (с примерами на TypeScript)

JavaScript *Анализ и проектирование систем *Проектирование и рефакторинг *ООП *TypeScript *
Когда я узнал об этих принципах, гибкость моего кода, по ощущениям, выросла х2, а скорость принятия решения по дизайну сущностей х5.

Если SOLID – это набор принципов написания качественного кода, то Law of Demeter (LoD) и Tell Don’t Ask (TDA) – это конкретные приемы как добиться SOLID.

Сегодня поговорим про Law of Demeter («Закон Деметры»).

Утрированно


Это принцип помогает определиться: «Как я буду получать / изменять вложенные объекты» – применим в языках, где можно определить «классы» со свойствами и методами.

Часто складывается ситуация, когда мы откуда-то (например, из HTTP запроса) получили id сущности `a`, пошли за ней в БД и из сущности `a` нам надо получить / изменить сущность `b` вызвав метод `Method`.

Так вот Википедия гласит:
Код `a.b.Method()` нарушает Закон Деметры, а код `a.Method()` является корректным.

Пример


У Пользователя есть Посты, в которых есть Комментарии. Вы хотите получить «Комментарии последнего Поста».

Можно запилить такое:

const posts = user.posts
const lastPostComments = posts[posts.length-1].comments

Или вот такое:

const userLastPostComments = user.getPosts().getLast().getComments()

Проблема: код знает о всей иерархии вложенных данных и если эта иерархия меняется / расширяется, везде, где был вызвана вот эта цепочка, придется вносить изменения (рефакторить код + тесты).

Чтобы решить проблему, применяем LoD:

const userLastPostComments = user.getLastPostComments()

А вот уже в `User` пишем:

class User {
  // ...
  getLastPostComments(): Comments {
    return this.posts.getLastComments()
  }
  // ...
}

Та же самая история с добавлением комментария. Мы из:

const newComment = new Comment(req.body.postid, req.body.content)
user.getPosts().addComment(newComment)

Или, если хочется блеснуть диагнозом:

const newComment = new Comment(req.body.postid, req.body.content)
const posts = user.posts
posts[posts.length-1].comments.push(newComment)

Превращаем вот в это:

const posts = user.addCommentToPost(req.body.postid, req.body.content)

А у `User`:

class User {
  // ...
  addCommentToPost(postId: string, content: string): void {
    // The cleanest
    const post = this.posts.getById(postId)
    return post.addComment(content)
  }
  // ...
}


Уточнение: `new Comment` можно создать и вне `user` или `post`, все зависит от того, как устроена логика вашего приложения, но чем ближе к владельцу сущности (в данном случае, `post`), тем лучше.

Что это дает?


Мы скрываем детали реализации, и если когда-нибудь произойдет изменение / расширение иерархии (например, посты начнут лежать не только в свойстве `posts`) вам придется только отрефакторить метод `getLastPostComments` / `addCommentToPost` и переписать юнит-тест только этого метода.

В чем минусы


Много доп. кода.

В небольших проектах большая часть методов окажутся просто `getter` / `setter`.

Когда использовать


(1) LoD хорош для Моделей, Сущностей, Агрегатов или классов, которые имеют глубокие / сложные / требующие мерджа связи.

(2) Код требует понятия: «Получить комментарии последнего поста» – а посты у вас не в 1-ом свойстве, а в 2-х и более, тогда, точно нужно делать метод `getLastPostComments` и уже там мерджить несколько свойств с разными постами.

(3) Я стараюсь максимально часто использовать этот принцип, когда речь идет о трансформации (изменение, создание, удаление) данных. И реже, когда речь о получении данных.

(4) Основываясь на здравом смысле.

Лайфхак


Многие начали переживать из-за кол-ва proxy методов, поэтому вот упрощение:

Чтобы не создавать бесконечное кол-во proxy методов, LoD можно использовать в классе самого верхнего уровня (в нашем случае `User`), а внутри имплементации уже нарушить закон. Например:

const userLastPostComments = user.getLastPostComments()

class User {
  // ...
  getLastPostComments(): Comments {
    // Сильно нарушили LoD, но уменьшили код Post и сохранили основное преимущество
    const lastPost = this.posts[this.posts.length-1]
    return lastPost.comments
  }
  // ...
}


Со временем, если `post` будет расти, можно будет сделать ему методы `getLast()` или `getLastComments()` и это не повлечет море рефакторинга.

Что для этого нужно


LoD хорошо работает в том случае, если у вас правильно сделано дерево / иерархия зависимостей сущностей.

Что почитать


(1) https://qna.habr.com/q/44822
Обязательно почитать все комментарии и комментарии комментариев (до коммента Вячеслав Голованов SLY_G), помня, что там и правильные и неправильные примеры
(2) https://ru.wikipedia.org/wiki/Закон_Деметры
(3) Статья

P.S.


Я мог напортачить с некоторыми деталями / примерами или объяснить недостаточно понятно, поэтому отпишитесь в комментарии, что заметили, я внесу изменения. Всем добра.

P.P.S.


Почитайте вот эту линейку комментариев, в ней мы с lair подробнее разбираем неполностью освещенный кейс в данной статье
Теги:
Хабы:
Всего голосов 20: ↑12 и ↓8 +4
Просмотры 6.5K
Комментарии Комментарии 68