changes from proofread of CoP post

This commit is contained in:
Brian Picciano 2020-11-29 12:59:31 -07:00
parent d40fe10213
commit daae1b44bf

View File

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