Basic Concepts

This section will introduce you to feature-based development and build up high-level feature-u concepts.

feature-u

Feature Based Development

At a 30,000 ft view, feature-based development (as in most software) is all about dissecting hard problems into smaller pieces. Even when I started my career (back in the 70's), this was a prominent quote:

    "All problems in computer science can be solved by another level of indirection." David Wheeler

By breaking up your application into features, each feature can focus on a more specific and isolated set of tasks. In some ways you can think of a feature as a "mini application"!

There are many design considerations in defining your feature boundaries. You can find several articles on this topic that provide insight on feature-based design.

For the most part, these considerations are part of the design of each individual project. While feature-u does not dictate overall design considerations, it does however facilitate good feature-based principles (such as encapsulation). This will be the focus of this section.

Segregating Features

If you are like me, when you think about feature-based development, the first thing that comes to mind is to isolate your code into feature directories.

Project Organization - with types or features

In doing this your code is organized by what it accomplishes (i.e. features), rather than what it is (i.e. components, routes, logic, actions, reducers, selectors, etc.).

By segregating your features into individual directories, there is a semblance of isolation.

Feature Goals

Our goal is to encapsulate each feature in such a way as to make them truly plug-and-play. But how is this accomplished?

The directory structure is just a start. There are several hurdles that must be overcome to realize our goal ...

  • How do we encapsulate and isolate our features, while still allowing them to collaborate with one another?

  • How can selected features introduce start-up initialization (even injecting utility at the root DOM), without relying on some external startup process?

  • How can feature-based UI Composition be accomplished in an isolated and autonomous way?

  • How do we configure our chosen frameworks now that our code is so spread out?

  • How do we enable/disable selected features which are either optional, or require a license upgrade?

In short, how do we achieve a running application from these isolated features?

When you boil it all down, there are two overriding characteristics that must be accomplished to achieve our goals:

  1. Feature Runtime Consolidation: pulling our features back together into one running application

  2. Feature Collaboration: provide a mechanism by which our features can interact with one another

As it turns out, everything else is a byproduct of these two artifacts. Let's take a closer look at each of these items.

Feature Runtime Consolidation

Now that we have isolated our features into separate entities, how do we bring them back together so they run as one application? We must be able to pull and configure various aspects of our individual features, and "launch" them as a single homogeneous running application.

Feature Consolidation

This concern can be further divided into two sub-concerns:

  • App Initialization

    Some features may require certain startup initialization. As an example, a feature that encapsulates some DB abstraction will rely on a run-time setup of a DB service.

    Certainly we don't want to rely on some global app logic to accomplish this (once again, we want our features to be encapsulated and self-sufficient).

  • Framework Configuration

    If your application relies on other frameworks, chances are there are resources contained within each feature that must be accumulated and fed into the framework configuration process.

    How is this accomplished?

Feature Collaboration

The second characteristic (mentioned above) is Feature Collaboration - providing a mechanism by which our features can interact with one another.

A best practice of feature-based development (to the extent possible) is to treat each feature as an isolated implementation. Most aspects of a feature are internal to that feature's implementation (for example, actions are typically created and consumed exclusively by logic/reducers/components that are internal to that feature).

From this perspective, you can think of each feature as it's own isolated mini application.

With that said however, we know that "no man is an island"! Any given feature ultimately exists as part of a larger application. There are cases where a feature needs to promote a limited subset of it's aspects to other features. For example, a feature may need to:

  • be knowledgeable of some external state (via a selector)
  • emit or monitor actions of other features
  • consolidate component resources from other features - as in UI Composition
  • invoke the API of other features
  • etc. etc. etc.

These items form the basis of why Cross Feature Communication and Feature Based UI Composition are needed.

Feature Collaboration

To complicate matters, as a general rule, JS imports should NOT cross feature boundaries. The reason being that this cross-communication should be limited to public access points - helping to facilitate true plug-and-play.

Cross Feature Imports are BAD

Given all this then, how is Cross Feature Communication achieved in a way that doesn't break encapsulation?

Features need a way to promote their Public Interface to other features, and consume other feature's Public Assets.

The feature-u Solution

Let's take a look at the solution feature-u provides for all of these goals. The following sections will build feature-u concepts incrementally.

launchApp()

launchApp() is an essential utility in feature-u. It is an agent, working on your behalf, which provides the foundation that accomplishes all the goals of feature-u! It facilitates both Feature Runtime Consolidation and Feature Collaboration.

With this utility, your mainline startup process is extremely simple ... it merely invokes launchApp(), and you are done!

launchApp()

The launchApp() function actually starts your application running, employing various hooks that drive BOTH App Initialization and Framework Configuration!

You can find launchApp() examples in the Usage section, and Launching Your Application.

How does this work? What are the bindings to launchApp()? ... let's delve a bit deeper

Feature Object

To accomplish this, each feature promotes a Feature object (using createFeature()), that catalogs aspects of interest to feature-u.

This is the primary input to launchApp().

Feature Object

aspects

In feature-u, "aspect" (little "a") is a generalized term used to refer to the various ingredients that (when combined) constitute your application. Aspects can take on many different forms: UI ComponentsRoutesState Management (actions, reducers, selectors)Business LogicStartup Initialization Codeetc. etc. etc.

Not all aspects are of interest to feature-u ... only those that are needed to setup and launch the application ... all others are considered an internal implementation detail of the feature. As an example, consider the redux state manager: while it uses actions, reducers, and selectors ... only reducers are needed to setup and configure redux.

AspectContent

The Feature object is merely a lightweight container that holds aspects of interest to feature-u. These aspects can either be Built-In aspects (from core feature-u), or Extendable aspects (from plugin extensions).

Running the App

Let's see how launchApp() accommodates the two sub-goals of running the app:

App Initialization

Because launchApp() is in control of starting the app, it can introduce Application Life Cycle Hooks.

This allows each feature to perform app-specific initialization, and even inject components into the root of the app.

There are two hooks:

  1. Feature.appWillStart() - invoked one time at app startup time
  2. Feature.appDidStart() - invoked one time immediately after app has started

App Initialization

Application Life Cycle Hooks greatly simplify your app's mainline startup process, because initialization specific to a given feature can be encapsulated in that feature.

Framework Configuration

A fundamental goal of feature-u is to automatically configure the framework(s) used in your run-time-stack (by accumulating the necessary resources across all your features). This greatly reduces the boilerplate code within your app.

How can this be accomplished when there are so many frameworks out there ... and every project uses a different mix?

feature-u is extendable! It operates in an open plugable architecture where Extendable Aspects integrate feature-u to other frameworks, matching your specific run-time stack. This is good, because not everyone uses the same frameworks!

Extendable Aspects can be found in external NPM packages (the normal case), or you can create your own using createAspect() (a more advanced topic).

Framework Configuration

The Aspect object contains a series of Aspect Life Cycle Hooks that are invoked under the control of feature-u (launchApp()). In general, an Aspect's responsibility is to:

  • accumulate AspectContent across all features
  • perform some desired setup and configuration
  • expose it's functionality in some way (typically a framework integration)

An Aspect automatically extends the Feature object by allowing it's AspectContent to be "cataloged" in the Feature using Aspect.name as it's key. In the diagram above, you can see that

  • the reducerAspect (Aspect.name: 'reducer') permits a Feature.reducer: reducerContent construct

  • and the logicAspect (Aspect.name: 'logic') permits a Feature.logic: logicContent construct

It is important to understand that the interface to your chosen frameworks is not altered in any way. You use them the same way you always have (just within your feature boundary). feature-u merely provides a well defined organizational layer, where the frameworks are automatically setup and configured by accumulating the necessary resources across all your features.

Launching Your Application

In feature-u the application mainline is very simple and generic. There is no real app-specific code in it ... not even any global initialization! That is because each feature can inject their own app-specific constructs!! The mainline merely accumulates the Aspects and Features, and starts the app by invoking launchApp():

src/app.js

import ReactDOM              from 'react-dom';
import {launchApp}           from 'feature-u';
import {createRouteAspect}   from 'feature-router';
import {createReducerAspect} from 'feature-redux';
import {createLogicAspect}   from 'feature-redux-logic';
import features              from './features';

// launch our app, exposing the Fassets object (facilitating cross-feature communication)
export default launchApp({           // *4*

  aspects: [                         // *1*
    createRouteAspect(),   // Feature Routes ... extending: Feature.route
    createReducerAspect(), // redux          ... extending: Feature.reducer
    createLogicAspect(),   // redux-logic    ... extending: Feature.logic
  ],

  features,                          // *2*

  registerRootAppElm(rootAppElm) {   // *3*
    ReactDOM.render(rootAppElm,
                    getElementById('myAppRoot'));
  }
});

Here are some important points of interest (match the numbers to *n* in the code above):

  1. the supplied Aspects (pulled from separate npm packages) reflect the frameworks of our run-time stack (in our example redux, redux-logic, and feature-router) and extend the acceptable Feature properties (Feature.reducer, Feature.logic, and Feature.route respectively) ... see: Extendable aspects

  2. all of our app features are supplied (accumulated from the features/ directory)

  3. a registerRootAppElm() callback is used to catalog the supplied rootAppElm to the specific React platform in use. Because this registration is accomplished by your app-specific code, feature-u can operate in any of the React platforms, such as: react-web, react-native, and expo ... see: React Registration

  4. as a bit of a preview, the return value of launchApp() is a Fassets object, which promotes the accumulated Public Face of all features, and is exported to provide Cross Feature Communication.

Cross Feature Communication

In support of Feature Collaboration that doesn't break encapsulation, feature-u promotes feature-based resources through something called fassets (feature assets). This is how all Cross Feature Communication is accomplished. You can think of this as the Public Face of a feature.

SideBar: The term fassets is a play on words. While it is pronounced "facet" and is loosely related to this term, it is spelled fassets (i.e. feature assets).

A feature can expose whatever it deems necessary through the built-in Feature.fassets aspect). There is no real constraint on this resource. It is truly open.

Cross Feature Communication

The fassets aspect has a define directive where resources are cataloged.

Here is a simple example of how fassets are defined:

export default createFeature({

  name:     'featureA',

  fassets: {
    define: {
     'openView':      actions.view.open,      // openView(viewName): Action
     'currentView':   selector.currentView,   // currentView(appState): viewName
     'isDeviceReady': selector.isDeviceReady, // isDeviceReady(appState): boolean
    },
  },

  ...
});

feature-u accumulates fassets from all active features, and promotes them through the Fassets object (emitted from launchApp()).

SideBar: There are several ways to obtain access the Fassets object (see Obtaining fassets object).

To reference a fassets resource, simply dereference it as any other object reference. There is also a Fassets.get() method that can be supplied Wildcards, returning an array of resources.

if (fassets.isDeviceReady(appState)) {
  ...
}

This is an example of a push philosophy. Here the supplier is is simply publicly promoting a resource for other features to use (take it or leave it). The supplier is merely saying: "this is my Public Face".

You can find more information about this topic in Cross Feature Communication.

Feature Based UI Composition

It is common for a UI component to be an accumulation of sub-components that span several features. As a result, UI Composition is a very important part of Cross Feature Communication.

In support of this, feature-u provides two APIs that give your UI components access to fassets.

  1. useFassets() is a React Hook that provides functional component access to fassets.

    Hooks are an exciting new React feature that allows you to "hook into" React state and lifecycle aspects from functional components.

    Here is how a component would access a company.logo (defined by another feature):

    export default function MyComponent() {
    
      const Logo = useFassets('company.logo');
    
      return (
        <div>
          <Logo/>
        </div>
        ... snip snip
      );
    }
    

    In this example, because the Logo property is a component, MyComponent can simply reference it using JSX.

  2. withFassets() is a Higher-order Component (HoC) that auto-wires fasset properties into a component. This is a common pattern popularized by redux connect() (simplifying component access to application state).

    Here is the same example (from above) using withFassets():

    function MyComponent({Logo}) {
      return (
        <div>
          <Logo/>
        </div>
        ... snip snip
      );
    }
    
    export default withFassets({
      component: MyComponent,
      mapFassetsToProps: {
        Logo: 'company.logo',
      }
    });
    

    The withFassets() HoC auto-wires named feature assets as component properties through the mapFassetsToPropsStruct hook.

You can find more information about this topic in UI Composition.

Resource Contracts

It is common for UI Composition to be represented as a contract, where a component in one feature has a series of injection needs that are to be supplied by other features.

The fassets aspect has additional constructs to facilitate this contractual arrangement, allowing feature-u to provide more validation in the process.

Rather than just defining resources in one feature and using them in another:

  • A given feature can specify a series of injection needs using the fassets.use directive. This identifies a set of injection keys that uniquely identify these resources.

  • Other features will supply this content using the fassets.defineUse directive, by referencing these same injection keys.

This represents more of a pull philosophy. It gives feature-u more knowledge of the process, allowing it to verify that supplied resources are correct.

Wildcards (*) can be used to add additional dynamics to the process, allowing features to inject their content autonomously.

Here is a main feature that is pulling in a series of sub-components (links and bodies) from other features:

  • main feature

    src/features/main/feature.js

    createFeature({
      name: 'main',
    
      fassets: {
        use: [
           'MainPage.*.link',
           'MainPage.*.body',
        ],
      },
      ... snip snip
    });
    

    Because our specification includes wildcards, a series of definitions will match!

    Here is the MainPage component that fulfills the usage contract:

    src/features/main/comp/MainPage.js

    export default function MainPage() {
    
      const fassets = useFassets();
    
      const Logo       = fassets.get('company.logo');    // from our prior example
      const mainLinks  = fassets.get('MainPage.*.link'); // find matching
      const mainBodies = fassets.get('MainPage.*.body');
    
      return (
        <div>
          <div> {/* header section */}
            <Logo/>
          </div>
    
          <div> {/* left-nav section */}
            {mainLinks.map( (MainLink, indx) => <MainLink key={indx}/>)}
          </div>
    
          <div> {/* body section */}
            {mainBodies.map( (MainBody, indx) => <MainBody key={indx}/>)}
          </div>
        </div>
      );
    }
    

When useFassets() (or withFassets()) encounters wildcards (*), it merely accumulates all matching definitions, and promotes them as arrays.

Through this implementation, any feature may dynamically inject itself in the process autonomously! In addition, this dynamic implicitly handles the case where a feature is dynamically disabled (very kool indeed)!!

The following snippets are taken from other features that supply the definitions for the content to inject:

  • cart feature

    src/features/cart/feature.js

    createFeature({
      name: 'cart',
    
      fassets: {
        defineUse: {
         'MainPage.cart.link': () => <Link to="/cart">Cart</Link>,
         'MainPage.cart.body': () => <Route path="/cart" component={ShoppingCart}/>,
        },
      },
      ... snip snip
    });
    
  • search feature

    src/features/search/feature.js

    createFeature({
      name: 'search',
    
      fassets: {
        defineUse: {
         'MainPage.search.link': () => <Link to="/search">Search</Link>,
         'MainPage.search.body': () => <Route path="/search" component={Search}/>,
        },
      },
      ... snip snip
    });
    

Two external features (cart and search) define the content that is requested by the main feature.

The fassets.defineUse directive requires that the resource keys match a fassets.use feature request. This is the contract that provides feature-u insight when enforcing it's validation.

SideBar: Because we are also dealing with navigation, we introduce react-router into the mix (with the Link and Route components). Because of RR's V4 design, our routes are also handled through component composition (see Feature Based Routes for more information).

You can find more information about this topic in UI Composition.

Feature Enablement

Features can be dynamically disabled by setting the Feature.enabled boolean property (part of the Built-In aspects):

export default createFeature({
  name:     'sandbox',
  enabled:  false,
  ... snip snip
});

In this example, it is just as though the sandbox feature doesn't exist. In other words it has been logically removed.

Typically, this indicator is based on some run-time expression, allowing packaged code to be dynamically enabled/disabled during the application's start-up process:

export default createFeature({
  name:     'sandbox',
  enabled:  inDevelopmentMode(),
  ... snip snip
});

This dynamic is useful in a number of different situations. For example:

  • some features may require a license upgrade

  • other features may only be used for diagnostic purposes, and are disabled by default

You can find more information about this topic in Feature Enablement.

In Summary

The following diagram summarizes feature-u's Basic Concepts (as discussed above):

Basic Concepts

results matching ""

    No results matching ""