Tips for Writing Modules for All Frameworks (But Especially for ColdBox)

Tips for Writing Modules for All Frameworks (But Especially for ColdBox)

Cbddee54e0016667b9bcb0fdec4ab21e?s=128

Eric Peterson

April 27, 2018
Tweet

Transcript

  1. Tips for Writing Modules for All Frameworks (But Especially for

    ColdBox)
  2. What this talk isn't: → How to add ColdBox module

    magic to other frameworks. → Anything to do with UI modules. → Suggestions on what modules to write.
  3. What this talk is: → How to build awesome modules

    for the entire CFML community to use. → How to do it without losing all of the special sauce that makes ColdBox awesome.
  4. Who Am I? Eric Peterson ! Utah " Ortus #

    ForgeBox, ColdBox Elixir $ Prolific Module Author % 1 wife, 2 kids, 1 dog
  5. Quick Overview of Modules

  6. Quick Overview of Modules → Reusable packages of functionality. →

    Semantically Versioned → Can depend on other modules → Autoload functionality (in ColdBox)
  7. Examples of modules you don't want to write yourself →

    qb — a CFML query builder → cfcollection — Functional array programming → cbmarkdown — Markdown parsing → str — String utility library
  8. On to cross-platform modules!

  9. TL;DR Good Documentation & ModuleConfig.cfc

  10. Major Platforms → ColdBox → CommandBox → FW/1 → CFWheels

    → No Framework
  11. Coupling your module to ColdBox makes it harder to use

    in other frameworks.
  12. What does coupling to ColdBox look like? component singleton {

    property name="interceptorService" inject="coldbox:interceptorService"; property name="wirebox" inject="wirebox"; function load( mapping ) { interceptorService.processState( "preLoad", { "mapping" = mapping } ); var instance = wirebox.getInstance( mapping ); interceptorService.processState( "postLoad", { "instance" = instance "mapping" = mapping, } ); return instance; } }
  13. With just a few changes, this component can be used

    in any framework (or no framework).
  14. Sneak Peak component { property name="interceptorService"; property name="DIEngine"; function init(

    DIEngine, interceptorService = new NullInterceptorService() ) { variables.DIEngine = arguments.DIEngine; variables.interceptorService = arguments.interceptorService; return this; } function load( mapping ) { interceptorService.processState( "preLoad", { "mapping" = mapping } ); var instance = DIEngine.getInstance( mapping ); interceptorService.processState( "postLoad", { "instance" = instance "mapping" = mapping, } ); return instance; } }
  15. This is the Dependency Injection principal in action.

  16. Important Reminder!

  17. Not coupling to ColdBox does not mean not using CommandBox

  18. Every major CFML framework has endorsed CommandBox and ForgeBox, so

    you can rely on them without reservation.
  19. If you have dependencies, you will need to document how

    they are used. We'll talk about dependency injection later.
  20. But what about...? → WireBox → Interceptors → Testing →

    All the other cool stuff that ColdBox does for me that I don't want to recreate?
  21. WireBox

  22. Persistence WireBox

  23. ¯\_(ϑ)_/¯

  24. Dependency Injection WireBox

  25. For the most part, your annotations will work just fine

    in ColdBox and do nothing in other frameworks. This could be perfectly fine. component { property name="grammar" inject="DefaultGrammar@qb"; function get( options ) { return grammar.runQuery( toSQL() ); } }
  26. But now users of your module are required to use

    a DI engine.
  27. Yes, we know they should be, but how about we

    help bring them in to the future instead of just chiding them?
  28. Instead of property injection, let's use constructor injection... component {

    property name="grammar"; /** * @grammar.inject DefaultGrammar@qb */ function init( grammar ) { variables.grammar = arguments.grammar; } function get( options ) { return grammar.runQuery( toSQL() ); } }
  29. ...or property injection plus explicit setters. // Setters here dynamically

    created by accessors="true" component accessors="true" { property name="grammar" inject="DefaultGrammar@qb"; function get( options ) { return grammar.runQuery( toSQL() ); } }
  30. Make sure to document it! <!-- README.md --> ## Not

    using ColdBox? To create a `QueryBuilder`, you will need to pass in your desired grammar to the constructor. (You can always change this later by calling the `setGrammar` method and passing in a new grammar.)
  31. Everything you can do with annotations, you can do in

    your ModuleConfig.
  32. component { property name="grammar"; /** * @grammar.inject DefaultGrammar@qb */ function

    init( grammar ) { variables.grammar = arguments.grammar; } function get( options ) { return grammar.runQuery( toSQL() ); } }
  33. component { function configure() { binder.map( "QueryBuilder@qb" ) .to( "#moduleMapping#.models.QueryBuilder"

    ) .initArg( name = "defaultGrammar", ref = "DefaultGrammar@qb" ); } } }
  34. Creating New Instances WireBox

  35. Option #1 Use new

  36. component { function onMissingMethod( missingMethodName, missingMethodArguments ) { var req

    = new Hyper.models.HyperRequest() return invoke( req, missingMethodName, missingMethodArguments ); } }
  37. Option #2 Use an interface

  38. interface name="DIEngine" { function getInstance( string name, string dsl, struct

    initArguments ); }
  39. Then, in your code, use DIEngine: component { property name="DIEngine";

    property name="entityMapping" function newEntity() { return DIEngine.getInstance( entityMapping ); } }
  40. And in your ModuleConfig.cfc, set up WireBox as the default

    for ColdBox apps: component { function configure() { binder.map( "MyComponent@MyModule" ) .to( "#moduleMapping#.models.MyComponent" ) .initArg( name = "DIEngine", dsl = "wirebox" ) } }
  41. You don't need to actually type-hint this interface — you

    just have to document it in your README: <!-- README.md --> ## Not using ColdBox? Make sure to bring your own DIEngine. It needs to conform to the following interface: interface name="DIEngine" { function getInstance( string name, string dsl, struct initArguments ); }
  42. Documentation is 90% of the battle to create a good

    cross-framework module.
  43. Other Gotchas → Avoid onDIComplete methods in favor of something

    more explicit. → Remember that going the old-fashioned constructor route means that everything needs to be injected. → On the flip side, remember that if you are expecting or needing DI, you need to use a DI engine.
  44. Interceptors

  45. Option #1 Follow the same pattern with an InterceptorInterface for

    other frameworks.
  46. interface { function processState( name, data ); }

  47. Option #2 Inject the service and check for existence before

    using it.
  48. component accessors="true" { property name="interceptorService"; function run() { if (

    ! isNull( interceptorService ) ) { interceptorService.processState(); } } }
  49. Option #3 Employ the NullObject design pattern

  50. component { property name="interceptorService" function init( interceptorService = new NullInterceptorService()

    ) { variables.interceptorService = arguments.interceptorService; return this; } function run() { interceptorService.processState( "preRun", getData() ); // ... } }
  51. // NullInterceptorService.cfc component { function processState() { return; } }

  52. Testing

  53. Once you've implemented these suggestions, testing is actually pretty easy

    without running ColdBox.
  54. One thing you should not need is heavy use of

    mocks and stubs.
  55. You can always include an integration test with ColdBox as

    well. In fact, this is a good idea to make sure you're ModuleConfig.cfc is configured correctly.
  56. Module Authors, remember... The community is here to help you

  57. Community Members, remember... Most modules are just a few lines

    of config away from being perfect for your project
  58. Case Study: qb

  59. → Started with ColdBox in mind. → Decided to use

    plain old new for the composition → Tony Junkes wrote a blog post about integrating with FW/1
  60. → I knew I was going to add interceptor support

    next. This blog post made me rethink the implementation from being ColdBox only to something more inclusive. → FW/1 integration is part of the qb docs. → Basic ModuleConfig.cfc support is now in FW/1
  61. One day, our modules may ship with configuration support for

    multiple frameworks!
  62. Wrap Up

  63. It's okay if you're not perfect at making your module

    framework-agnostic.
  64. We have the entire community to help us out