Closed francescomari closed 1 year ago
You can find some inspiration about hot to structure web application backends in How I write Go HTTP services after seven years by Mat Ryer. He also wrote a great book, Go Programming Blueprints, which has a ton of useful information about the same topic.
Code organization
I think the organization of the
internal/fe26
package could be improved. The package does three things. First, it inspect the command line and the environment variables to collect the necessary configuration. Second, it creates a router based on that configuration. Third, it starts an HTTP server using that router. I would split these responsibilities in three separate packages.Decouple the definition of the router from the HTTP server
Coupling the definition of the router and the launch of an HTTP server based on that handler prevents you from reusing that hander for different purposes or instantiate it for testing purposes. Fixing this is quite easy. Just move the call to
http.ListenAndServe
fromfe26Router
toFe26
. Of course, you need to changefe26Router
in order to return ahttp.Handler
.Decouple the configuration of the routers
Instead of creating a main router on the fly in
fe26Router
by reading the configuration directly from a global variable, a better approach would be to pass the router configuration when the router is created. That's relatively easy forfe26Router
:Unluckily, this change should propagate to the rest of the handlers. One handler and some of the code used by the handlers still use
Config
directly. This should be decoupled little by little. A very simple example is the redirect handler. You could implement it by leveraging function closures:You would register this new handler like this:
Make steps to test the /fe62 and /fe26.json routers
These two routers do very similar things, but they change the way they present data. There are some commonalities that can be exploited. First of all, they both have the same initial logic. Maybe you can encapsulate this into its own wrapper
http.Handler
. For example:With such a "filter" in place, you can focus about just the presentation logic in
ListFilesHTML
andListFilesJson
. I won't repeat everything, butListFilesHTML
should look something like that:When you register the handler, you need to wrap your handlers like this:
I find that this solution has two benefits. First, it prevents you from repeating the same code in two different handlers. Second, it improves the testability of your code. With this improvement, you don't need a real filesystem to test
ListFilesHTML
. Instead, you can create an instance oflistFileData
, put it in the request context, and use the packagehttptest
to test your handlers completely in memory. You can also go one step farther and isolate the new file data handler from file-system operations.The registration for this handler ties all the pieces together.
You can also unit test the new file data handler in isolation from the file system by creating a test implementaiton of
fileDataReader
to test every possible corner case.Don't swallow errors
If a component can't handle errors, it should just bubble them up to the caller, optionally adding some additionally context. Logging an error must be a conscious choice to handle that error. I have to admit that I don't know all the internals of this project, but the choice to log errors in
FileManager
instead of passing them to the caller looks a bit suspicious.For example,
FileManager.ReadDir
should return an error if it's not possible to read the directory, andNewFileManager
should probably return that error up the chain. Eventually, an HTTP handler will have to deal with it, and I think that a handler the right place to either log it or show an error message to the user (or both).The FileManager doesn't manage much
I think that the
FileManager
API could be simplified a bit. In order to use aFileManager
, I have to create one by invokingNewFileManager
passing both the URL and the document root. Once I have aFileManager
, the only thing I can do is invokeReadDir
, which will just list the files in the directory identified by the URL provided at construction time. Moreover, the result of reading the content of a directory is stored in the fields of theFileManager
itself.In this case, I would prefer to either have a function, like this:
or a type that encapsulates the context common to every call (e.g. the document root), like this:
Since the document root comes from the configuration and is not supposed to change for the lifetime of the application, I would prefer the second option. With the second solution, you could create just an instance of a
FileManager
and reuse it in all the handlers that need it. In addition, it's easy to mock aFileManager
for testing purposes and decouple functionality if you opt for the second solution.