I need a Service Locator

Last modified 2 years, 113 days ago

This started as notes to try to solve a problem I’ve developed over time with making multiple websites/web-based projects. That problem is re-use of code. I keep making similar database structures, user-authentication systems, etc, and it really saps time away from developing whatever is unique about a particular project when I keep having to implement stuff I’ve already done. Even copying it over from an existing repository can be draining.

There’s a design pattern for this called Service Locator. The concept is to abstract the requirement of a service from its concrete definition. To be able to get something I know I need without having to know in code exactly where it is. This wouldn’t be a problem if everything was in one big project, but my code ain’t that simple. Importing pieces of projects into a new one and then using them requires that they aren’t necessarily in the same logical space all the time.

So let’s start with

What problems am I trying to solve?

What sacrifices will I have to make?

First off, I know I’m going to modify the default models file that comes with Lapis. There’s no way around it, because I want to just import from models with impunity, not caring where the model is located. But I don’t want everything to be fixed in that file, because then I’ll have ridiculousness like importing a random utility function from models, which is for, you know, models.

Second, whatever other file I define for this has to be located in the same location in every project from now on, because without that, nothing can access the locator to find other things!

While I do work on Windows, I tend to work more often on Linux, so I’m fine with making scripts and such that only run on Linux. (For example, I am most familiar with Lua, so making Lua scripts without an extension, a #!/usr/bin/lua (or whatever it is) at the top, and chmod‘ing them is sufficient for my scripting needs. Then again, for the simpler stuff, I might as well just use .sh files with #!/bin/bash

What do I want usage to look like?

For sub-applications, the most important thing is for them to know WHERE they are before they can do anything:

-- v1
import pwd from require "locator" -- almost named this location, but pwd is easier to type and seems more appropriate
path = pwd(...)
import user_view from require "#{path}/views" -- now this is possible

-- v2
locator = require "locator"
import user_view from locator(..., "views")

-- v3
locate = require("locator")(...) -- returns a function that can be used to build a locally-scoped require-like function
import user_view from locate "views" -- I like this version most because ease of use

Where the hell are my models? In order to find out, the locator has to have some concept of storing where to look..and somehow also has to know where to look for anything at all times in order to work no matter what order things are called in. (I can’t do something like locator.registerModelPath(..., "models").) One solution is to have two always-in-the-same-place files, the locator itself, and a config file for paths that it will require (as well as anything else I may need). My only concern with this idea is with updating the locator itself.

Related to that, I’ve been doing a bunch of reading about how Lua handles modules and packages because I was confued about how Lua knows to translate require('name') to ./name/init.lua sometimes, but not other times. Seems to be it’s a feature of the Love engine that I didn’t realize wasn’t part of standard Lua (standard Lua only looks in special module installation paths based on package.path, my guess is Love’s implementation defines a game’s source code directory as one of these paths, making that function there, and nowhere else).

I think as a solution to being able to later expand or extend the locator in the future, the locator should be in a directory as a init.lua file (when compiled, I will be writing it in MoonScript), and you will need to manually add a file with the same name as that folder with return require((...) .. '.init') in it. This doesn’t solve the config file though. I can’t add it to a gitignore, and can’t leave it hanging inside a subtree. Config will have to be just another file… OR could just be incorporated into Lapis’s config system if I figure out exactly how that works. Lapis’s config system is available during server execution, but that happens separately from migrations.

Now, that doesn’t solve things for models, unless I also include a small shim for it to require something inside of the locator project.

I lost my train of thought so here’s a shortened less-train-of-thought’d list of things I need to do

-- so now I'm gonna write some more "how I'd like to use it" code
locate = require("locator")(...) -- returns a function that can be used to build a locally-scoped require-like function
-- maybe the order of these should be attempting to use local scope first! (if locate is a fn grabbed from factory)
import user_view from locate "views" -- will try to use main views, then the views in each sub-application as specified by the config (haven't decided on config format yet)
import Model from locate "models" -- will try the main models table first, then in each sub-application (basically, one simple method for everything solves several problems at once)
-- NOTE the locator should handle searching for a folder with an init.lua inside of it and returning that!

-- locate "migrations" -- would just return the normal migrations table.. I want something a little better
locator = require "locator"
print locator.migrations -- this is a table of all migrations (as specified by config format (which will contain legacy-supporting options))
import some_func from locate "utility.string" -- this is how I would access the utility functions
-- legacy option for migrations would be specifying what key to START migrating from on specific sub-applications' migrations (so older keys are ignored)
-- NOTE to do this, I will be sorting migration tables

-- in order to support the before_filter ... I'm not sure yet
-- for the shared state / modifying a request... I think I leave this up to how sub-applications handle things, this was more of a note to myself about how I want to rewrite users
-- as for how the locator knows where something is, it tries a local scope first if specified, then it tries global, then it tries each registered sub-application (specified by unknown config format)
-- commands to update subtrees / config will be added later if they are added
-- as for it updating as a subtree, I need to re-read this whole document and start writing it

And that’s all for now.

This story doesn’t have an ending just yet, but I have a pretty solid foundation to decide how to proceed from.

Originally published February 10, 2018

Previous Post Next Post