基于接口的编程:使用TypeScript、Angular 2和SystemJS

8

我目前正在尝试清理代码,以便针对接口而不是实现进行编程,但无法弄清楚如何做到。

更具体地说,我正在使用: * TypeScript 1.5.0 beta -> 转译为 ES5 / commonjs * SystemJS 加载模块

我当前尝试使用外部模块如下:

posts.service.ts 文件:

///<reference path="../../../typings/tsd.d.ts" />
///<reference path="../../../typings/typescriptApp.d.ts" />
...
export interface PostsService{ // 1
    fetchPosts(): Rx.Observable<any>;
}
export var PostsService:PostsServiceImpl; // 2
...
export class PostsServiceImpl implements PostsService { // 3
    ...
    constructor(){
        console.log('Loading the Posts service');
}
...
fetchPosts(): Rx.Observable<any>{ 
   ...
}

那个模块被导入到了posts.ts中:

    ///<reference path="../../../typings/tsd.d.ts" />
    ///<reference path="../../../typings/typescriptApp.d.ts" />

    import {PostsService, PostsServiceImpl} from 'components/posts/posts.service';

    @Component({
    selector: 'posts',
    viewInjector: [
        //PostsServiceImpl
        //PostsService
        bind(PostsService).toClass(PostsServiceImpl)
    ]
})
...
export class Posts {
    private postsServices: PostsService;

    constructor(postsService: PostsService) {
        console.log('Loading the Posts component');
        this.postsServices = postsService;
        ...
    }

    ...
}

以上代码可以正常编译,但问题基本上在于Angular在Posts组件中无法注入PostsServiceImpl的实例。
当然,这只是因为我没有找到正确的声明/导出/导入方式。
如果没有“export interface PostsService...”,我在posts控制器中使用该服务将会得到TS编译错误。
如果没有“export var PostsService: PostsServiceImpl;”,我将在@View装饰器的viewInjector中得到TS编译错误。
在生成的JS代码中,我只发现“exports.PostsService;”,这是由于导出变量而添加的。我知道接口在运行时消失了。
所以,我有点迷失在所有这些之中。有什么实用的想法可以让我在TypeScript和Angular 2中使用基于接口的编程吗?
4个回答

6

有没有实际的想法,如何在TypeScript和Angular 2中使用基于接口的编程

接口在运行时被擦除。事实上,装饰器也不支持接口。因此,最好使用在运行时存在的东西(即实现)。


2
这对我来说是个悲伤的消息 ;-). 我想我应该适应并忘记我的完全类型化的Java世界.. ^^ - dSebastien
3
在我的应用程序中如何处理解耦?我还没有达到那个程度,但我的目标是编写可测试的代码。据我所知,在TypeScript中还没有对抽象类的支持,对吗? - dSebastien

4

您需要记住,您的类可能(并且应该)依赖于抽象层,但是有一个地方不能使用抽象层:就是在 Angular 的依赖注入中。

Angular 必须知道要使用哪种实现。

示例:

/// <reference path="../../../../../_reference.ts" />

module MyModule.Services {
    "use strict";

    export class LocalStorageService implements ILocalStorageService {
        public set = ( key: string, value: any ) => {
            this.$window.localStorage[key] = value;
        };

        public get = ( key: string, defaultValue: any ) => {
            return this.$window.localStorage[key] || defaultValue;
        };

        public setObject = ( key: string, value: any ) => {
            this.$window.localStorage[key] = JSON.stringify( value );
        };

        public getObject = ( key: string ) => {
            if ( this.$window.localStorage[key] ) {
                return JSON.parse( this.$window.localStorage[key] );
            } else {
                return undefined;
            }
        };

        constructor(
            private $window: ng.IWindowService // here you depend to an abstraction ...
            ) { }
    }
    app.service( "localStorageService",
        ["$window", // ... but here you have to specify the implementation
            Services.LocalStorageService] );
}

因此,在您的测试中,您可以轻松使用模拟对象,因为您的所有控制器/服务等都依赖于抽象。但是对于真正的应用程序,Angular需要实现。


我的(天真的)想法是使用 bind(PostsService).toClass(PostsServiceImpl) 来实现这一点,让 Angular 知道如何从抽象映射到具体。如果接口在运行时没有被删除,那么这可能是有意义的,正如 @basarat 指出的那样。就客户端代码而言,我希望完全隐藏注入特定实现的事实。 - dSebastien

3

TypeScript不为接口生成任何符号,但是您需要某种符号来使依赖注入起作用。

解决方案:使用OpaqueTokens作为符号,通过provide()函数将一个类分配给该标记,并在您的类的构造函数中使用Inject函数与该标记一起使用。

在这个例子中,我假设您希望将PostsServiceImpl分配给PostsService以在Posts组件中使用,因为当代码都在同一个地方时更容易解释。 provide()调用的结果也可以放入angular.bootstrap(someComponent,arrayOfProviders)的第二个参数,或者放入高于Posts组件的组件的provide []数组中。

在components / posts / posts.service.ts中:

import {OpaqueToken} from "@angular/core";
export let PostsServiceToken = new OpaqueToken("PostsService");

在你的构造函数中:
import {PostsService, PostsServiceImpl, PostsServiceToken} from 'components/posts/posts.service';
import {provide} from "@angular/core";
@Component({
    selector: 'posts',
    providers: [provide(PostsServiceToken, {useClass: PostsServiceImpl})]
})
export class Posts {
    private postsServices: PostsService;

    constructor(
        @Inject(PostsServiceToken)
        postsService: PostsService
        ) {
        console.log('Loading the Posts component');
        this.postsServices = postsService;
        ...
    }

    ...
}    

1
在TypeScript中,您可以实现类。

示例

config.service.ts:

export class ConfigService {
  HUB_URL: string;
}

export class ConfigServiceImpl implements ConfigService {
  public HUB_URL = 'hub.domain.com';
}

export class ConfigServiceImpl2 implements ConfigService {
  public HUB_URL = 'hub2.domain.com';
}

app.component.ts:

import { Component } from '@angular/core';
import { ConfigService, ConfigServiceImpl, ConfigServiceImpl2 } from './config.service';

@Component({
  ...
  providers: [
    { provide: ConfigService, useClass: ConfigServiceImpl }
    //{ provide: ConfigService, useClass: ConfigServiceImpl2 }
  ]
})
export class AppComponent {
}

你可以为服务提供不同的实现。

我已经使用过这种方法,效果非常不错。 - wickdninja
我倾向于仍然使用I作为基类的前缀(例如IConfigService),即使它不是一个实际的接口。这种命名约定至少可以传达我的类意图。 - wickdninja

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接