4

Scenario:

I have a situation where I need to refactor a Web service with a single API endpoint that uses complex logic to insert, update and fetch data from a database. The clients use one call, and depending on the data transfer object they send, the service is doing different things (the API was not designed by me so I cannot do anything about it - it needs to support existing clients).

So for example, a client sends the following JSON:

{
    "Id": 1234,
    "FacebookToken": "some facebook access token"
}

Then the service needs to respond with data fetched from the database. All the fields in JSON request are optional, and depending on what kind of combination of them have values or not, and on the current content of the database, the service needs to either create, update or not modify an entity in the database, and return the created/updated/selected entity to the client.

Question

My question is if you can think of any design patterns that I could use to avoid implementing the logic as bunch of nested if statements in the domain layer. I'm not talking about splitting it into fine-grained functions (I already have that), but more about how to engage object orientation and polymorphism in the design, maybe utilize some well known design patterns.

I realize there is not one answer to this but I'm just wondering if anyone has faced similar design problem and have some good hints. Thanks in advance.

Example of a bad solution:

Here's a simple C# example of how I don't want the Service to be implemented. Note that it's intentionally simplistic to keep it short, normally I'd have interfaces and used dependency injection container etc. The part most relevant for the question here is the Service.PostRequest() method.

public class Request
{
    public string Id { get; set; }
    public string ExternalServiceToken { get; set; }
}

public class Response
{
    public string Id { get; set; }
    public string Name { get; set; }

    public override string ToString()
    {
        return $"{Id}: {Name}";
    }
}

public class UserEntity
{
    public string Id { get; set; }

    public string Name { get; set; }
}

public class Service
{
    private readonly Repository _repository;
    private readonly ExternalService _externalService;

    public Service(Repository repository, ExternalService externalService)
    {
        _repository = repository;
        _externalService = externalService;
    }

    public Response PostRequest(Request request)
    {
        if (request == null) // create new entity
        {
            return ResponseFactoryMethod(_repository.Create(null));
        }

        UserEntity entity;
        if (request.Id != null) // requested specific entity
        {
            entity = _repository.SelectById(request.Id);
            if (request.ExternalServiceToken != null && entity.Name == null) // update entity with Name
            {
                entity.Name = _externalService.GetName(request.ExternalServiceToken);
                _repository.AssignName(entity.Id, entity.Name);
            }
            return ResponseFactoryMethod(entity);
        }

        // entity.Id == null
        if (request.ExternalServiceToken != null)
        {
            string name = _externalService.GetName(request.ExternalServiceToken);
            entity = _repository.SelectByName(name); // try to find entity by Name, otherwise create
            if (entity == null)
            {
                entity = _repository.Create(name);
            }
            return ResponseFactoryMethod(entity);
        }
        return ResponseFactoryMethod(_repository.Create(null));
    }

    private Response ResponseFactoryMethod(UserEntity entity)
    {
        var response = new Response();
        response.Id = entity.Id;
        response.Name = entity.Name;
        return response;
    }
}

public class ExternalService
{
    private Dictionary<string, string> _names = new Dictionary<string, string>
    {
        { "abc", "John" },
        { "def", "Jane" },
        { "ghi", "Bob" }
    };

    public string GetName(string token)
    {
        return _names[token];
    }
}

public class Repository
{
    private readonly List<UserEntity> _entities = new List<UserEntity>();

    public UserEntity Create(string name)
    {
        var newUser = new UserEntity { Id = _entities.Count.ToString(), Name = name };
        _entities.Add(newUser);
        return newUser;
    }

    public UserEntity SelectById(string id)
    {
        return _entities.SingleOrDefault(e => e.Id == id);
    }

    public UserEntity SelectByName(string name)
    {
        return _entities.SingleOrDefault(e => e.Name == name);
    }

    public void AssignName(string id, string name)
    {
        UserEntity user = SelectById(id);
        if (user != null)
        {
            user.Name = name;
        }
    }
}

public class Client
{
    private static Service _service;

    public static void Main(string[] args)
    {
        var repository = new Repository();
        _service = new Service(repository, new ExternalService());

        RequestFromService(null);
        RequestFromService(new Request {Id = "0", ExternalServiceToken = "abc"});

        RequestFromService(new Request());
        RequestFromService(new Request { Id = "1", ExternalServiceToken = "def" });

        RequestFromService(new Request {ExternalServiceToken = "ghi"});
        RequestFromService(new Request {ExternalServiceToken = "ghi" });

        Console.ReadLine();
    }

    static void RequestFromService(Request request)
    {
        Response response = _service.PostRequest(request);

        string responseAsString = response != null ? response.ToString() : "null";

        Console.WriteLine("Response from service: " + responseAsString);
    }
}
Piotr
  • 151
  • 5
  • 1
    Create every possible key/parameter combination, order each one alphabetically and use them as keys for a HashMap that contains instances of classes that can treat the request with that specific combination. Now on every request, order all existing request parameters alphabetically and lookup the relevant treatment class from your Map. – ASA May 12 '16 at 08:43
  • @Traubenfuchs so you mean sort of a "Strategy" selector right? Yeah it is an idea. I'll think about it, thanks. – Piotr May 12 '16 at 09:11

1 Answers1

1

Having been the culprit of such an API, I feel your pain. I wound up being forced by my boss to go back and redo it without affecting existing implementations (yuck) and come up with a different answer.

A lot of the refactoring I did was to use properties from the incoming objects as keys to a dictionary, and get around it that way. Then, I added individual endpoints that went to these indicated functions directly to manage expansion and correct my mistakes. For example, if my endpoint was /api/sync, my dictionary looked like this

<String, Function>
<"Facebook", ProcessFacebookRequest()>
<"PowerSchool", ProcessPowerSchoolRequest()>
//And so on...

I then added endpoints for each particular 'generic' request

POST /api/sync/facebook
POST /api/sync/powerschool

Which is also only a tiny bit better. I really, strongly recommend a versioning scheme to make sure you have the flexibility to update and grow the API as your project does. So, right now the endpoint as-is is okay, but i would heavily consider doing something like /v2/sync/facebook/ becoming a standard in the near future so you can preserve old functionality and introduce new functionality without buggering either.

Adam Wells
  • 908
  • 4
  • 15