Whats the difference between using a class and interface?

放肆的年华 提交于 2019-11-26 14:38:28

问题


What is the difference between doing this

export class Comment {
  likes: string;
  comment: string;

  constructor(likes: string, comment: string){
    this.comment = comment;
    this.likes = likes;
  }
}

and this

export interface CommentInterface {
  likes: string;
  comment: string;
}

in relation to declaring an observable type

register: Observable<CommentInterface[]> {
    return this.http.get()
}

回答1:


As JB Nizet quite correctly points out, the deserialized JSON values that result from HTTP requests will never be instances of a class.

While the dual role (see below) of the class construct in TypeScript makes it possible to use a class to describe the shape of these response values, it is a poor practice because the response text will be deserialized into plain JavaScript objects.

Note that throughout this answer I do not use a type for Comment.likes because in the question you have it as a string, but it feels like a number to me, so I leave it up the reader.

Class declarations in JavaScript and TypeScript:

In JavaScript, a class declaration

class Comment {
  constructor(likes, comment) {
    this.likes = likes;
    this.comment = comment;
  }
}

creates a value that can be instantiated using new to act as what is essentially a factory.

In TypeScript, a class declaration creates two things.

The first is the exact same JavaScript class value described above. The second is a type that describes the structure of the instances created by writing

new Comment(4, 'I love your essays')

That second artifact, the type, can then be used as a type annotation such as in your example of

register(): Observable<Comment[]> {
    return this.http.get()
}

which says that register returns an Observable of Arrays of Comment class instances.

Now imagine your HTTP request returns the following JSON

[
  {
    "likes": 4,
    "comment": "I love you oh so very much"
  },
  {
    "likes": 1,
    "comment": "I lust after that feeling of approval that only likes can bring"
  }
]

However the method declaration

register(): Observable<Comment[]>;

while it correctly allows callers to write

register().subscribe(comments => {
  for (const comment of comment) {
    if (comment.likes > 0) {
      likedComments.push(comment);
    }
  }
});

which is all well and good, it unfortunately also allows callers to write code like

getComments() {
  register().subscribe(comments => {
    this.comments = comments;
  });
}

getTopComment() {
  const [topComment] = this.comments.slice().sort((x, y) => y < x);
  // since there might not be any comments, it is likely that a check will be made here
  if (topComment instanceof Comment) { // always false at runtime
    return topComment;
  }
}

Since comments are not actually instances of the Comment class the above check will always fail and hence there is a bug in the code. However, TypeScript will not catch the error because we said that comments is an array of instances of the Comment class and that would make the check valid (recall that the response.json() returns any which can be converted to any type without warnings so everything appears fine at compile time).

If, however we had declared comment as an interface

interface Comment {
  comment: string;
  likes;
}

then getComments will continue to type check, because it is in fact correct code, but getTopComment will raise an error at compile time in the if statement because, as noted by many others, an interface, being a compile time only construct, can not be used as if it were a constructor to perform an instanceof check. The compiler will tell us we have an error.


Remarks:

In addition to all the other reasons given, in my opinion, when you have something that represents plain old data in JavaScript/TypeScript, using a class is usually overkill. It creates a function with a prototype and has a bunch of other aspects that we do not likely need or care about.

It also throws away benefits that you get by default if you use objects. These benefits include syntactic sugar for creating and copying objects and TypeScript's inference of the types of these objects.

Consider

import Comment from 'app/comment';

export default class CommentService {    

  async getComments(): Promse<Array<Comment>> {
    const response = await fetch('api/comments', {httpMethod: 'GET'});
    const comments = await response.json();
    return comments as Comment[]; // just being explicit.
  }

  async createComment(comment: Comment): Promise<Comment> {
    const response = await fetch('api/comments', {
      httpMethod: 'POST',
      body: JSON.stringify(comment)
    });
    const result = await response.json();
    return result as Comment; // just being explicit.
  }
}

If Comment is an interface and I want to use the above service to create a comment, I can do it as follows

import CommentService from 'app/comment-service';

export async function createComment(likes, comment: string) {    
  const commentService = new CommentService();

  await commentService.createCommnet({comment, likes});
}

If Comment were a class, I would need to introduce some boiler plate by necessitating the import of Comment. Naturally, this also increases coupling.

import CommentService from 'app/comment-service';
import Comment from 'app/comment';

export async function createComment(likes, comment: string) {    
  const commentService = new CommentService();

  const comment = new Comment(likes, comment); // better get the order right

  await commentService.createCommnet(comment);
}

That is two extra lines, and one involves depending on another module just to create an object.

Now if Comment is an interface, but I want a sophisticated class that does validation and whatnot before I give it to my service, I can still have that as well.

import CommentService from 'app/comment-service';
import Comment from 'app/comment';

// implements is optional and typescript will verify that this class implements Comment
// by looking at the definition of the service method so I could remove it and 
// also remove the import statement if I wish
class ValidatedComment implements Comment {
  constructor(public likes, public comment: string) {
    if (Number(likes) < 0 || !Number.isSafeInteger(Number(likes))) {
      throw RangeError('Likes must be a valid number >= 0'
    }
  }
}

export async function createComment(likes, comment: string) {
  const commentService = new CommentService();

  const comment = new ValidatedComment(likes, comment); // better get the order right

  await commentService.createCommnet(comment);
}

In short there are many reasons to use an interface to describe the type of the responses and also the requests that interact with an HTTP service when using TypeScript.

Note: you can also use a type declaration, which is equally safe and robust but it is less idiomatic and the tooling around interface often makes it preferable for this scenario.




回答2:


As in most other OOP languages: For classes you can create instances (via their constructor), while you cannot create instances of interfaces.

In other words: If you just return deserialized JSON, then it makes sense to use the interface to avoid confusion. Lets assume you add some method foo to your Comment class. If your register method is declared to return a Comment then you might assume that you can call foo on the return value of register. But this wont work since what register effectively returns is just deserialzed JSON without your implementation of foo on the Comment class. More specifically, it is NOT an instance of your Comment class. Of course, you could also accidentally declare the foo method in your CommentInterface and it still wouldn't work, but then there would be no actual code for the foo method that is just not being executed, making it easier to reason about the root cause of your call to foo not working.

Additionally think about it on a semantic level: Declaring to return an interface gurantees that everything that is declared on the interface is present on the returned value. Declaring to return a class instance gurantees that you... well... return a class instance, which is not what you are doing, since you are returning deserialized Json.



来源:https://stackoverflow.com/questions/45404571/whats-the-difference-between-using-a-class-and-interface

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!