(Un) breakable laws of cool code: The Law of Demeter (with examples in TypeScript)

When I learned about these principles, the flexibility of my code felt x2, and the speed of decision making on the design of entities x5.



If SOLID is a set of principles for writing quality code, then Law of Demeter (LoD) and Tell Don't Ask (TDA) are specific tricks for achieving SOLID.



Today we will talk about the Law of Demeter ("Law of Demeter").



Exaggerated



This principle helps to determine: "How will I get / modify nested objects" - applicable in languages โ€‹โ€‹where you can define "classes" with properties and methods.



Often there is a situation when we from somewhere (for example, from an HTTP request) received the id of the entity `a`, followed it to the database and from the entity` a` we need to get / change the entity `b` by calling the method` Method`.



So Wikipedia says:

The `abMethod ()` code violates the Law of Demeter, and the `a.Method ()` code is correct.


Example



The User has Posts that have Comments. You want to receive "Last Post Comments".



You can file this:



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


Or like this:



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


Problem: the code knows about the entire hierarchy of nested data and if this hierarchy changes / expands, wherever this chain was called, you will have to make changes (refactor the code + tests).



To solve the problem, apply LoD:



const userLastPostComments = user.getLastPostComments()


But already in ` User` we write:



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


The same story with the addition of a comment. We are from:



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


Or, if you want to show off your diagnosis:



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


Turning this into this:



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


And for ` User` :



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




Clarification: ` new Comment` can be created outside ` user` or ` post` , it all depends on how the logic of your application is arranged, but the closer to the owner of the entity (in this case, ` post` ), the better.



What does it do?



We hide the implementation details, and if ever there is a change / hierarchy extension (for example, positions will lie not only in the `property of the posts ') you only have to refactor the method` getLastPostComments `/` addCommentToPost `and rewrite unit test only this method.



What are the cons



A lot of extra code.



In small projects, most of the methods will be just ` getter` /` setter` .



When to use



(1) LoD is good for Models, Entities, Aggregates or Classes that have deep / complex / merged connections.



(2) The code requires the concept: "Get the comments of the last post" - and your posts are not in the 1st property, but in 2 or more, then, for sure, you need to make the ` getLastPostComments` method and merge several properties with different posts.



(3) I try to use this principle as often as possible when it comes to transforming (changing, creating, deleting) data. And less often when it comes to retrieving data.



(4) Based on common sense.



Life hack



Many began to worry about the number of proxy methods, so here's a simplification:



In order not to create an infinite number of proxy methods, LoD can be used in the top-level class (in our case, `User`), and inside the implementation it is already breaking the law. For instance:



const userLastPostComments = user.getLastPostComments()

class User {
  // ...
  getLastPostComments(): Comments {
    //   LoD,    Post    
    const lastPost = this.posts[this.posts.length-1]
    return lastPost.comments
  }
  // ...
}




Over time, if `post` grows, it will be possible to make it methods` getLast () `or` getLastComments () `and this will not entail a lot of refactoring.



What is needed for this



LoD works well if you have a proper entity dependency tree / hierarchy.



What to read



(1) https://qna.habr.com/q/44822

Be sure to read all comments and comments of comments (before the comment Vyacheslav GolovanovSLY_G), remembering that there are both correct and incorrect examples

(2) https://ru.wikipedia.org/wiki/Zakon_Demeter

(3) Article



PS



I could screw up with some details / examples or explain it is not clear enough, so write down in the comments that you noticed, I will make changes. All good.



PPS



Read this line of comments , in it welair we disassemble an incompletely lit case in more detail in this article



All Articles