summaryrefslogtreecommitdiff
path: root/src/_posts/2020-11-16-component-oriented-programming.md
diff options
context:
space:
mode:
Diffstat (limited to 'src/_posts/2020-11-16-component-oriented-programming.md')
-rw-r--r--src/_posts/2020-11-16-component-oriented-programming.md352
1 files changed, 352 insertions, 0 deletions
diff --git a/src/_posts/2020-11-16-component-oriented-programming.md b/src/_posts/2020-11-16-component-oriented-programming.md
new file mode 100644
index 0000000..3400090
--- /dev/null
+++ b/src/_posts/2020-11-16-component-oriented-programming.md
@@ -0,0 +1,352 @@
+---
+title: >-
+ Component-Oriented Programming
+description: >-
+ A concise description of.
+---
+
+[A previous post in this
+blog](/2019/08/02/program-structure-and-composability.html) focused on a
+framework developed to make designing component-based programs easier. In
+retrospect, the proposed pattern/framework was over-engineered. This post
+attempts to present the same ideas in a more distilled form, as a simple
+programming pattern and without the unnecessary framework.
+
+## Components
+
+Many languages, libraries, and patterns make use of a concept called a
+"component," but in each case the meaning of "component" might be slightly
+different. Therefore, to begin talking about components, it is necessary to first
+describe what is meant by "component" in this post.
+
+For the purposes of this post, the properties of components include the
+following.
+
+ 1... **Abstract**: A component is an interface consisting of one or more
+methods.
+
+   1a... A function might be considered a single-method component
+_if_ the language supports first-class functions.
+
+   1b... A component, being an interface, may have one or more
+implementations. Generally, there will be a primary implementation, which is
+used during a program's runtime, and secondary "mock" implementations, which are
+only used when testing other components.
+
+ 2... **Instantiatable**: An instance of a component, given some set of
+parameters, can be instantiated as a standalone entity. More than one of the
+same component can be instantiated, as needed.
+
+ 3... **Composable**: A component may be used as a parameter of another
+component's instantiation. This would make it a child component of the one being
+instantiated (the parent).
+
+ 4... **Pure**: A component may not use mutable global variables (i.e.,
+singletons) or impure global functions (e.g., system calls). It may only use
+constants and variables/components given to it during instantiation.
+
+ 5... **Ephemeral**: A component may have a specific method used to clean
+up all resources that it's holding (e.g., network connections, file handles,
+language-specific lightweight threads, etc.).
+
+   5a... This cleanup method should _not_ clean up any child
+components given as instantiation parameters.
+
+   5b... This cleanup method should not return until the
+component's cleanup is complete.
+
+   5c... A component should not be cleaned up until all its
+parent components are cleaned up.
+
+Components are composed together to create component-oriented programs. This is
+done by passing components as parameters to other components during
+instantiation. The `main` procedure of the program is responsible for
+instantiating and composing the components of the program.
+
+## Example
+
+It's easier to show than to tell. This section posits a simple program and then
+describes how it would be implemented in a component-oriented way. The program
+chooses a random number and exposes an HTTP interface that allows users to try
+and guess that number. The following are requirements of the program:
+
+* A guess consists of a name that identifies the user performing the guess and
+ the number that is being guessed;
+
+* A score is kept for each user who has performed a guess;
+
+* Upon an incorrect guess, the user should be informed of whether they guessed
+ too high or too low, and 1 point should be deducted from their score;
+
+* Upon a correct guess, the program should pick a new random number against
+ which to check subsequent guesses, and 1000 points should be added to the
+ user's score;
+
+* The HTTP interface should have two endpoints: one for users to submit guesses,
+ and another that lists out user scores from highest to lowest;
+
+* Scores should be saved to disk so they survive program restarts.
+
+It seems clear that there will be two major areas of functionality for our
+program: score-keeping and user interaction via HTTP. Each of these can be
+encapsulated into components called `scoreboard` and `httpHandlers`,
+respectively.
+
+`scoreboard` will need to interact with a filesystem component to save/restore
+scores (because it can't use system calls directly; see property 4). It would be
+wasteful for `scoreboard` to save the scores to disk on every score update, so
+instead it will do so every 5 seconds. A time component will be required to
+support this.
+
+`httpHandlers` will be choosing the random number which is being guessed, and
+will therefore need a component that produces random numbers. `httpHandlers`
+will also be recording score changes to `scoreboard`, so it will need access to
+`scoreboard`.
+
+The example implementation will be written in go, which makes differentiating
+HTTP handler functionality from the actual HTTP server quite easy; thus, there
+will be an `httpServer` component that uses `httpHandlers`.
+
+Finally, a `logger` component will be used in various places to log useful
+information during runtime.
+
+[The example implementation can be found
+here.](/assets/component-oriented-design/v1/main.html) While most of it can be
+skimmed, it is recommended to at least read through the `main` function to see
+how components are composed together. Note that `main` is where all components
+are instantiated, and that all components' take in their child components as
+part of their instantiation.
+
+## DAG
+
+One way to look at a component-oriented program is as a directed acyclic graph
+(DAG), where each node in the graph represents a component, and each edge
+indicates that one component depends upon another component for instantiation.
+For the previous program, it's quite easy to construct such a DAG just by
+looking at `main`, as in the following:
+
+```
+net.Listener rand.Rand os.File
+ ^ ^ ^
+ | | |
+ httpServer --> httpHandlers --> scoreboard --> time.Ticker
+ | | |
+ +---------------+---------------+--> log.Logger
+```
+
+Note that all the leaves of the DAG (i.e., nodes with no children) describe the
+points where the program meets the operating system via system calls. The leaves
+are, in essence, the program's interface with the outside world.
+
+While it's not necessary to actually draw out the DAG for every program one
+writes, it can be helpful to at least think about the program's structure in
+these terms.
+
+## Benefits
+
+Looking at the previous example implementation, one would be forgiven for having
+the immediate reaction of "This seems like a lot of extra work for little gain.
+Why can't I just make the system calls where I need to, and not bother with
+wrapping them in interfaces and all these other rules?"
+
+The following sections will answer that concern by showing the benefits gained
+by following a component-oriented pattern.
+
+### Testing
+
+Testing is important, that much is being assumed.
+
+A distinction to be made with testing is between unit and non-unit tests. Unit
+tests are those for which there are no requirements for the environment outside
+the test, such as the existence of global variables, running databases,
+filesystems, or network services. Unit tests do not interact with the world
+outside the testing procedure, but instead use mocks in place of the
+functionality that would be expected by that world.
+
+Unit tests are important because they are faster to run and more consistent than
+non-unit tests. Unit tests also force the programmer to consider different
+possible states of a component's dependencies during the mocking process.
+
+Unit tests are often not employed by programmers, because they are difficult to
+implement for code that does not expose any way to swap out dependencies for
+mocks of those dependencies. The primary culprit of this difficulty is the
+direct usage of singletons and impure global functions. For component-oriented
+programs, all components inherently allow for the swapping out of any
+dependencies via their instantiation parameters, so there's no extra effort
+needed to support unit tests.
+
+[Tests for the example implementation can be found
+here.](/assets/component-oriented-design/v1/main_test.html) Note that all
+dependencies of each component being tested are mocked/stubbed next to them.
+
+### Configuration
+
+Practically all programs require some level of runtime configuration. This may
+take the form of command-line arguments, environment variables, configuration
+files, etc.
+
+For a component-oriented program, all components are instantiated in the same
+place, `main`, so it's very easy to expose any arbitrary parameter to the user
+via configuration. For any component that is affected by a configurable
+parameter, that component merely needs to take an instantiation parameter for
+that configurable parameter; `main` can connect the two together. This accounts
+for the unit testing of a component with different configurations, while still
+allowing for the configuration of any arbitrary internal functionality.
+
+For more complex configuration systems, it is also possible to implement a
+`configuration` component that wraps whatever configuration-related
+functionality is needed, which other components use as a sub-component. The
+effect is the same.
+
+To demonstrate how configuration works in a component-oriented program, the
+example program's requirements will be augmented to include the following:
+
+* The point change values for both correct and incorrect guesses (currently
+ hardcoded at 1000 and 1, respectively) should be configurable on the
+ command-line;
+
+* The save file's path, HTTP listen address, and save interval should all be
+ configurable on the command-line.
+
+[The new implementation, with newly configurable parameters, can be found
+here.](/assets/component-oriented-design/v2/main.html) Most of the program has
+remained the same, and all unit tests from before remain valid. The primary
+difference is that `scoreboard` takes in two new parameters for the point change
+values, and configuration is set up inside `main` using the `flags` package.
+
+### Setup/Runtime/Cleanup
+
+A program can be split into three stages: setup, runtime, and cleanup. Setup is
+the stage during which the internal state is assembled to make runtime possible.
+Runtime is the stage during which a program's actual function is being
+performed. Cleanup is the stage during which the runtime stops and internal
+state is disassembled.
+
+A graceful (i.e., reliably correct) setup is quite natural to accomplish for
+most. On the other hand, a graceful cleanup is, unfortunately, not a programmer's
+first concern (if it is a concern at all).
+
+When building reliable and correct programs, a graceful cleanup is as important
+as a graceful setup and runtime. A program is still running while it is being
+cleaned up, and it's possibly still acting on the outside world. Shouldn't
+it behave correctly during that time?
+
+Achieving a graceful setup and cleanup with components is quite simple.
+
+During setup, a single-threaded procedure (`main`) first constructs the leaf
+components, then the components that take those leaves as parameters, then the
+components that take _those_ as parameters, and so on, until the component DAG
+is fully constructed.
+
+At this point, the program's runtime has begun.
+
+Once the runtime is over, signified by a process signal or some other mechanism,
+it's only necessary to call each component's cleanup method (if any; see
+property 5) in the reverse of the order in which the components were
+instantiated. This order is inherently deterministic, as the components were
+instantiated by a single-threaded procedure.
+
+Inherent to this pattern is the fact that each component will certainly be
+cleaned up before any of its child components, as its child components must have
+been instantiated first, and a component will not clean up child components
+given as parameters (properties 5a and 5c). Therefore, the pattern avoids
+use-after-cleanup situations.
+
+To demonstrate a graceful cleanup in a component-oriented program, the example
+program's requirements will be augmented to include the following:
+
+* The program will terminate itself upon an interrupt signal;
+
+* During termination (cleanup), the program will save the latest set of scores
+ to disk one final time.
+
+[The new implementation that accounts for these new requirements can be found
+here.](/assets/component-oriented-design/v3/main.html) For this example, go's
+`defer` feature could have been used instead, which would have been even
+cleaner, but was omitted for the sake of those using other languages.
+
+
+## Conclusion
+
+The component pattern helps make programs more reliable with only a small amount
+of extra effort incurred. In fact, most of the pattern has to do with
+establishing sensible abstractions around global functionality and remembering
+certain idioms for how those abstractions should be composed together, something
+most of us already do to some extent anyway.
+
+While beneficial in many ways, component-oriented programming is merely a tool
+that can be applied in many cases. It is certain that there are cases where it
+is not the right tool for the job, so apply it deliberately and intelligently.
+
+## Criticisms/Questions
+
+In lieu of a FAQ, I will attempt to premeditate questions and criticisms of the
+component-oriented programming pattern laid out in this post.
+
+**This seems like a lot of extra work.**
+
+Building reliable programs is a lot of work, just as building a
+reliable _anything_ is a lot of work. Many of us work in an industry that likes
+to balance reliability (sometimes referred to by the more specious "quality")
+with malleability and deliverability, which naturally leads to skepticism of any
+suggestions requiring more time spent on reliability. This is not necessarily a
+bad thing, it's just how the industry functions.
+
+All that said, a pattern need not be followed perfectly to be worthwhile, and
+the amount of extra work incurred by it can be decided based on practical
+considerations. I merely maintain that code which is (mostly) component-oriented
+is easier to maintain in the long run, even if it might be harder to get off the
+ground initially.
+
+**My language makes this difficult.**
+
+I don't know of any language which makes this pattern particularly easier than
+others, so, unfortunately, we're all in the same boat to some extent (though I
+recognize that some languages, or their ecosystems, make it more difficult than
+others). It seems to me that this pattern shouldn't be unbearably difficult for
+anyone to implement in any language either, however, as the only language
+feature required is abstract typing.
+
+It would be nice to one day see a language that explicitly supports this
+pattern by baking the component properties in as compiler-checked rules.
+
+**My `main` is too big**
+
+There's no law saying all component construction needs to happen in `main`,
+that's just the most sensible place for it. If there are large sections of your
+program that are independent of each other, then they could each have their own
+construction functions that `main` then calls.
+
+Other questions that are worth asking include: Can my program be split up
+into multiple programs? Can the responsibilities of any of my components be
+refactored to reduce the overall complexity of the component DAG? Can the
+instantiation of any components be moved within their parent's
+instantiation function?
+
+(This last suggestion may seem to be disallowed, but is fine as long as the
+parent's instantiation function remains pure.)
+
+**Won't this will result in over-abstraction?**
+
+Abstraction is a necessary tool in a programmer's toolkit, there is simply no
+way around it. The only questions are "how much?" and "where?"
+
+The use of this pattern does not affect how those questions are answered, in my
+opinion, but instead aims to more clearly delineate the relationships and
+interactions between the different abstracted types once they've been
+established using other methods. Over-abstraction is possible and avoidable
+regardless of which language, pattern, or framework is being used.
+
+**Does CoP conflict with object-oriented or functional programming?**
+
+I don't think so. OoP languages will have abstract types as part of their core
+feature-set; most difficulties are going to be with deliberately _not_ using
+other features of an OoP language, and with imported libraries in the language
+perhaps making life inconvenient by not following CoP (specifically regarding
+cleanup and the use of singletons).
+
+For functional programming, it may well be that, depending on the language, CoP
+is technically being used, as functional languages are already generally
+antagonistic toward globals and impure functions, which is most of the battle.
+If anything, the transition from functional to component-oriented programming
+will generally be an organizational task.