3

I want to use OCP rule in my "small" project. Actually i have a problem with implementing this one. Let me show you what have i wrote.

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);
  }
}

Do I write it good? If I do, then what actually i have gained? I need to send object of FadingAnimation class if i want to add this animation. If i want transform animation i need to send object of TransformAnimation class - so somewhere i have to use switch(this.animationType).

  • 1
    Perhaps the worst thing you can do for the quality of your TypeScript code is to apply patterns that originate in Java. – Aluan Haddad Apr 24 '18 at 05:38
  • 1
    Previous comment is likely correct. Should this code be object oriented at all? AnimationType doesn't have a state, what's the purpose for it to be a class? – Estus Flask Apr 24 '18 at 06:22
  • Actually, it can be interface also. Just want to implement OCP –  Apr 24 '18 at 06:25
  • 2
    Then you implemented it, I guess. Not sure what you've gained. Can probably be useless abstraction. Functions can be typed as well. Unless you intend to distinguish class instances with `animation instanceof TransformAnimation` (and this is something you don't want to do with OCP), there's no use for classes. – Estus Flask Apr 24 '18 at 06:30

3 Answers3

3

Principles and patterns are absolutely transferable between languages. They may be less applicable when you cross the bounds of a paradigm. SOLID principles and OOP patterns apply to any object-orientation, not just Java.

Remember that TypeScript/JavaScript are not just object-oriented though.

Start with a Problem

The problem isn't that you are attempting to draw from principles that come from other languages. The problem here is that you are attempting to start with the principle.

Good code design, whatever paradigm you use, starts with a problem that you want to solve. Write more code before looking for patterns or applying principles, so the design can emerge from the real problems you find as your application grows.

By starting with a principle, you are deciding too early what the problem may eventually be; and your design may take you further away from the goal of clean code.

Patterns and principles are highly transferable, but wait for signals that you have a problem, rather than prematurely applying some solution.

Simple is Better

In your case, you can just solve the problem the simplest way. If you have a test to guide you, you can easily update the implementation details later.

Perhaps in your case, a simple dictionary/object would be sufficient.

const animationType = {
    fading: {
        in: 'opacity: 0',
        out: 'opacity: 1'
    },
    transforming: {
        in: 'transform: translateX(-100%)',
        out: 'transform: translateX(100%)'
    }
};

You could type this to allow any key, if you didn't want to enforce fading and transforming - but you don't need to add this flexibility yet:

interface AnimationTypes {
    [key: string]: { in: string, out: string };
}
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • Thanks, but where could i use OCP? Do you know good example of using it in TS? –  Apr 24 '18 at 07:50
  • Take a look at your business grade printer. You don't depend on a "Super Printer 586", you just use the abstract concept "Printer". That makes it possible to add a "Super Printer & Stapler 593", it prints _and_ staples. Awesome. You didn't change the original printer and it can still be used to print stuff. You didn't change the original printer, or the abstract concept of a printer - but you have extended it. I hope I haven't over-stretched the metaphor. – Fenton Apr 24 '18 at 07:58
2

The code above doesn't really need to be object oriented. AnimationType class doesn't have a state, cannot benefit from multiple instances and thus isn't needed. Unless class instances are distinguished with animation instanceof TransformAnimation (and this is not something that should be done with open-closed principle), there's no use of these classes.

AnimationType class can be replaced with functional approach:

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);
  }
}

Depending on what AnimationCreator does, it may be replaced with a function as well or be abolished in favour of using animation factory functions directly.

This may change if AnimationType is stateful, e.g. it accepts transform % as constructor parameter. But in this case it's unclear why transform property should considered of a different kind than timing - both can vary from one animation to another and thus be defined on construction. In this case AnimationCreator doesn't need timing, so it is stateless and its purpose becomes unclear.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
1

I agree with Aluan Haddad. Do not think in plain JAVA-Patterns here. Although Typescript looks a lot like C# and therefor even a little like JAVA, it is still JavaScript in the end. The rules of inheritance, as you know them from JAVA, do not work here in depth. Do not try to build up complex inheritance patterns or related design principles. Using Typescript you have to adjust your way of coding.

A good approach to your problem would be using a plain service with 2 methods that return the needed object.

e.g.

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%)' };
    }

}
  • Hmm.. so actualy i shouldn't use OCP in typescript, yes? –  Apr 24 '18 at 06:42
  • Yes, that's my recommendation. You will get stuck pretty soon the more complex your code gets. I took the same long road 2 years ago, coming from JAVA either. You have to get used to services as your matter of choice here. –  Apr 24 '18 at 06:47