TypeScript - 开闭原则

3

我希望在我的“小型”项目中使用OCP原则。实际上,我在实现这个原则方面遇到了问题。让我展示一下我写的内容。

abstract class AnimationType {
    public abstract createAnimation(timing: number): { in: string, out: string };
}

class FadingAnimation extends AnimationType {
  public createAnimation(timing: number): { in: string, out: string } {
    return { in: 'opacity: 0', out: 'opacity: 1' }
  }
}

class TransformAnimation extends AnimationType {
  public createAnimation(timing: number): { in: string, out: string } {
    return { in: 'transform: translateX(-100%)', out: 'transform: translateX(100%)' }
  }
}

class AnimationCreator {
  private timing: number;

  constructor(time: number) {
    this.timing = time;
  }

  getAnimation(animation: AnimationType): { in: string, out: string } {
    return animation.createAnimation(this.timing);
  }
}

我写得好吗?如果是,那我真正获得了什么?如果我想添加淡入淡出动画,我需要发送FadingAnimation类的对象。如果我想要转换动画,我需要发送TransformAnimation类的对象 - 所以我必须在某个地方使用switch(this.animationType)。


1
可能对于您的TypeScript代码质量而言,最糟糕的事情就是应用源自Java的模式。 - Aluan Haddad
1
前面的评论很可能是正确的。这段代码真的需要面向对象吗?AnimationType 没有状态,它作为一个类的目的是什么? - Estus Flask
实际上,它也可以是接口。只是想要实现OCP。 - user6372887
2
那么你已经实现了它,我猜。不确定你获得了什么。可能是无用的抽象。函数也可以被分类。除非你打算使用 animation instanceof TransformAnimation 区分类实例(这是你不想在 OCP 中做的事情),否则类没有用处。 - Estus Flask
3个回答

3
原则和模式在各种编程语言之间是可以完全迁移的。当您跨越范例的界限时,它们可能不太适用。SOLID原则和OOP模式适用于任何面向对象编程,而不仅仅是Java。
请记住,TypeScript / JavaScript 不仅仅是面向对象的。
从问题开始
问题不在于您尝试从其他语言中获取原则,而在于您试图从原则开始。无论使用哪种范例,良好的代码设计都始于您想要解决的问题。在寻找模式或应用原则之前先写更多的代码,这样设计可以从您应用增长中遇到的实际问题中出现。
通过从一个原则开始,您会过早地决定问题的可能性,并且您的设计可能会使您远离清洁代码的目标。
模式和原则非常可转移,但请等待信号表明您有一个问题,而不是过早地应用某个解决方案。
简单就是美
在您的情况下,您可以以最简单的方式解决问题。如果有测试来引导您,稍后可以轻松更新实现细节。
也许在您的情况下,一个简单的字典/对象就足够了。
const animationType = {
    fading: {
        in: 'opacity: 0',
        out: 'opacity: 1'
    },
    transforming: {
        in: 'transform: translateX(-100%)',
        out: 'transform: translateX(100%)'
    }
};

如果您不想强制实施 fadingtransforming,可以键入此内容以允许任何键 - 但是目前您还不需要添加此灵活性:

interface AnimationTypes {
    [key: string]: { in: string, out: string };
}

谢谢,但是我在哪里可以使用OCP呢?你知道在TS中使用它的好例子吗? - user6372887
看看你的商用打印机。你不依赖于“超级打印机586”,而是使用抽象概念“打印机”。这使得添加“超级打印机和订书机593”成为可能,它可以打印并装订。太棒了。你没有改变原始打印机,它仍然可以用来打印东西。你没有改变原始打印机或打印机的抽象概念-但你已经扩展了它。我希望我没有过度引申这个比喻。 - Fenton

2
上面的代码并不需要面向对象。AnimationType类没有状态,无法从多个实例中受益,因此不需要。除非使用animation instanceof TransformAnimation来区分类实例(这不应该使用开闭原则),否则这些类没有用处。
AnimationType类可以用函数式方法替换:
interface IAnimation {
  type?: string;
  in: string;
  out: string;
}

type TAnimationFactory = (timing: number) => IAnimation;

const createFadeAnimation: TAnimationFactory = (timing: number) => {
   return { in: 'opacity: 0', out: 'opacity: 1' };
}

class AnimationCreator {
  private timing: number;

  constructor(time: number) {
    this.timing = time;
  }

  getAnimation(animationFactory: TAnimationFactory): IAnimation {
    return animationFactory(this.timing);
  }
}

根据 AnimationCreator 的功能,它也可能被替换为函数或者被废除以直接使用动画工厂函数。如果 AnimationType 是有状态的,例如它接受变换%作为构造函数参数,则情况可能会改变。但在这种情况下,不清楚为什么 transform 属性应该被视为与 timing 不同的属性 - 两者都可以从一个动画变化到另一个动画,因此可以在构造时定义。在这种情况下,AnimationCreator 不需要 timing,因此它是无状态的,其目的变得不明确。

1
我同意Aluan Haddad的观点。在这里不要以纯JAVA模式思考。尽管Typescript看起来很像C#,甚至有点像JAVA,但最终它仍然是JavaScript。继承规则,正如你从JAVA中了解到的那样,在这里并不深入有效。不要试图建立复杂的继承模式或相关的设计原则。使用Typescript,您必须调整编码方式。
解决您的问题的一个好方法是使用一个普通服务,其中包含两个返回所需对象的方法。
例如:
import { Injectable } from '@angular/core';

@Injectable()
export class AnimationTypeService {

    constructor() {}

    getFadingAnimation(timing: number): { in: string, out: string } {
        return { in: 'opacity: 0', out: 'opacity: 1' };
    }

    getTransformAnimation(timing: number): { in: string, out: string } {
        return { in: 'transform: translateX(-100%)', out: 'transform: translateX(100%)' };
    }

}

嗯...实际上我不应该在TypeScript中使用OCP,是吗? - user6372887
是的,这是我的建议。随着您的代码变得越来越复杂,您很快就会陷入困境。我两年前也走过同样漫长的道路,从JAVA转换而来。您必须习惯将服务作为您的首选方式。 - user6749601

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