Code Monkey home page Code Monkey logo

Comments (10)

jodosha avatar jodosha commented on May 21, 2024

@janko-m TL;DR Lotus offers tools to avoid monolithic applications and to test components in isolation.

Slices

It allows to run multiple applications in the same Ruby process, to encourage isolation of macro functionalities.

Imagine an user facing application, a JSON API and an administration backend.
I've seen a lot of cases like this where the extraction (into a separated service) of the single "slice" was hard because of the tight coupling between them. With Lotus you can build the project planning and developing those "slices" starting from day 0.

Framework duplication

Because of this specific need, you want to keep your configuration separated. For instance, the default response type of your JSON api is application/json, but this isn't the case of the web application that your users will see.

To avoid conflicts between configurations, Lotus has a technique called "framework duplication". It allows to (thread) safely duplicate the components and the configurations of each framework, so they can coexist in the same Ruby process without friction. That's why if you have Web < Lotus::Application and JsonApi < Lotus::Application, you have to include Web::Action and JsonApi::Action.

Testing in isolation

For convenience sake, we configure the behavior of the application with Lotus::Application.configure. The application is able is able to communicate with the several frameworks and to set the proper values. If you use default_format :json, it will set the corresponding value into Lotus::Controller::Configuration#default_format. In other words, you don't have to configure the single frameworks by hand.

# Tedious
router = Lotus::Router.new do
  # ...
end

Lotus::Controller.configure do
  default_format :json
end

Lotus::View.configure do
  root __root__ + '/app/templates'
end

# This is new ;)
Lotus::Assets.configure do
  prefix '/myapp'
end

vs

# Convenient
module Bookshelf
  class Application < Lotus::Application
    configure do
      default_format :json
      templates 'app/templates'

      routes do
        # ...
      end
    end
  end
end

Now, forget for a second the framework duplication. Imagine that you still include the old Lotus::Action, instead of Bookshelf::Action. Because the configuration for Lotus::Action happens in the application definition, you still need to load it to test your actions.

Lotus helps you with this. When you do Bookshelf::Application.load! it preloads and configure the frameworks, without loading all the application classes. In other words, it ignores all your controllers, views, presenters, etc..

In your testing suite you can do:

# spec/spec_helper.rb
RSpec.configure do |config|
  # You load the application environment once
  config.before :suite do
    Bookshelf::Application.load!
  end
end
# spec/controllers/home/index_spec.rb
require 'spec/spec_helper'
require 'path/to/home/index'

describe Bookshelf::Controllers::Home::Index do
  # ...
end

If you run that single spec with bundle exec rspec spec/controllers/home/index_spec.rb, you only have the application configuration and the single controller. Which I think it's a good level of isolation.

Conclusion

I hope all the explanations above are exhaustive on why things are designed like that in Lotus. Closing this. Feel free to ask other questions.

/cc @jeremyf

from hanami.

janko avatar janko commented on May 21, 2024

Thank you very much for the detailed explanation. I really appreciate that you care so much that everybody who wants to get involved understands the main concepts. I will try to tag on to your main points.

Framework duplication

To avoid conflicts between configurations, Lotus has a technique called "framework duplication". It allows to (thread) safely duplicate the components and the configurations of each framework, so they can coexist in the same Ruby process without friction. That's why if you have Web < Lotus::Application and JsonApi < Lotus::Application, you have to include Web::Action and JsonApi::Action.

Yes, this is what Lotus::Controller.duplicate does, right? How could the conflicts happen between configurations? Because if I configure Lotus::Controller itself, and include it in both of my microservices, then I expect those 2 microservices to both inherit that configuration (I shouldn't be suprised). I'm explicit about it, so the only conflicts that can happen are by my mistake (which I won't make if I know what I'm doing).

Testing in isolation

For convenience sake, we configure the behavior of the application with Lotus::Application.configure. The application is able is able to communicate with the several frameworks and to set the proper values. If you use default_format :json, it will set the corresponding value into Lotus::Controller::Configuration#default_format. In other words, you don't have to configure the single frameworks by hand.

That's a bummer, because this was for me one of main beauties of Lotus, that I configure each framework separately, that Lotus::Application only needs to do very little work gluing the frameworks together. Because, now for every new Lotus::Controller configuration, you need to remember to decide whether to add it to Lotus::Application also (and how do you decide that?). And the same for Lotus::View, Lotus::Assets and so on.

I really liked the explicitness, and it isn't tedious at all. I have the following structure for controllers (generalize it to views and others):

# my_app.rb
require "lotus"

module MyApp
  class Application < Lotus::Application
    load_paths << ["my_app/controllers"]
    routes do
      # ...
    end
  end
end
# my_app/controller.rb
require "lotus/controller"

module MyApp
  # this line would be if `Lotus::Application` wouldn't create it
  Controller = Lotus::Controller.duplicate(self) do
    # configuration
  end
end
# my_app/controllers/home.rb
require "my_app/controller"

module MyApp
  module Controllers
    module Home
      include MyApp::Controller
    end
  end
end

Now, forget for a second the framework duplication. Imagine that you still include the old Lotus::Action, instead of Bookshelf::Action. Because the configuration for Lotus::Action happens in the application definition, you still need to load it to test your actions.

You see, I configured the controller outside of my_app.rb, so I don't need to require it in the unit tests. And it feels so natural to configure my controller in MyApp::Controller instead of MyApp::Application. I originally did the above structure, and then it took me 4 hours to figure out that Lotus does it automagically, and that it's the reason why I was getting weird errors.

If you run that single spec with bundle exec rspec spec/controllers/home/index_spec.rb, you only have the application configuration and the single controller. Which I think it's a good level of isolation.

Yes, I agree, at the end it is testing in isolation. But it still strongly doesn't feel right that you need to require MyApp::Application in your controller tests, instead just having to require the controller you're testing. And Lotus is so close to not having to do that, because you made a lot of great decisions early on.

from hanami.

jodosha avatar jodosha commented on May 21, 2024

@janko-m You're welcome.

Without framework duplication there will be race conditions.
Look at this simplified example:

# json_api.rb
Lotus::Controller.configure do
  default_format :json
end

# web.rb
Lotus::Controller.configure do
  default_format :html
end

# At this point Lotus::Controller.configuration.default_format is :html
# When you load the code for both the applications, and then inject the configuration, they will both see `:html`.

module JsonApi
  class MyAction
    include Lotus::Action
  end
end

module Web
  class MyAction
    include Lotus::Action
  end
end

JsonApi::MyAction.configuration.default_format # => :html WRONG
Web::MyAction.configuration.default_format # => :html OK

Speaking of convenience, you may not find tedious to configure all the frameworks separately, but they are a lot of people that are finding the current usage of Lotus a bit verbose. So why not streamline what can be easily achieved?

In conclusion, if you find those conventions not adhering to your point of view, Lotus is also a DIY framework. You can always pick the separate components and make them to work together. Don't get me wrong, I don't want to be rude here, just communicating the power of having reusable gems. 😉

from hanami.

janko avatar janko commented on May 21, 2024

Yes, that is a race condition, but it's an obvious one, because you're configuring a Lotus::Controller, which you include in both apps, so of course it will be the same one :). I think every user will know that they have to create JsonApi::Controller and Web::Controller, and configure those ones if they don't want the configuration to be shared.

About convenience, doesn't it feel wrong (tedious) for you to have to add each configuration from each framework to Lotus::Application? I think it will often be out of sync (for example, currently #modules and #format are missing in Lotus::Configuration), so users will "have to" configure individual frameworks for those things. I hope you see here that my wish is to make Lotus easier to develop, and I think it doesn't trade users' convenience at all.

I just wanted to show you the advantages of my point of view about this problem, because I'm so sure it's the right way, but I can stop now :). Thank you for listening me out; you're right, maybe I can just use the frameworks separately.

from hanami.

jodosha avatar jodosha commented on May 21, 2024

Yes, that is a race condition, but it's an obvious one

How to solve this problem then? 😉

I think it will often be out of sync (for example, currently #modules and #format are missing in Lotus::Configuration)

Good catch, it's because the configuration API for the single frameworks is still under high development. So I'm slowly porting them.

I have another idea in mind to avoid the porting at all:

module Bookshelf
  class Application < ::Lotus::Application
    configure do
      controller.default_format :json
    end
  end
end

That syntax will talk to Bookshelf::Controller.configuration.default_format directly. No sync between frameworks and Lotus and it clarifies which framework we are configuring. Not sure if this will land in the framework at all.

from hanami.

jeremyf avatar jeremyf commented on May 21, 2024

From the outside looking in (on this conversation) @jodosha is extending an olive branch to those requesting a less verbose means of configuring Lotus. Its a higher level of abstraction/obfuscation; but is something Rails "hostages" may be needing.

And @janko-m you are advocating for a more explicit and verbose process for configuration. Perhaps you might be willing to craft a guide/application that uses your preferred process. This is something that would fit great in Lotus::Docs.

I would love to see guides/examples of both.

from hanami.

janko avatar janko commented on May 21, 2024

How to solve this problem then? 😉

I'm not sure I understand what you mean, but isn't this the solution?

module JsonApi
  Controller = Lotus::Controller.duplicate(self) do
    default_format :json
  end
end
module Web
  Controller = Lotus::Controller.duplicate(self) do
    default_format :html
  end
end

I have another idea in mind to avoid the porting at all:

This solution looks much better 👍

@jeremyf Good advice, I will definitely submit an example application, because I came to this by working on a real application.

from hanami.

jodosha avatar jodosha commented on May 21, 2024

@janko-m But that creates the JsonApi::Controller and Web::Controller duplication as well. I assumed that you didn't liked it.

from hanami.

janko avatar janko commented on May 21, 2024

No, I do like it :), I just don't think that Lotus::Application should do it by itself, because I found it suprising when I first found out about it (I first created MyApp::Controller, and then I was getting warnings of already initialized constant, because Lotus::Application also did it).

from hanami.

twe4ked avatar twe4ked commented on May 21, 2024

This thread is gold and should be linked somewhere :)

from hanami.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.