Hacker News new | past | comments | ask | show | jobs | submit login

I dont think this makes much sense - a lot of them would likely share functionality and you could group them, like ISavable and just give that all save functions



In my experience I prefer grouping by domain over grouping by abstraction

An ISaveable interface would need to be generic but "save" functions might take different numbers of arguments, and of different types.

I've found the advantage of interfaces to be dependency injection, where i can inject a different implementation of an interface without breakijg anything (eg save to s3, to google cloud storage, to filesystem, to memory), or a test version of the function/class.

Abstractions like ISaveable on the service interface just make the code more fragile in my experience in an attempt to save some extra but simple lines of code. Granted it may make sense on active record model classes though.


I'll ignore the argument over whether a generic Save<T> (or even Repository<T>) type would be better.

I'm unsure about DI in general [1], but using the Objectifier-Pattern just to appease the DI-System is bad. Likely, you can use `Symbol()` to indicate how your code should be wired.

Decoupling your dependency seems reasonable, but you can do it much simpler.

  type GetProject = (...) => ...
  type ProjectCRUD = { get: GetProject, set: ... }

  constructor(private projectCrud: ProjectCrud)
Depending on the situation, I would go even further and inline the type definition:

  constructor(args: {
    getProject: (...) => ...,
    setProject: (...) => ...,
  })
[1]: https://news.ycombinator.com/item?id=31547975 - I'd love to hear opinions on that.


>Decoupling your dependency seems reasonable, but you can do it much simpler.

> type GetProject = (...) => ... > type ProjectCRUD = { get: GetProject, set: ... }

> constructor(private projectCrud: ProjectCrud)

Yep that is a more concise way of doing the same thing. I've considered that approach and it seems perfectly reasonable, even cleaner. The only reason I haven't is because constructors offer a more standardised approach to object creation for service type objects that other developers are familiar with, rather than higher order functions or function constructors. Although my method is kind of bespoke anyway so I could go either way. Another advantage of your approach is composing finer grained functions or objects with methods into more expansive services becomes delightfully easy.

>[1]: https://news.ycombinator.com/item?id=31547975 - I'd love to hear opinions on that.

I've tried to use TypeScriot DI containers... Oh I've tried.. But eventually they all just feel gross.

These days I'm perfectly happy having a bootstrap / createServices method for the application, or with multiple entry points each requiring a subset of services with different configurations, different create<command>Services functions. Works well for CLI apps. Downside is when there are too many entry points with different depenencies, like an HTTP API, you don't want to create a bootstrap method for each entrypoint. In this case I create all services once on bootup. I pass the request context as method argument. Don't have a perfect way yet of doing request-level services like GraphQL caching. Currently I lazy load request level services on request context.


Ah, I thought you were talking about _automatic_ DI. Apologies.

> Yep that is a more concise way of doing the same thing

Your interfaces still dictate a method name and signature that must be known by both parties. I would argue that this connection is the concern of the integrating layer. If either party changes, here's where the error should occur:

  const service = new ProjectService()
  const controller = new ProjectController({ 
    getProject: service.get.bind(service) // this is why I don't like js classes
    setProject: service.set.bind(service)
  })
Naturally, this needs some caution. If the boundaries aren't chosen well, the integration layer grows.

I think you're right that our fundamental difference is the desire to stick to a more standardized class-based architecture. I like js-classes to communicate that something is stateful. But conceptually, they're more hindering than helpful.


Don’t cache GraphQL at request level. Cache it at schema object level, like most (all?) off-the-shelf servers do.


I cache at the schema level for each request.

How do GraphQL servers automatically cache objects? I was not aware servers do this. I generally avoid them because of their inflexibility (eg. Unable to support the two websocket subscription sub protocols simultaneously).


Typically by using the id field of objects as cache key. Even some clients do this, which is nice, except if your objects aren’t unique by their ids. It can usually be configured, however.


How though do they know the ID before the object is created (/ fetched from db)?

Sounds like it's inferior to manually using a DataLoader within every resolver that fetches an object.

eg project has a client. In the resolver for a projects client, you fetch the client from DB. With DataLoader you manually cache the request for the client. With in-built caching you must resolve the client for the server to know the ID, and then on next resolve of the client, after fetching, it matches the ID and doesn't have to resolve it's fields again, but it still had to fetch both times?


Well, how could they cache something they haven’t fetched?


I think your intuition is getting there. Using DI always is an anti-pattern[0]. DI in general should only be used to nasty, circular-esque problems. If you can split your code between pure and impure code, and then just pass around the data, you mostly don't need it at all. Passing functions around is more complex than passive data, so you need a really good reason to do so.

The GP asked how to do mocking, and my answer is: Don't mock. Unit test pure code. Integration test impure code.

[0]: https://blog.ploeh.dk/2017/01/27/from-dependency-injection-t...


You're right. I don't actually DI always. Only (most of the time) when I have may have multiple implementations of a function, mock the function, or test it with different implementations of its depenencies.

I still use many static functions




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: