从JavaScript到TypeScript,Pt。 IIB:使用类,接口和混合器进行设计

Class-based design has become such an instinct that many developers can't imagine any alternative.

基于类的设计已经成为一种本能,许多开发人员无法想象任何替代方案。

Fortunately for that lot, ES6 adds a class keyword to simplify the syntactical cacophany of working with JavaScript's native prototypes. TypeScript goes further, adding support for access modifiers; interfaces; enums; and a smörgasbård of other classical object-oriented goodies.

幸运的是,ES6添加了一个class关键字来简化使用JavaScript的本机原型的句法模糊性。 TypeScript更进一步,增加了对访问修饰符的支持; 接口; 枚举 以及其他一些经典的面向对象的东西。

Of course, JavaScript doesn't have native classes. Nor does it have native interfaces. Nor does it have native access modifiers. TypeScript does a good job of emulating them, but the illusion can't be total. Keep that in mind, as it's possible to slip past the compile-time checks if you're clever enough.

当然,JavaScript没有本机类。 它也没有本机接口。 它也没有本机访问修饰符。 TypeScript在模拟它们方面做得很好,但是错觉并不能完全解决。 请记住这一点,因为如果您足够聪明,就有可能跳过编译时检查。

After reading this article, you'll be able to:

阅读本文之后,您将能够:

  • Write and design robust classes and interfaces;

    编写和设计健壮的类和接口;
  • Decide whether classes or interfaces are more appropriate for your use case; and

    确定类或接口是否更适合您的用例; 和
  • Article the major principles of object-oriented programming and design.

    文章介绍了面向对象编程和设计的主要原理。

All the code samples for this article are hosted at my GitHub. Clone it down, and run:

本文的所有代码示例都托管在我的GitHub上 。 克隆下来,然后运行:

git checkout Part-2B-OOP_Design_Principle

If you're on Windows, you should be able to run tsc --target ES5. You'll get a frightening bundle of errors, but you can safely ignore them.

如果您使用的是Windows,则应该能够运行tsc --target ES5 。 您会收到一系列令人恐惧的错误,但可以放心地忽略它们。

If you're on Linux or Mac, you'll still run tsc, but you can pipe the errors to nowhere:

如果您使用的是Linux或Mac,则仍将运行tsc ,但可以将错误传递到任何地方:

# Compile all TS files, and silence irrelevant errors.
tsc --target ES5 > /dev/null 2>&1

Either way, you should end up with a folder called built with the transpiled JavaScript.

无论哪种方式,您都应该最终获得一个由已编译JavaScript built的文件夹。

Catch Up: Read From JavaScript to TypeScript Parts I and IIA

追赶 :从JavaScript读到TypeScript Part I和IIA

类:背景和设计注意事项 ( Classes: Background & Design Considerations )

At the risk of sounding like a broken record: JavaScript does not have classes. It has a class keyword. These are very different things.

听起来有破纪录的危险: JavaScript没有类它有一个class关键字 。 这些是完全不同的东西。

JavaScript uses prototype-based delegation to achieve what Java and C++ accomplish with classes. Even though we might be more comfortable with classical OOP, and even though we might be able to emulate it with prototypes, it will only ever be an emulation.

JavaScript使用基于原型的委托来实现Java和C ++使用类完成的工作。 即使我们对经典的OOP可能更满意,即使我们可以用原型对其进行仿真但它永远只是一种仿真

Under the hood, it's prototypes all the way down.

在引擎盖下,它一直是原型。

That's why it's crucial to understand how JavaScript's classes and prototypes work under the hood. You don't have to like them, but life will be easier if you're familiar with them.

这就是为什么了解JavaScript的类和原型如何在后台运行至关重要。 您不必喜欢它们,但是如果您熟悉它们,生活会更轻松。

抽象和代码重用 (Abstraction & Code Reuse)

DRY code is not necessarily good code, but good code is pretty much always DRY.

DRY代码 不一定是好的代码,但是好的代码几乎总是DRY。

Avoid duplicate code is such a common admonition in modern development that it's hard to write the same code twice without feeling . . . Dirty. And for good reason: If you write your code in one place, and reuse it elsewhere, you'll only have one place to worry about when you debug, maintain, or extend it.

避免重复代码是现代开发中的常见建议,很难两次编写相同的代码。 。 。 脏。 并有充分的理由:如果您将代码编写在一个地方,然后在其他地方重用,则在调试,维护或扩展代码时,只需要担心一个地方。

The basic motivation for classes and objects derives from a similar principle

类和对象的基本动机源自相似的原理

Objects provide a way to associate a data of a certain shape with behavior.

对象提供了一种将特定形状数据行为相关联的方法

Objects provide a way to associate a data of certain shapes with behaviors -- that is, things you'll often want to do with that data. Classes provide a way to desribe those shapes and behaviors in the abstract, making it easy for us to create arbitrariy many similar objects throughout the lifetime of our program.

对象提供了一种将某些形状数据行为相关联的方法 -也就是说,您经常需要对该数据进行处理。 提供了一种抽象描述这些形状和行为的方法,使我们可以在程序的整个生命周期内轻松创建任意相似的对象。

Put another way, a class is like a blueprint: It describes how to build a house. The house is the object itself. Building the house according to the blueprint is analogous to instantiating an object from the class. If we want to change the way the house is built, everywhere it's built, all we have to do is change the blueprint -- not the houses.

换句话说,一个类就像一个蓝图:它描述了如何盖房子。 房子是物体本身。 根据蓝图建造房屋类似于从类中实例化对象。 如果我们想改变房屋的建造方式, 无论房屋到处都是 ,我们要做的就是更改蓝图- 而不是房屋。

We create classes and objects that reflect the structural components of the problems we're solving. This allows us to reason about our programs at a high level of abstraction, which is one of the fundamental aspects of OOP.

我们创建的类和对象反映了我们正在解决的问题的结构性组成部分。 这使我们能够以高度抽象的方式对程序进行推理,这是OOP的基本方面之一。

// user.ts
"use strict";

// Blueprint :: This doesn't create users. It just describes them.
class User {

    constructor (private _name : string,
                 private _email : string) {}

    get name () : string  { return this._name; }

    get email () : string { return this._email; }

    speak () : void { console.log(`I am ${this.name}!`);

}

// Instantiation :: Create an actual thing that acts as the class describes.
const peleke = new User('Peleke', 'resil.design@gmail.com');
console.log(peleke.name);

An object expresses its behavior through the functions attached to it, called methods, and it holds its data, or state, in instance variables.

对象通过附加到它的函数(称为方法)来表达其行为 ,并将其数据状态保存在实例变量中

In JavaScript, instantiated objects get their own instance variables, but delegate method calls to the class's prototype object. In other words, each object gets its own data, but shares methods with other instances of the class.

在JavaScript中,实例化的对象获得其自己的实例变量,但是将委托方法调用委托给该类的原型对象。 换句话说,每个对象都有自己的数据,但与该类的其他实例共享方法。

That means two things:

这意味着两件事:

  1. If you change methods on the class prototype, all of your instances will behave differently, even if you created them before making the change; and

    如果您在类原型上更改方法,则即使您在进行更改之前创建了它们,所有实例的行为也会有所不同。 和
  2. If you add methods to the class's prototype, all of your instances will be able to call them, even if you created your objects before adding the new methods.

    如果将方法添加到类的原型中,即使您添加新方法之前创建了对象,所有实例都将能够调用它们。
peleke.speak(); // 'I am Peleke!'
peleke.hasOwnProperty('speak'); // false; delegated to User.prototype
peleke.hasOwnProperty('_name'); // true; 'private' property is still technically visible

User.prototype.speak = null;
try {
    peleke.speak();
} catch (err) {
    // Throws TypeError
}

This dynamism is the reason prototypes are so powerful. It's also one of the reasons that we can't perfectly emulate classes.

这种活力是原型如此强大的原因。 这也是我们无法完美模拟类的原因之一。

Unfortunately, declaring methods private to prevent others from overriding it elsewhere doesn't solve the problem -- that keeps you from using it in the first place*.

不幸的是,将方法声明为private以防止其他人在其他地方覆盖它并不能解决问题-导致您一开始就无法使用它*。

You'll also notice that, while you can't read or write private variables, you can still "see" them at runtime. In most popular object-oriented languages, that would throw an access error.

您还将注意到,尽管您无法读取或写入private变量,但仍可以在运行时“查看”它们。 在大多数流行的面向对象语言中,这将引发访问错误。

In the classical model, methods and instance variables are mostly final after instantiation. In other words, under normal circumstances, you can't assign new methods or properties to an object after it's been created.

在经典模型中,方法和实例变量在实例化之后大部分是最终的。 换句话说,在通常情况下,创建对象后,您无法为其分配新的方法或属性。

These are a couple fundamental assumptions of classical design patterns that don't hold in our dynamic environment. Neither is likely to surprise longtime JavaScripters, but they're minor gotchas for folks migrating from other OO languages.

这些是经典设计模式的几个基本假设,这些假设在我们的动态环境中不成立。 长期使用JavaScript的人都不会对这感到惊讶,但是对于从其他OO语言进行迁移的人们来说,它们只是小问题。

* You can return a function from a private getter, though, which is a nifty potential solution to the overwrite problem.

*但是, 您可以从私有getter返回一个函数 ,这对于覆盖问题是一个很不错的解决方案。

封装与实现隐藏 (Encapsulation & Implementation Hiding)

Another advantage of classes is that that they allow us to keep an object's internals hidden from the outside world. We want people to know how to use our objects; we don't want them to know how they work.

类的另一个优点是它们使我们可以使对象的内部结构对外界隐藏。 我们希望人们知道如何使用我们的对象。 我们希望他们知道他们的工作方式。

Exposing as little information about your programs as possible is almost always a good idea. This is called the principle of least privilege, or the Law of Demeter.

尽可能少地公开有关程序的信息几乎总是一个好主意。 这称为最小特权原则 ,或称得墨meter耳法则

This principle implies that our objects should be defined by two things:

这个原则意味着我们的对象应该由两件事来定义:

  1. An Interface An object's interface is what you promise it can do -- in other words, a list of its public methods.

    接口对象的接口就是您所承诺的功能,换句话说,就是其公共方法的列表。
  2. Hidden Implementation Details *. If you ask an object to sort a list of words, whether it use quicksort, mergesort, or O(n) black-magic-sort is irrelevant. All you care about is the sorted list it hands back. *How it gets it to you is none of your business.

    隐藏的实施细节* 如果您要求对象对单词列表进行排序,则它是使用quicksort,mergesort还是O(n)black-magic-sort都是无关紧要的。 您只关心它递回的排序列表。 *如何获得它与您无关。

Separating what an object can do from how it does them is called encapsulaton, and it's one of the cornerstones of good object-oriented design.

分离是什么对象可以从它怎么做他们被称为encapsulaton做的,它是良好的面向对象设计的基石之一。

The reason is that clients who know a method's implementation will write code that relies on that implementation. If the implementation changes, their code breaks, they complain, maintainers have to fix it, and everyone's unhappy.

原因是知道方法实现的客户将编写依赖于该实现的代码。 如果实现发生更改,他们的代码将中断,他们会抱怨,维护人员必须对其进行修复,并且所有人都会感到不满意。

Now, it's clear that library authors have to worry about this, because they literally have clients to support. But the issue is more general than that: Any part of your code that uses one of your objects is a client of that object. If all of your client code depends on the object's implementation, changing your object means changing the code . . . Everywhere.

现在,很明显,库作者必须为此担心,因为他们确实有客户来支持。 但是问题比这更笼统:使用您的一个对象的代码的任何部分都是该对象的客户端 。 如果您所有的客户代码都取决于对象的实现,那么更改对象就意味着更改代码。 。 。 到处。

That largely defeats the purpose of collecting everything into an object in the first place. Ensuring we don't fall into that trap is one of the reasons encapsulation is so essential to good design.

首先,这大大挫败了将所有内容收集到对象中的目的。 确保封装不陷入陷阱是封装对于良好设计至关重要的原因之一。

The interface of a class is the one place where we assert all of the assumptions that a client may make about any instances of the class; the implementation encapsulates details about which no client may make assumptions. ~ Grady Booch, Object Oriented Design & Analysis (pp. 52), emphasis mine

类的接口是一个地方,在这里我们可以声明客户可能对类的任何实例进行的所有假设。 该实现封装了没有客户可以做出假设的细节 。 〜Grady Booch, 面向对象的设计与分析(pp。52) ,重点是我的

To recap, encapsulation buys us:

回顾一下,封装使我们获得了:

1.Safety, by ensuring clients can't muck with an object's guts; and

1. 安全 ,通过确保客户不会弄脏物品的胆量; 和

  1. Flexibility, because clients can't tell if we change internal details as long as our objects still satisfy their interface.

    灵活性 ,因为只要对象仍然满足其接口,客户就无法确定我们是否更改内部细节。

JavaScript has traditionally achieved encapsulation via clever manipulation of closures. TypeScript also provides the private access modifier for properties, which prevents reads or writes at compile-time. It doesn't truly hide the property, however, so you can't trust it to provide airtight encapsulation.'

传统上,JavaScript通过对闭包的巧妙处理来实现封装 。 TypeScript还为属性提供了private访问修饰符,以防止在编译时进行读取或写入。 但是,它并没有真正隐藏属性,因此您不能信任它提供不透气的封装。

交换后端 (Swapping Backends)

Let's imagine we're prototyping an app that fetches definitions for all the words in a user-input sample of Spanish text.

假设我们正在制作一个原型,该应用程序将获取用户输入的西班牙文本样本中所有单词的定义。

We're still figuring out the details, so we expect to be . . . Erm, promiscuous with our back-end, so to speak.

我们仍在弄清楚细节,所以我们希望能做到。 。 。 可以这么说,Erm,与我们的后端混杂在一起。

"use strict";
/* I've omitted some mock implementations from this code, so it won't compile.
  *   The file, vocabulary_list.ts, on the repository, /does/ compile.
  */

const config = {
"use strict";

const CONFIG = {
    api : 'wordreference',
    key : 'API_KEY'
};

class VocabularyList {

  private english_words : Array<string>;

    constructor (private spanish_words : Array<string>) {
      this.english_words = [];
    }

    fetchWords () : Map<string, string> {
        return this.parseResponse ( this.makeRequest(CONFIG.api, CONFIG.key) );
    }

    private makeRequest (api : string, key : string) : any {
      const magicalJson = { /* For illustration */ }
      return magicalJson; 
    }

    private magicallyGetWord (word : string) : any {
      // Mock data to please compiler
      return { english_data : 'English language data' }
    }

    // This logic is specific to WR's (made-up) response format:
    //    We have to extract the english_entries property from the response
    private parseResponse (json : any) : Map<string, string> {
        const translations : Map<string, string> = new Map<string, string>();

        this.spanish_words.forEach((spanish_word) => {
            // Get word from API with magic spell
            const translation_data = this.magicallyGetWord(spanish_word);

            // Get data from WORDREFERENCE response
            const english_data = translation_data.english_data;

            // Get the english_data property -- let's pretend it's also an object
          translations.set(spanish_word, english_data)
        });

        return translations;
    }
}

Works great, until WordReference shuts down its API (which it kind of did).

效果很好,直到WordReference关闭其API( 确实如此 )。

D'oh.

天啊

After a long stint at the drawing board, we jump ship to Merriam-Webster.

在绘图板上待了很长时间之后,我们跳船去了Merriam-Webster

const config = {
    api : 'merriam-webster',
    key : 'NEW_API_KEY'
};

class VocabularyList {

    // Nothing new . . . 

    private parseResponse : Array<string> (json : any) {
        const translations : Map<string, string> = new Map<string, string>();

        this.spanish_words.forEach((spanish_word) => {
            // Get word from API with magic spell
            const translation_data = this.magicallyGetWord(spanish_word);

            // Get data from MERRIAM-WEBSTER response -- 
            //   this is the only place we need to make a change!
            const english_data = getDataFromUglyResponse( translation_data );

         // Get the english_data property -- let's pretend it's also an object
          translations.set(spanish_word, english_data)

        });

        // Create an information-packed object to send back to the caller
        return buidObject(english_data);
    }
}

Unfortunately, this API doesn't deliver everything we need in a nice packaged english_data property, so we have to put it together ourselves. That's what getDataFromUglyResponse is for.

不幸的是,此API并没有在打包好的english_data属性中提供我们所需的一切,因此我们必须将其放在一起。 这就是getDataFromUglyResponse目的。

Other parts of our application -- our clients -- use the VocabularyLists's fetchWords method, knowing that it'll hand back a map of translated words.

我们应用程序的其他部分(即客户 )使用VocabularyListsfetchWords方法,因为它知道该方法将返回翻译过的单词的映射。

What they don't know is whether fetchWords is talking to WordReference, Merriam-Webster, or the Oxford English Dictionary. All they need to know is its interface -- that it can fetchWords. They don't know how. That's an implementation detail.

他们不知道fetchWords是与WordReference,Merriam-Webster还是牛津英语词典交谈。 所有他们需要知道的是它的界面- 可以fetchWords 。 他们不知道怎么做。 这是一个实现细节。

If they did know that VocabularyList got its words from WordReference, and had to provide their own logic to parse its response, we'd have at least two problems.

如果他们确实知道VocabularyList是从WordReference获得其单词的,并且必须提供自己的逻辑来解析其响应,那么我们至少会遇到两个问题。

  1. The design would suck, because we'd have to parse JSON in a function that we really should just pass everything it needs; and

    设计很烂,因为我们必须在一个函数中解析JSON,而我们实际上应该只传递它需要的所有内容。 和
  2. Refactoring would suck, because you'd have to write new parsing logic all over the place.

    重构会很麻烦,因为您必须在各处编写新的解析逻辑。

That would be called programming to an implementation, and it's one of the cardinal sins of object-oriented design. It's not just a theoretical no-no, either: This mistake has been responsible for a substantial proportion of my debugging time, as well as that of others. Avoid it like the plague it is.

那将被称为对实现的编程 ,这是面向对象设计的主要缺点之一。 这不仅是理论上的禁忌,也不是:这个错误导致了我和其他人调试时间的很大一部分。 避免它像瘟疫一样。

What we did instead is program to an interface. We wrote our code to expect fetchWords to send us an object with all of the information we need, so we can pick and choose what we want from it.

相反,我们所做的是对接口进行编程 。 我们编写了代码,期望fetchWords向我们发送一个对象,其中包含我们需要的所有信息,因此我们可以从中选择所需的信息。

How VocabularyList gets it to us is irrelevant. Its interface makes a promise; we trust the promise; and so VocabularyList is obligated to keep it.

VocabularyList如何获取给我们是无关紧要的。 它的界面可以保证; 我们相信诺言; 因此VocabularyList有义务保留它。

If the details change, we only worry about it in the code for the class -- nowhere else. This kind of flexibility, and stability of client code in the face of implementation changes, is precisely the point of encapsulation and information hiding.

如果细节发生变化,我们只需要担心该类的代码中的其他问题。 面对实现更改时,客户端代码的这种灵活性和稳定性恰好是封装和信息隐藏的重点。

类关系 ( Class Relationships )

Classes define the shape of the objects they instantiate, as well as their API.

类定义它们实例化的对象的形状以及它们的API。

Classes can also be related to other classes. The most common relationships are:

类也可以与其他类相关。 最常见的关系是:

  • Subclass relationships, and

    子类关系,以及
  • Multiple inheritance.

    多重继承。

JavaScript doesn't support multiple inheritance, but TypeScript's interfaces and mixins scratch a similar itch.

JavaScript不支持多重继承,但是TypeScript的接口和mixins碰到了类似的问题。

子类化和多态 (Subclassing & Polymorphism)

Subclassing is the process of creating a new class that inherits the behavior and data members of another class. A class you create through subclassing is called a child class, or a derived class.

子类化是创建一个新类的过程,该类继承了另一个类的行为和数据成员。 通过子类创建的类称为子类派生类

// raw_fish.ts
class Fish {

    constructor (public name : string) { }

    // Pseudo-abstract method :: No-op, unless
    //   a subclass provides an implementation.
    cook () : void { }

}

class SushiFish extends Fish {

    constructor ( name : string, cooked : boolean = false ) {
        super(name);
    }

  cook () : void { 
      console.log('You don\'t cook a sushi fish!')
  }

}

class CookedFish extends Fish {

  constructor (name : string, cooked : boolean = true) {
      super (name);
  }

}

const tuna = new Fish('Tuna');
const dinner = new CookedFish('Halibut');

tuna.cook(); // 'Fish has been cooked!'
dinner.cook(); // 'This fish is already cooked!'

To create a subclass, you use the extends keyword, followed by the base class name. CookedFish is our derived class; Fish is called its superclass.

要创建子类,请使用extends关键字,后跟基类名称。 CookedFish是我们的派生类; Fish被称为超类

Note that we can call cook on our dinner object, even though we didn't define that method explicitly. That's because every method on Fish is available via inheritance. Since a CookedFish is a type of Fish, it should expose the same API.

请注意,即使我们未明确定义该方法,也可以在dinner对象上调用cook 。 这是因为Fish上的每种方法都可以通过继承使用 。 由于CookedFishFish ,因此它应该公开相​​同的API。

It is not, however, obligated to behave identically. Both our SushiFish and our CookedFish classes override their parent's cook method, and so behave differently.

但是,它没有义务表现相同。 我们两个SushiFish和我们CookedFish重写他们的父母的cook方法等不同的表现。

The fact that different classes with unique behavior can share the same interface by relation to a common superclass is called polymorphism.

具有独特行为的不同类可以通过与共同的超类联系而共享同一接口的事实被称为多态性

多重继承 (Multiple Inheritance)

Languages with multiple inheritance allow you to inherit from several parents classes at once.

具有多重继承的语言允许您一次从多个父类继承。

JavaScript doesn't support this, but if it did, it might look like this:

JavaScript不支持此功能,但如果支持,则可能如下所示:

class Wolf extends Canine, Predator {
    constructor () {
    // Which superconstructor do we use? Hm . . . 
    super();
    }
}

const wolfie = new Wolf();

wolf.howl(); // Canines howl
wofl.hunt(); // Predators hunt

TypeScript doesn't provide sugar for this. But we can achieve similar behavior using either interfaces or mixins.

TypeScript不为此提供糖。 但是我们可以使用接口或混合来实现类似的行为。

接口及其抽象 (Interfaces & Their Abstractions)

An interface describes what you can expect an object to do, but it doesn't define how the object should do it.

接口描述,你可以期望一个对象做什么 ,但它并没有定义对象应该怎么做。

More technically, an interface defines an API that its implementers must expose, but defers the implementation to the implementing classes.

从技术上讲,接口定义了其实现者必须公开的API,但将实现推迟到实现类。

While classes can only inherit from a single superclass, they can implement multiple interfaces. This allows a lot of flexibility in organizing and defining the behavior of our objects.

虽然类只能从单个超类继承,但它们可以实现多个接口。 这为组织和定义对象的行为提供了很大的灵活性。

// printable.ts
 interface Identifiable {
     name : string;
     identify ();
 }

 interface Printable {
     text : Array<string>;
     print ();
 }

 class Book implements Identifiable, Printable {
    // Creative implementations here . . . 
 }

Major benefits of interfaces are:

接口的主要优点是:

  • Abstraction. They encourage you to design systems in terms of common behaviors, and discouage you from writing your code with excessive attention to implementation details.

    抽象 。 它们鼓励您根据常见行为来设计系统,并避免过分关注实现细节来阻止编写代码。
  • Flexibility. Interfaces define behavior, but defer the implementation(s) of that behavior to the objects that implement the interface.

    灵活性强 。 接口定义了行为,但是将该行为的实现推迟到实现接口的对象上。
  • Composability. You can implement an arbitrary number of interfaces, allowing you to compose an arbitrary number of APIs into a single object.

    可组合性 。 您可以实现任意数量的接口,从而可以将任意数量的API组合到一个对象中。

One downside is that interfaces can't provide method definitions. This means you'll have to provide implementations in each class that implements the interface.

缺点之一是接口无法提供方法定义。 这意味着您必须在实现该接口的每个类中提供实现。

On one hand, that's largely the point. Deferring implementation details to implementing classes is what makes interfaces so powerful.

一方面,这就是重点。 使实现细节推迟到实现类上是使接口如此强大的原因。

But sometimes, a number of otherwise unrelated classes will implement the same interfaces in exactly the same way.

但是有时,许多其他不相关的类将以完全相同的方式实现相同的接口。

// printable.ts
"use strict";

class Book implements Printable, Identifiable {

    // Shorthand; creats private members automatically
    constructor ( public name : string,  public text : Array<string>) { }

    print () : void {
        console.log(this.text);
    }

    // This is the same in both classes!
    identify () : void {
        console.log(this.name);
    }
}

class User implements Identifiable {

    constructor (public name : string) { }

    // This is the same in both classes!
    identify () : void {
        console.log(this.name);
    }
}

Inheritance is the wrong abstraction for creating largely unrelated classes that act similarly, so extends doesn't make sense. But implements forces us to:

继承是错误的抽象,用于创建行为相似的很大程度上不相关的类,因此extends没有意义。 但是implements迫使我们:

  1. Write the same implementation in multiple places; or

    在多个地方编写相同的实现; 要么
  2. Define the implementation somewhere else, and have both objects refer to it.

    在其他地方定义实现,并让两个对象都引用它。

Crappy design choice, don't you think?

笨拙的设计选择,您不觉得吗?

  1. Neither solution is DRY. If the common behavior changes, you'll have to change it in every implementing class.

    两种解决方案都不是DRY。 如果常见行为发生变化,则必须在每个实现类中进行更改。
  2. Defining a single method somewhere else is a better design choice. But externalizing the implementation somewhere arbitrary largely defeats the organizational purposes of classes and interfaces.

    在其他地方定义单个方法是更好的设计选择。 但是在任意地方将实现外部化在很大程度上破坏了类和接口的组织目的。

Languages like Groovy and Scala have traits, which are effectively interfaces that hold state and implement methods.

诸如GroovyScala之类的语言具有traits ,它们是有效的保持状态和实现方法的接口。

TypeScript offers two ways to go about trait-based design:

TypeScript提供了两种基于特征的设计方式:

  1. Abstract classes; or

    抽象类; 要么
  2. Mixins.

    Mixins。

An abstract class lies somewhere between a normal class and an interface.

抽象类位于普通类和接口之间。

Similar to interfaces, you:

与接口类似,您:

  1. . . . Can't instantiate an abstract class. Rather, you must extend from it, and instantiate the derivcd class.

    。 。 。 无法实例化抽象类。 相反,您必须从中extend并实例化派生类。
  2. . . . Can include method signatures without definitions, which derived classes must implement. The only difference to interfaces is that you mark these with the keyword abstract in an abstract class.

    。 。 。 可以包括没有定义的方法签名,派生类必须实现这些方法签名。 与接口的唯一区别是,您在抽象类中用关键字abstract标记了这些接口。

And similar to classes, you:

与类类似,您:

  1. . . . Must use extends to inherit from an abstract class. This means you can only have one abstract parent.

    。 。 。 必须使用extends从抽象类继承。 这意味着您只能有一个抽象父级。
  2. . . . Can provide method implementations, which your derived classes will inherit.

    。 。 。 可以提供派生类将继承的方法实现。

The major difference between abstract classes and traits is that you can extend a single abstract class, whereas you can implement an arbitrary number of traits.

抽象类和特征之间的主要区别在于,您可以扩展单个抽象类,而您可以实现任意数量的特征。

Abstract classes are a good design choice if you're abtracting over several different child structures, which will differ in their implementation of an interface while sharing certain common behaviors.

如果您要抽象几个不同的子结构,则抽象类是一个不错的设计选择。这些子结构在共享某些共同行为的同时,它们在接口的实现上也会有所不同。

If you 've got several mostly unrelated objects implementing an interface identically, though, than a mixin is probably the better solution.

但是,如果您有几个几乎不相关的对象完全相同地实现了一个接口,那么比之类的mixin可能是更好的解决方案。

Mixins vs工具 (Mixins vs Implements)

Thre are few hard and fast rules where design is concerned, but you'll find certain patterns pop up as you do it more. If you're describing behavior:

很少有涉及设计的硬性规则,但是当您执行更多操作时,您会发现某些模式会弹出。 如果您要描述行为:

  • . . . So general that every class implementing it will do so differently, use an ** interface **.

    。 。 。 一般而言,每个实现它的类都将使用**接口**进行不同的操作。
  • . . . General enough that unrelated classes will implement it, but common enough that they'll mostly do so in the same way, use a ** mixin **.

    。 。 。 一般而言,无关的类将实现它,但足够普遍的是,它们将以相同的方式执行此操作,请使用** mixin **。

If the answer to the former is yes, consider an interface. If the answer to the latter is yes, consider mixins. Don't be afraid to ignore the guideline, though; code to a problem, not to a guideline.

如果前者的答案为 ,请考虑使用接口。 如果对后者的回答是肯定的 ,请考虑使用mixins。 但是,不要害怕忽略该准则。 代码来解决问题,而不是准则。

分手评论 (A Parting Comment)

"If you could do Java over again, what would you change?" - "I'd leave out classes." ~ James Gosling, creator of Java, as quoted by Alan Holub, Holub on Patterns

“如果您可以再次使用Java,您将改变什么?” -“我不上课。” 〜Java的创造者James Gosling, Holub在Patterns上的 Alan Holub引用

Never thought I'd hear that one.

没想到我会听到那个。

Turns out that Gosling's point is not that classes are intrinsically bad. Rather, it's that using extends where one should use implements has caused more headaches than anticipated, and that you should be careful creating unnatural class hierarchies.

事实证明,高斯林的观点并不是说班级本质上是不好的。 而是在人们应该使用implements地方使用extends导致了比预期更多的麻烦,并且您应该谨慎创建不自然的类层次结构。

实现继承 (Implementation Inheritance)

Using extends to build new classes is called implementation inheritance, because subclasses created with extends have the same methods as their parents, with the same implementations.

使用extends构建新类称为实现继承 ,因为使用extends创建的子类与其父级具有相同的方法,并具有相同的实现。

This sort of inheritance is natural if you're creating a class that's a special case of another -- say, a FreshwaterFish that extends our Fish class.

如果您创建的类是另一类的特例,则这种继承是很自然的,例如,扩展了Fish类的FreshwaterFish

// freshwater_fish.ts
"use strict";

class Fish {

    constructor (private name : string) { }

}

class FreshwaterFish extends Fish {

    constructor (name : string, private salt_tolerant : boolean = false) {
        super(name);
    }

}

FreshwaterFish class behaves exactly like our Fish class, but holds additional state.

FreshwaterFish类的行为与我们的Fish类完全相同,但是具有其他状态。

More generally, subclassing makes sense when you can use your subclass anywhere you can use your superclass. A program that receives a FreshwaterFish but expects a Fish should remain well-typed throughout, even if it runs differently.

更一般而言,当可以可以使用超类的任何地方使用子类时,子类才有意义。 收到FreshwaterFish但希望Fish在整个过程中都应保持良好类型的程序,即使它的运行方式不同也是如此。

This principle is encoded in the Liskov Substitution Principle. Covering it properly is out of the scope of this article, but take the time to read up on it at some point.

此原理编码在Liskov替代原理中 。 适当地涵盖它不在本文的讨论范围之内,但是请花一些时间仔细阅读它。

接口继承 (Interface Inheritance)

The alternative to implementation inheritance is interface inheritance, or using the implements keyword to declare that an object exposes the API defined by a given interface.

实现继承的替代方法是接口继承 ,或者使用implements关键字声明一个对象公开给定接口定义的API。

Interface inheritance has a number of advantages. It:

接口继承具有许多优点。 它:

  • Loosens coupling between parts of your program;

    松开程序各部分之间的耦合;
  • Eliminates the problem of changes to a superclass breaking subclasses;

    消除了更改超类打破子类的问题;
  • Affords greater flexibility, because any class implementing an interface can implement its API differently.

    提供更大的灵活性,因为任何实现接口的类都可以不同地实现其API。

A class that implements an interface is still said to be a subclass of the interface; and by corollary, an interface can be a supertype.

实现接口的仍然被称为该接口的子类 。 因此,接口可以是超类型。

这个或那个? (This, or That?)

A natural reaction at this point is extreme tenderness at being told to use extends with caution.

在这一点上自然的React是在使用被告知极端压痛extends谨慎。

There are those who would argue that extends is evil. There's a case for that, but I don't think categorical avoidance is a very productive solution.

有些人认为扩展是邪恶的 。 有一个理由,但是我不认为绝对避免是一种非常有效的解决方案。

Subclassing is not inherently worse than any other language feature. Gosling himself admits to using extends more than implements, in spite of the above quip.

子类别在本质上并不比任何其他语言功能都差。 尽管有上述怪癖,高斯林本人也承认使用extends而不是implements

Using subclasses wrong can suck, though, and it's true that a lot of people have a habit of using subclasses where interfaces, mixins, or a different pattern entirely would make more sense. That makes for unmaintanable code, mysterious subclass behavior, and an abundance of senseless class hierarchies.

但是, 错误地使用子类会很烂,而且的确,很多人都有使用子类的习惯,在这些子类中,接口,mixin或其他模式完全有意义。 这导致了不可维护的代码,神秘的子类行为以及大量毫无意义的类层次结构。

The solution isn't to avoid extends: It's to design your code well. Subclasses are easy to understand and easy to implement, which is why they're easy to overuse. That can come back to bite you if you get overzealous.

解决方案不是避免extends :而是设计良好的代码。 子类易于理解且易于实现,这就是为什么它们容易被过度使用的原因。 如果您太过热情,那可能会再次咬住您。

Just remember to always consider your alternatives. As long as you do that, your hierarchies should turn out alright.

只要记住要始终考虑您的替代方案。 只要您这样做,您的层次结构就应该可以了。

结论 (Conclusion)

By now, you should be able to:

现在,您应该能够:

  • Define the Four Fundamentals of OOP: Abstraction, Encapsulation, Polymorphism, and Subclassing;

    定义OOP四个基本原理 :抽象,封装,多态和子类化;
  • Decide whether an interface, class, or mixin is the best abstraction for your problem; and

    确定接口,类或mixin是否是解决问题的最佳抽象; 和
  • Weigh the pros and cons of building systems in terms of implementation or interface inheritance.

    在实现或接口继承方面权衡构建系统的优缺点。

Try reading some code that uses these principles to understand how they work in the real world -- I've been digging through the Angular 2 source, myself.

尝试阅读一些使用这些原理的代码,以了解它们在现实世界中的工作方式-我一直在挖掘Angular 2的源代码 ,我自己。

As usual, feel free to leave questions in the comments, or shoot them to me on Twitter (@PelekeS); I'll get back to everyone individually.

像往常一样,随时在评论中留下问题,或在Twitter( @PelekeS )上向我射击; 我将单独与每个人联系。

翻译自: https://scotch.io/tutorials/from-javascript-to-typescript-pt-iib-designing-with-classes-interfaces-mixins

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值