2
\$\begingroup\$

I want to organize mapping for my Angular app so that code structure would clear for other developers and I want to have as less code as possible.

I have an api service that is responsible for communicating with API. I create separate data services, which utilize the api service, to get backend data. The same data can be used in different features. I want to provide the data to some feature. But the feature needs a more complex model than backend provides. So, I want to do a complex mapping specific for the particular feature which extends the backend model. But I also do a basic mapping that is required for every feature like property naming ('_name' to 'name') or type converting (string to Date). What is your recommendation on how to approach this?

My current idea is the following. I put the data services in the app/core folder and do the basic mapping on this level. I have feature related models and mapper services in the app/feature folders. It looks OK in terms of separation of concerns. But I have doubts about the number of mapping functions and services. Looks wordy, doesn't it?

app/core/lesson

lesson-dto.model.ts

export interface LessonDto {
    id: string;
    _departmentId: string;
    begin: string;
    end: string;
}

get-lessons-response.model.ts

export interface GetLessonsResponse {
    lessons: LessonDto[];
}

lesson.model.ts

export interface Lesson {
    id: string;
    departmentId: string;
    begin: Date;
    end: Date;
}

lesson-basic-mapping.ts

export function mapLessonDtoToLesson(lessonDto: LessonDto): Lesson {
    return {
        id: lessonDto.id,
        departmentId: lessonDto._departmentId,
        begin: new Date(lessonDto.begin),
        end: new Date(lessonDto.end)
    };
}

lesson.data.service.ts

export class LessonDataService {

    constructor(private readonly apiService: ApiService) { }

    public getLessons(
        departmentId: string
    ): Observable<Lesson[]> {
        const requestUrl = `/departments/${departmentId}/lessons`;

        return this.apiService.get(requestUrl).pipe(
            map((response: GetLessonsResponse) => response.lessons
                .map(mapLessonDtoToLesson))
        );
    }
}

app/features/schedule

schedule-lesson.model.ts

export interface ScheduleLesson {
    id: string;
    name: string;
    begin: Date;
    end: Date;
    description: string;
}

scheduler-mapping.ts

export function mapLessonToScheduleLesson(lesson: Lesson): ScheduleLesson {
    return {
        id: lesson.id,
        name: calculateName(lesson.id, lesson.departmentId),
        begin: new Date(lessonDto.begin),
        end: new Date(lessonDto.end),
        description: doSomeOtherComplexMappingHere(lesson.departmentId)
    };
}

lesson-mapped-data.service.ts

export class LessonMappedDataService {

    constructor(private readonly lessonDataService: LessonDataService) { }

    public getLessons(
        departmentId: string
    ): Observable<ScheduleLesson[]> {
        return this.lessonDataService.get(departmentId).pipe(
            map((lessons: Lesson[]) => lessons
                .map(mapLessonToScheduleLesson))
        );
    }
}

One more thing that concerns me is that the more models I have or the longer they are the more similar code I have. I did some analysis. I like the approach with decorators. It looks similar to Newtonsoft in C# and hides the implementation of basic mapping. But it requires some hacky solutions to align it with TypeScript. And the feature of decorators is still unclear. So, I don't use it.

Do you have any ideas on how to make the code better (more clear and less to read)? And what do you think of the file/folder structure?

Thank you in advance.

\$\endgroup\$
2
  • \$\begingroup\$ Welcome to the Code Review Site where we review working code from your project and provide suggestions on how to improve that code. Is this code implemented and working yet, if not then the question is off-topic. If you are still in the design stage then try software engineering but read their guidelines for asking first. \$\endgroup\$ – pacmaninbw Oct 27 '20 at 0:16
  • \$\begingroup\$ Thank you for your message. Indeed, even though it is an implemented code, the main question is about design. But I see in the description of the community the first mentioned topic is 'Application of best practices and design pattern usage'. That is exactly I am asking for. So, I think, this question can be here. If there is no any answer in a short time, I will try Software Engineering. As I've just known they also help with questions about practices. Thank you very much for this recommendation. \$\endgroup\$ – greatromul Oct 27 '20 at 7:53
2
\$\begingroup\$

I made some good experience with the following approach. Its more code, but it helped the other developers in the team (and especially the new ones) to get a good grip and a fast start.
As always, many roads, choose the one that fits best in your context, they all have drawbacks. :-)

First i describe the expected data from the Backend

export interface AddressJSON{
  street: string,
  city: string
};

export interface UserJSON{
  id: number,
  name: string,
  address: AddressJSON
}

That helps to understand what to expect from the backend. Also its easier to track mapping problems (for example a Java LONG that is stored in a JavaScript NUMBER => Number has a smaller value range)

Then i create the FE models and if the mapping between backend and FE is easy, then i add converter methods

class Address {
  public street: string;
  public city: string;

  constructor(street: string, city: string){
    this.street = street;
    this.city = city;
  }

  public toJSON(): AddressJSON{
    return {
      street: this.street,
      city: this.city
    }
  }

  public static fromJSON(address: AddressJSON): Address{
    return new Address(address.street, address.city);
  }
}


class User {
  public name: string;
  public address: Address;
  private readonly id: number;

  constructor(id:number){
    this.id = id;
  }

  public toJSON(): UserJSON {
    return {
      id: this.id,
      name: this.name,
      address: this.address.toJSON()
    }
  }

  public static fromJSON(user: UserJSON): User{
    const newUser: User = new User(user.id);
    newUser.name = user.name;
    newUser.address = Address.fromJSON(user.address);
    return newUser;
  }
}

As you can see the entities can be stacked.
One reason i choose classes over interfaces for my entities is that i can move functionality directly to the entities.
And, i can FORCE the way this entity has to be used. For example i can disallow the creation of a artificial user without an id.
It reduces the risc for later changes. And it enforces a deeper understanding of the business requirements behind those entities.
On the over side its much more work at the start.

Most people who didn´t liked this approach hated the extra work, that you have to invest. The people who liked it, mostly highlighted the better understanding of the business requirements and the possibly to enforce correct usage.

\$\endgroup\$
4
  • \$\begingroup\$ I see, you use static for fromJSON and do not use it for toJSON? Why is there a difference? Do I understand correctly that every instance of the User class will have toJSON method and will not have fromJSON, because the last one belongs to the class itself. Does it make sense to make both static? \$\endgroup\$ – greatromul Nov 30 '20 at 15:43
  • \$\begingroup\$ The "fromJSON" is static, because the method will create the instance out of the JSON. The "toJSON" is an instance method, because it will take the current instance and create a JSON out of it. You could makte "toJSON" static also, but then the method would need a parameter (''toJSON(instance: User):UserJSON''), because only then it could know WHAT should be converted into a JSON. With a instance method you don´t need that parameter because it knows that it should convert itself. \$\endgroup\$ – JanRecker Nov 30 '20 at 17:10
  • \$\begingroup\$ Oh, thanks. I understand the idea. What I actually like is that the mapping methods are close to the model. We can use the advantage of intellisense. We can also decrease the memory footprint using static methods. The footprint itself can be a drawback. For example, mapping service + interface are less memory consumptive. But I actually find your approach good enough in terms of readability. \$\endgroup\$ – greatromul Nov 30 '20 at 17:48
  • 1
    \$\begingroup\$ :-) In my experience the cases were the memory footprint or the CPU usage is more relevant then the readability are a bit rare in modern times. Also, IF there is a performance problem, its much easier to tackle it if the code is understandable. :-) Therefor i first try to make my code nice and shiny, only then i start with a performance analysis and (if really necessary) start to optimize. :-) But as always, three developers, 4 approaches :-) \$\endgroup\$ – JanRecker Nov 30 '20 at 18:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.