4

I have recently inherited a codebase which has a weird problem and I am trying to search for an extensible solution that can solve my issue. Consider I have a model class that is used as a model to populate data in the UI which looks something like this.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
}

All the information gets reterived via the Webservices and the above class object is used to do the required job.

But this model class serves another purpose and that is to pass information which is not received as a part of JSON response and due to this, there are new properties introduced in the model object which are JSON Ignored, so consider in some particular screen I need to know if the person is a new person, since this property is not received from JSON, the model object now looks something like this

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    [JsonIgnore]
    public bool IsNewPerson { get; set; }
}

Now, the problem is this class is getting uglier as newer properties are getting introduced due to the requirements and these properties are not a part of the JSON response and only contribute to the UI and hence the JSONIgnore attribute.

My question is, What's the ideal way to handle such a scenario where the model keeps changing? As per the OCP the class shouldn't be modified every time. How do I extend this model class? Is it good practice to create a interface for model class? is it good to create a basemodel class?

Can anyone suggest a good design for this?

2 Answers 2

6

How to model classes that can be extendable?

In the field of software development, extensibility specifically refers to inheritance, and inheritance is not the correct approach here.

The above class object is used to do the required job. But this model class serves another purpose and that is to pass information which is not received as a part of JSON response.

"Another purpose" is an SRP violation. Different purposes should be represented individually, specifically so that a change to one does not inherently cause a change in the other.

The explanation in your question is a bit ambiguous, but if I understand you correctly, your application is a man in the middle, i.e. the second Person class you show is the data you send back to your end user, and the first Person class is the data you receive from an external web service?

If that is the case, then you should always separate these two DTOs, as they represent the exchange between different sets of layers (i.e. external service => you, and you => end user).

Which means that your code should contain several bits. The DTO from the external web service:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
}

Your own DTO which you return to your end user:

public class PersonModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
    public bool IsNewPerson { get; set; }
}

Note that I'm using a different name for clarity's sake here. You can choose your own name or even reuse the same one (if they're in different namespaces).

And then you need some mapping logic. This mapping logic is the exact spot where you can inject your additional IsNewPerson value. Note I'm just inventing some example logic here to showcase how you could cleanly structure this.

public PersonModel GetPersonById(int id)
{
    var person = _externalService.GetPerson(id);

    var isNewPerson = _magic8Ball.TellMeTrueOrFalse();

    return MapToPersonModel(person, isNewPerson);
}

private PersonModel MapToPersonModel(Person p, bool isNewPerson)
{
    return new PersonModel()
    {
        FirstName = p.FirstName,
        LastName = p.LastName,
        Address = p.Address,
        IsNewPerson = isNewPerson,
    };
}

Obviously, replace _magic8Ball.TellMeTrueOrFalse() with your custom logic.


If I misunderstood, and both purposes you talk about are both interactions between your application and the end user, there are a few more options you have.

You could still separate the classes like you did above, and then each endpoint simply uses their own class to achieve their result, without conflict.

Or you could promote reusability for the person-related field using composition (over inheritance). For example:

// Purpose 1
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
}

// Purpose 2
public class CreatedPerson
{
    public Person Person { get; set; }
    public bool IsNewPerson { get; set; }
}

Again, the CreatedPerson name is a generic example. A better name depends on what this "purpose 2" is, and you can choose a more apt name.

A third option is to consider that you might have this same IsNewXXX type of field for many different entities, not just person, so you could consider an even more resuable generic class:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
}

public class CreatedObject<T>
{
    public T Value { get; set; }
    public bool IsNew { get; set; }
}

This CreatedObject<T> makes it so that you don't have to create this same kind of class for every entity you expose. For example, if you expose Person, Animal and Car, now you don't have to write custom CreatedPerson, CreatedAnimal and CreatedCar classes, you can simply use CreatedObject<T> with the appropriate generic type (CreatedObject<Person>, CreatedObject<Animal>, CreatedObject<Car>)

Which of these approaches is the most appropriate heavily relies on context which your question is somewhat light on.

But the main takeaway here is don't use the same class for more than one purpose.

0

One alternative is to add behavior to objects.

You said the object "serves another purpose" in addition to be used to communicate json messages. So add both "purposes" (i.e. behavior) explicitly into the object instead of trying to "annotate your way out" the problem.

Add toApiMessage() and the other "purpose" as explicit methods. Adding all the behavior into the object is the only way to make it maintainable. That is, to be able to change the "data" and the "behavior" it influences in a single place.

It is also coincidentally the object-oriented way.

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.