0

I am using Ionic 4 and Angular 7 to create a hybrid mobile app. This app will communicate with an API that I have developed in PHP (Yii2) to display data to the users.

I want to create a generic REST API class to communicate with the server, and I have used this article as a baseline for my development (https://medium.com/@krishna.acondy/a-generic-http-service-approach-for-angular-applications-a7bd8ff6a068).

From my understanding reading multiple other articles and documentation, the serializer class example in that article should declare the methods fromJson() and toJson() as static. I am used to doing this kind of stuff in PHP. Since there is no state to this object and these are essentially helper functions, I don't want multiple instances of these classes around when I'm really only using these helper functions (help me understand if I am wrong here).

However, in the rest.service.ts file those functions are being called on an instance (this.serializer.fromJson(data)). To call a static method from what I've read I need to call it like ClassName.staticMethod() but in the context of rest.service.ts I cannot do that since I do not know how to call a static method dynamically on a variable (this.serializer.staticMethod()). In PHP I am used to being able to call $className::staticMethod() which is what I am trying to find an equivalent for here but it doesn't seem possible.

I found this question (Dynamically calling a static method) which seems to be touching on what I want to do but it seems like there must be a better way. Doing it that way in each one of my XYZ.service.ts classes I will have to write out the same map using the serializer class' static methods, which seems to be repeating myself and makes me think there must be a better practice for what I am trying to achieve.

Another reason for all of this is that in my serializers I sometimes need to reference another serializer for subresources. Right now I am creating an instance to do this, but this seems inefficient and further makes me think that I should be finding a way to do this statically since I may have multiple instances of XYZSerializer created just to be able to run the methods that I feel should be static.

Here is an example of the working code I have right now, modeled after the above article, abbreviated for ease of reading:

export interface Serializer {
    fromJson(json: any): ApiResource;
    toJson(resource: ApiResource): any;
}

export class SharkBiteSerializer implements Serializer{
    playerSerializer: PlayerSerializer = new PlayerSerializer;

    fromJson(json: any): SharkBite {
        const sharkBite = new SharkBite();
        sharkBite.id = json.id;
        ...
        sharkBite.featuredPlayer = this.playerSerializer.fromJson(json.featuredPlayer);
        return sharkBite;
    }

    toJson(sharkBite: SharkBite): any {
        return {
            id: sharkBite.id,
            ...
        };
    }
}

export class SharkBiteService extends RestService<SharkBite> {
    endpoint = 'shark-bites'
    serializer = new SharkBiteSerializer;

    constructor(
        httpClient: HttpClient,
    ) 
    {
        super(httpClient);
    }
}

export abstract class RestService<T extends ApiResource> {
    abstract endpoint: string;
    abstract serializer: Serializer;

    constructor(
        private httpClient: HttpClient,
    ) {}

    /**
     * Add a new record
     */
    public create(item: T): Observable<T> {
        return this.httpClient
            .post<T>(`${environment.apiUrl}/${this.endpoint}`, this.serializer.toJson(item))
            .pipe(
                map(data => this.serializer.fromJson(data) as T)
            );
    }

    /**
     * Update an existing record
     */
    public update(item: T): Observable<T> {
        return this.httpClient
            .put<T>(`${environment.apiUrl}/${this.endpoint}/${item.id}`, this.serializer.toJson(item))
            .pipe(
                map(data => this.serializer.fromJson(data) as T)
            );
    }

    /**
     * Retrieve a single record
     */
    public read(id: number): Observable<T> {
        return this.httpClient
            .get(`${environment.apiUrl}/${this.endpoint}/${id}`)
            .pipe(
                map(data => this.serializer.fromJson(data) as T)
            );
    }

    /**
     * Retrieves muleiple records based on a query
     */
    public list(httpParams: HttpParams): Observable<T[]> {
        return this.httpClient
            .get(`${environment.apiUrl}/${this.endpoint}`, {params: httpParams})
            .pipe(
                map((data: any) => this.convertData(data))
            );
    }

    /**
     * Delete a single record
     */ 
    public delete(id: number) {
        return this.httpClient
            .delete(`${environment.apiUrl}/${this.endpoint}/${id}`);
    }

    /**
     * Converts the array of items into an array of the typed items
     */
    private convertData(data: any): T[] {
        return data.map(item => this.serializer.fromJson(item));
    }
}

To try to reiterate the point of what I am trying to do, I would like to instead have fromJson and toJson be static methods. In rest.service.ts I should be calling SerializerClassName.toJson() and SerializerClassName.fromJson(); then in the SharkBiteSerializer I could call PlayerSerializer.toJson() and PlayerSerializer.fromJson() statically. I toyed around with using an abstract class Serializer to have the methods be static but I can't seem to figure out how to call a static method on a variable.

If I am not following a best practice here, please enlighten me. I am new to Typescript and I realize my PHP background may be seeping in. I am just trying to find the best way to implement this and am willing to take any suggestions. Thank you!

#

Update:

I understand the concept that I cannot call a static method on a instance of the class. I am wondering whether I could any way store the class name in a variable and then call the static method on the class name stored in that variable. For instance, in PHP I can:

$className = '\namespace\for\ClassName';
echo $className::staticMethod();

I would like some way to store the name of the serializer class on each rest service and have it call the static function from that serializer class. Or, if there is a better method of implementing what I am looking for I am open to ideas.

BVBAccelerate
  • 172
  • 1
  • 5
  • 17

1 Answers1

3

The short answer is, you can't - you can't call a static method on a variable because, on TypeScript, the static method belongs to the class, not the variable.

That said, you can hack it because TypeScript becomes JavaScript. It's not clean since you'll lose the appropriate typing that you achieve by using TypeScript, but at the very least it enables you to proceed on your frame of mind.

The TS code below...

class Foo {
    public static bar() {
        console.log("bar");
    }
}

Foo.bar();

...compiles to

var Foo = /** @class */ (function () {
    function Foo() {
    }
    Foo.bar = function () {
        console.log("bar");
    };
    return Foo;
}());
Foo.bar();

Which means that bar is not on the prototype (hence why it isn't inherited), instead, it leaves on the class itself.

The way to access the class function is through the constructor property, so:

class Foo {
    public static bar() {
        console.log("bar");
    }
}

function testIt(f: Object) {
    (f.constructor as any).bar();
}

let f = new Foo();
testIt(f);

Again, not clean, but it's a way.


Now, the big question here is: why do you really need a static method? I guess there's some habit to it (god knows I write a lot of static stuff until I remember I shouldn't), but usually it isn't necessary or even advisable. Static methods are leakage from procedural programming, having little or no place in Object Oriented code. You get little from it (less allocation, sure, but even this doesn't apply to your case), whereas you get a lots of benefits from common methods.

In you case, as you've seen in the article, you should define a Serializable interface just like you said (I'd call it Jsonable to be more aligned with the current trend) and define those two methods there. Now you have a simple way of calling then in every object. And you could create a base class (abstract class Jsonable) that would have base logic that could be used by those classes. The abstract class should contain common logic.

The only reason to have both the interface and the abstract base class is to avoid forcing objects to inherit the base class. So call sites should expect the interface, but you give the option that classes that use the basic behavior inherit from somewhere.

(as an aside, I'd not even do an abstract here, just an inheritable class with the virtual methods, which could be used both by Inheritance and Composition)

Of course, the base abstract class only makes sense if you can actually write generic code that does the required serialization. In the example you provided, SharkBiteSerializer is coupled to SharkBite, so it's not a good base class (you can't share it with other classes).

Bruno Brant
  • 8,226
  • 7
  • 45
  • 90
  • I'm not sure I need a static method. My understanding tells me that `fromJson` functionality should not require an object instantiation (aligns with this answer https://stackoverflow.com/questions/2671496/java-when-to-use-static-methods - they're both performing a conversion). If on one page I use SharkBiteService and PlayerService, now I have PlayerSerializer instantiated twice (once in SharkBiteSerializer and once in the PlayerService) which seems to be to be bad practice considering what it's doing. But I'm new to TypeScript so maybe my understanding is poor. I updated the question. – BVBAccelerate Mar 11 '19 at 23:43