Re Integrating Eventing - ldco2016/microurb_web_framework GitHub Wiki

import axios, { AxiosResponse } from "axios";

interface UserProps {
  id?: number;
  name?: string;
  age?: number;
}

export class User {
  constructor(private data: UserProps) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }

  fetch(): void {
    axios
      .get(`http://localhost:3000/users/${this.get("id")}`)
      .then((response: AxiosResponse): void => {
        this.set(response.data);
      });
  }

  save(): void {
    const id = this.get("id");
    if (id) {
      // put
      axios.put(`http://localhost:3000/users/${id}`, this.data);
    } else {
      // post
      axios.post("http://localhost:3000/users", this.data);
    }
  }
}

My end goal here via using Composition is to have my class User and rather than having a ton of methods inside of it, we want to have a couple of different properties that class User can use to delegate out some very important operations. So I just extracted out an eventing class and now I want to assign back to some property on class User called events.

Hopefully there will eventually be an attributes property and sync property for handling properties tied to the User and take care of the server saving stuff and what not as well.

So it's not just about integrating back the Eventing class, but also integrating back in attributes and something to handle sync as well.

import { dateStringToDate } from './utils';
import { MatchResult } from './MatchResult';
import { MatchData } from './MatchData';
import { CsvFileReader } from './CsvFileReader';

export class MatchReader {
  static fromCsv(filename: string): MatchReader {
    return new MatchReader(new CsvFileReader(filename));
  }

  matches: MatchData[] = [];

  constructor(public reader: DataReader) {}

  load(): void {
    this.reader.read();
    this.matches = this.reader.data.map(
      (row: string[]): MatchData => {
         return [

         ]
      }
    )
  }
}

The above is a MatchReader file. The MatchReader made use of the CsvReader(), so I made heavy use of Composition inside this project to get some amount of code reuse. CsvFileReader had all the logic of how to open up a csv file and extract some plain text out of it and do some basic parsing on the text.

So the whole idea is when I created an instance of MatchReader() I would pass into the constructor some property that satisfies the data reader interface. So when I created a MatchReader, I could pass in a CsvFileReader or maybe an API reader instead. So I can easily customize where the reader gets their information from just by passing a different argument to the constructor. Now to save on typing and avoid having to say return new MatchReader(new CsvFileReader(filename)); every time I want to create a class reader, I had created a class method which was called from csv which gave me a preconfigured copy of MatchReader that alreaady had a csv file loaded into it.

Now when I start talking about assigning that Eventing class back into the User, I essentially have to ask the same kind of question. How am I going to get that Eventing class back into the User.

This is not as easy for one important reason. The reason is slightly different this time around is that I already have a constructor and the constructor takes in some user information: constructor(private data: UserProps) or some data on this user.

So if i am talking about providing some dependency to the class through the constructor, I will have to work it in through some existing code.

So to take a look at a couple of ways of handling this, I am going to look at three different options.

I will do analysis on each one and decide which one might be the best.

// Option #1
// Accept dependencies as second constructor argument
export class User {
  constructor(private data: UserProps) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }
}

This first option to wire Eventing back into the User would be to still accept these dependencies as the second argument to the constructor or the initial data for the user and I am just going to say the second argument will be dedicated to taking in some dependencies to get the User to work correctly and so that would look like so:

// Option #1
// Accept dependencies as second constructor argument
export class User {
  constructor(private data: UserProps, private events: Eventing) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }
}

What the above means, everytime I want to create a user I would have to do something like new User({ id: 1 }, new Eventing());.

So every single time I want to create a user I would have to create a new Eventing() class as well and that means when I start to refactor out some different pieces of functionality I would have to add different arguments like so: new User({ id: 1 }, new Eventing(), new Attributes());, maybe its not the best approach for this case, but it could be an approach.

Now the second way of doing this would only to accept dependencies into the constructor and define a static class method to preconfigure User and assign properties afterwards:

// Option #2
// Only accept dependencies into constructor
// Define a static class method to preconfigure
// User and assign properties afterwards
export class User {
  constructor(private data: UserProps) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }
}

So the above should be similar to the MatchReader I just illustrated. I will say the constructor is always dedicated 100% to receiving configuration for the sub-modules of the class.

So the constructors inside the project will be 100% about receiving these composable sub-classes. We are not talking about inheritance, I mean to say we are going to receive these classes to make my classes work correctly into the constructor and the constructor will never be about taking in any initial data.

This clarifies the purpose of the constructor a bit, let's take a look at what that would look like:

// Option #2
// Only accept dependencies into constructor
// Define a static class method to preconfigure
// User and assign properties afterwards
export class User {
  static fromData(data: UserProps): User {
    const user = new User(new Eventing());
    user.set(data);
    return user;
  }
  
  private data: UserProps;
  
  constructor(private events: Eventing) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }
}

So the above would be the second approach. This approach is pretty good but simultaneously having to do this user.set(data) feels a bit nasty. In this case, initializing a user with the correct set of properties off of data is only one line of code, but imagine we have a more complicated class in the future that requires many lines of initialization and so I would potentially have to add a ton of configuration inside of this static class method and if I ever wanted to have some other configuration option like maybe instead of using Eventing class, I would use reallyCoolEvents class, well then I would have to duplicate the static class method, called new SUPEREVENTS() and possibly have many lines of initialization code as well and those would be easily duplicated between both classes, so it still does not feel that great as well, but at least its an improvement from option #1.

Option #3

// Option #3
// Only accept properties into constructor
// Hard code dependencies as class properties
export class User {
  constructor(private events: Eventing) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }
}

In this scenario, I will say the constructor is still all about receiving user properties. So doing some configuration of a user.

I will hard code the dependencies into this class as properties, so inside of my actual class user this thing will have the actual events properties that will be of type Eventing and I will just initialize it on the same line like so:

// Option #3
// Only accept properties into constructor
// Hard code dependencies as class properties
export class User {
  events: Eventing = new Eventing();

  constructor(private events: Eventing) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }
}

So what does this events: Eventing = new Eventing(); do?

So that means if I now do this:

// Option #3
// Only accept properties into constructor
// Hard code dependencies as class properties
export class User {
  events: Eventing = new Eventing();

  constructor(private events: Eventing) {}

  get(propName: string): number | string {
    return this.data[propName];
  }

  set(update: UserProps): void {
    Object.assign(this.data, update);
  }

  fetch(): void {
    axios
      .get(`http://localhost:3000/users/${this.get("id")}`)
      .then((response: AxiosResponse): void => {
        this.set(response.data);
      });
  }

  save(): void {
    const id = this.get("id");
    if (id) {
      // put
      axios.put(`http://localhost:3000/users/${id}`, this.data);
    } else {
      // post
      axios.post("http://localhost:3000/users", this.data);
    }
  }
}

const user = new User({});

user.events

So if I create a new user and pass in some initial data for it such as an empty object initially, this thing is going to have a preconfigured events module inside of it. I don't have to do any further configuration.

The downside to this approach is that we lose some of the benefits of Composition. Essentially, everytime I create a user I will have the same exact class serving as the events submodule inside of here.

Inside of MatchReader, I said that one of its strengths in using Composition is that I can swap out what source of information I am using on the fly.

export class MatchReader {
  static fromCsv(filename: string): MatchReader {
    return new MatchReader(new CsvFileReader(filename));
  }

  matches: MatchData[] = [];

  constructor(public reader: DataReader) {}

  load(): void {
    this.reader.read();
    this.matches = this.reader.data.map(
      (row: string[]): MatchData => {
         return [

         ]
      }
    )
  }
}

So back on the project above, having the ability to swap out that submodule of sorts real quick made a lot of sense, but on this project, the Eventing class is basically how all eventing libraries are put together and I am having a hard time imagining replacing my Eventing class with some alternate implementation. So on this project in particular and most especially with this particular class, it does not feel that risky to just say, its always going to be a class Eventing.

Is there really a big difference between eventing libraries that I should want to swap this out? I have a hard time believing that there is.

Even if I hard code Option #3 we can maintain some of the benefits of Composition. I can reuse the eventing class on other classes inside of my project.

All the code is inside of one file, so there are still benefits of Composition, but I just cant easily swap out Eventing on class User.

So in this project I went with option 3, because I did not see having to replace a lot of these different modules.

I could always say I no longer want to use Eventing(). I could use new SUPEREVENTS(); but at the end of the day I don't see that happening.

Option number three makes sense for this project.