Where should I create data models?

Hey guys,

This is probably a beginner question, I’ve recently started trying out Ionic 3 (and Angular) and I’m not too sure where I should keep my “models”?

I’m trying to define a custom class, I was thinking to create the class as a provider. Then I read somewhere the providers are used as DI services, in my case, it’s really just a data model with some helper functions.

Thanks

4 Likes

I have found “just a data model with some helper functions” to be a generally bad fit for the JavaScript world. I have had better luck with smart providers and dumb data, so I would do something like this:

export interface Customer {
  id: string;
  name: string;
}

export interface Purchase {
  id: string;
  customerId: string;
  productId: string;
  quantity: number;
}

@Injectable()
export class Provider {
  constructor(private _http: Http) {}

  customerById(id: string): Observable<Customer> {
    return this._http.get('/api/customer/' + id).map(rsp => rsp.json());
  }

  purchasesByCustomer(id: string): Observable<Purchase[]> {
    return this._http.get('/api/purchases?customer=' + id).map(rsp => rsp.json().purchases);
  }
}

EDIT: I don’t know if it’s still a problem, but due to previous security concerns about naked JSON arrays, I always make my APIs return objects with a single array member instead of a raw array. Edited purchasesByCustomer() to reflect that.

4 Likes

Thanks, it looks promising I’ll give it a try.

1 Like

I use interfaces too, but I’ve found the following helps. I have a models folder with defining interface and useful constants that can be exported as needed. Example: WidgetsModel:

export interface Widget {
   id: string;
   photoURL: string;
   type: WidgetType;
}

export type WidgetType= bigWidget,
                        smallWidget,
                        undefined;

export const widgetInitialState = {
                                   id = '',
                                   photoURL: imagePlaceHolder,
                                   type: WidgetType.undefined
                                  }
export const imagePlaceHolder = //dataURI of a small image placeholder
1 Like

Generic warning about how const in JavaScript is less than worthless, because somebody can mess with your head like so:

let widget = widgetInitialState;
widget.id = 'ruh roh';

And now we’ve poisoned widgetInitialState. Because of this, I use functions here instead:

export function widgetInitialState() {
  return {
    id = '',
    photoURL: imagePlaceHolder,
    type: WidgetType.undefined
  };
}
2 Likes

Once again an old discussion. Seems like everybody is way past these concerns nowadays. But this Typescript “interface/class/providers” stuff is really twisting my brain.
I’m trying to create a simple Server object with properties and methods to use in some otherPages. Some methods would require external services like TCPcomms or Storage or whatever. Usually when using a “Provider”, this is “injected” as a parameter on the constructor, but when I do “new ServerObject” I only want to provide the properties not the list of required libraries. So far I came up with the following but there’s gotta be a better way. Just the naming “ServerType” and “ServerObject” are kind of weird to say the least.

serverStateMachine.js
server.ts

import { useMachine, useService } from "@xstate";
import { serverTCPmachine } from "./serverStateMachine";
export interface ServerType {
  name: string;
  ip: string;
}
export class ServerObject {
  name:string = "";
  ip:string = "";
  //constructor (tcp:TCPservices) {
  constructor(fields?:any) {  //-->how do I inject tcp:TCPservices ???
    if (fields!=undefined) {
      for (const f in fields) {
        this[f] = fields[f];  //should validate fields but out of scope for discussion
      }
    }
  //object methods
  public rename(newName:string):ServerObject { this.name=newName; return this; }
  public readress(newIP:string):ServerObject { this.ip=newIP;     return this; }
  ...
  //communication methods
  public getStatus()  { ... use stateMachine to connect and retrieve info }
  public getFiles() { ... }
  ...
  //internal TCP & xState mechanic
  private send(data) { tcp.send(data) }
  private read():string[] { return tcp.read() }
  }

serverView.ts

import { ServerType, ServerObject } from './server';
export class ServerViewPage {
  server: ServerType;
  serverStatus: string;
  constructor(navParams:NavParams) {
    this.server = navParams.get('server');
    this.serverStatus = this.server.getStatus();
  }

serverAdd.ts

import { ServerType, ServerObject } from './server';
export class ServerAddPage {
  server: ServerType;
  constructor() {
    this.server = new ServerObject( {name:"Server1", ip:"1.1.1.1"} );
  }

Can I just forget about the “interface” and just use “class Server” and be able to declare “server:Server” and use “this.server = new Server(...)” ? Or is this just a BAD idea ?

I won’t say it that strongly, but it’s a pattern I’ve decided to avoid, although perhaps primarily for a reason that may not strictly be relevant in your case: reducing bugs related to serialization and deserialization of objects. If we put the “intelligence” (i.e. “methods”) into the data-bearing object, then every time it goes or comes from JSON (most frequently, being dropped from heaven over the network), I have to remember to sprinkle magic pixie dust on it, and if I forget, I get an object that sort-of-looks-ok-but-doesn’t-actually-do-everything-it’s-supposed-to, which can make for some hard-to-diagnose and frustrating bugs. However, there’s another huge benefit of this that you’ll see in a minute.

So the rule I use is “never manually instantiate any objects that I define”. There are largely unavoidable times where you have to new a Something object defined by an external library, but I don’t new anything that I make. Either I let Angular’s DI manage it and inject when needed, or I make it an interface so that I can just assign {foo: "bar", baz: 42} to it.

A truly OO language would not allow you to make half-assed objects that have all their data members but not their function members, and this is how I choose to keep myself from stepping on that particular rake. I don’t know if it is now a concern about passing objects with prototypes through NavParams, but if they are going into URLs you get the exact same serialization worrries, because that’s how things get stuffed into query strings. I wouldn’t do it.

A slightly separate concern I have is with rename and readdress. You have to decide if, from the POV of a Server user, the name and address are intended to be exposed or encapsulated. My instinct says to encapsulate it, in which case rename and readdress shouldn’t be public.

So what I would do is put all the other methods you currently have on ServerObject into a DI-managed ServerService, and give them an explicit Server parameter indicating the data-bearing PODO they are to work on.

export interface Server {
  name: string;
  ip: string;
}

@Injectable()
export class ServerService {
  constructor(tcp: TCPservices) { 
    // previously mentioned "huge benefit"
    // woo hoo no problem injecting tcp here
  }

  watchStatus(server: Server): Observable<ServerStatus> { ... }
}
1 Like

Thanks a lot for this quick yet elaborate answer. If I get this straight,

  • ServerService.create({name:"Server1", ip:"1.1.1.1"}) or similar would be used for instantiating a new Server

  • ServerService.tcpXYZ(thisServer:Server) would be used to communicate with thisServer then save(…) a local copy of the file list for example

  • ServerListService would be the one using ServerService.create() and then storing the server parms in the list of server

  • ServerViewEditPages would also use ServerService.methodsXYZ to rename/readdress/refreshInfo/refreshFilesList/loadFile

And yes rename/readdress/andOtherParms are exposed to allow the user to modify them without having to delete/add the server causing a reload of file list. The server’s file list is stored locally, takes a while to load and the server connection parms change more frequently than the list.

1 Like

No, this becomes way easier because of duck typing.

let server = {name: "Server1", ip: "1.1.1.1"};

If it has a name and an ip, it’s a Server. No need to create or new anything.

I’m not entirely clear on what “file list” here means, so I’m going to pretend that you’re dealing with something like managing FTP servers, so “file list” means “stuff sitting on server X”. If that’s the case, I would declare

class ServerService {
  watchServerFiles(server: Server): Observable<FileList> {}
}

Another rule I have is “deliver clients things in the form that is easiest for them to work with, not in the form that is easiest to provide”. In my experience, that results in better division of labor balance between providers and consumers. I would want an Observable<FileList>, but if you don’t, then change that signature to match what you do want.

At this point, my instinct would be to merge ServerService and ServerListService. I’m not seeing enough of a case for separating them.

Yes here, especially if these modifications have side effects. If they don’t, and never ever will, then one can of course simply server.name = "new-name".

2 Likes

I’m currently porting my ObjectiveC iOS app to Ionic to make it multi-platform. At first glance, things can be converted 1:1 but the more I dig, the more I find differences. So once again, many thanks for your time and lights… you’ve been very helpful…

No, this becomes way easier because of duck typing.
let server = {name: "Server1", ip: "1.1.1.1"};
If it has a name and an ip , it’s a Server . No need to create or new anything.

Cool, but what about other properties with default values determined at creation, like 'createDatetime".
If it was not already obious, I guess that reveals my generation and legacy OOP background. obj=new object() feels comfortable for no good reason ;-/

…, so I’m going to pretend that you’re dealing with something like managing FTP servers, so “file list” means “stuff sitting on server X”.

Very good analogy, in fact they are song servers where the file list is a song list, with each item being Title & Artist. So the mobile keeps a list of servers with their song list. The song page shows the selected server’s list, you select a song, a detail page comes up with a button to get the content. It is pretty basic stuff. The main view is the song list view of the last visited server. Maybe TMI but nothing top secret here…

At this point, my instinct would be to merge ServerService and ServerListService . I’m not seeing enough of a case for separating them.

Agree. One of my earlier reflexion. My concern was the size of this module as it would have all the methods for the list, server, network communications, etc.

Yes here, especially if these modifications have side effects.

Yes, side effects would include updating server parms in the list and saving that list for next access, for example it the IP address changes.

1 Like

Ordinarily my initial reaction would be to allow undefined for properties like that, and do the default handling whereever the value is needed, but you’ve given a pretty pathological example here, because the default wouldn’t be the same at access time. If there is a registration bottleneck, such as registerNewServer in ServerService, I guess I would consider adding it there if needed. As far as TypeScript syntax goes, I make liberal use of optional properties (foo?: string) so that object literals don’t have to provide everything to be copacetic.

That sounds like a great use case for RxJS in general, because you can hide a lot of complexity behind a single Observable<Song[]>, that clients need know absolutely nothing about the details of, such as a lazily-fetched cache with periodic polling or push notification. All of that can be added progressively if needed by just modifying stuff inside ServerService, with zero impact on client code.

Fair concern, and something worth keeping in the back of your head. I’ll put it this way: I find myself more often merging several little services than splitting up big ones, and it’s irritating when I have to hunt around a bunch of little classes to find bugs that live in the crevices between them.

2 Likes