(the article below is pasted from http://johnstamkos.com/2019/10/19/definitions/ and it explores the idea of representing natural language descriptions with code)
In this article we will explore the idea of defining functional requirements in a code readable format. With the term functional requirements we are referring to the description of a feature that we want to implement in our application in a natural language. As code readable we are refering to text that code can parse and implement functionality.
A feature is generally a functionality that our application does. For example in a CRM system, a feature is the presentation of a list of customers under a project.
Normally features are described in natural language with issues in an issue tracker. Different issue trackers have different implementations but in essence an issue has a description of a functionality many times accompanied with pictures, relations to other issues, statuses, e.t.c. Basically a feature is described with words.
Because features are described with words there is room for human error. This can be mitigated by better descriptions and communication of course but again this process cannot be automated, a human needs to act when feature descriptions change.
Code on the other, like math, is strictly defined. There is no way that code execution can differ having the same environment.
Having these concepts in mind below there will be explained a pattern for defining features in code and give examples. Code samples are in PHP but the same concepts can be applied to other OOP languages.
Definitions. Models of implementation
The main concept that is introduced is a Definition. A Definition is a model that describes how you implement a feature or how it is structured. Definitions have the following requirements:
- Are immutable
- Can represent structure and/or process of feature
- Can be referenced
- Can reference other ontologies
In order to realize those requirements we implement Definitions as Classes in the OOP way with the following characteristics:
- Are namespaced. So they can be referenced easily
- Are final Classes. can reference other ontologies
- Have only properties.
- All properties are constants
Let's have an example Definition of something that is not related to programming:
<?php
namespace Village\People;
final class John
{
const morning_routine = [
'wake up',
'walk to',
'kitchen'
];
const night_routine = [
'brush teeth',
'shower',
'go to bed'
];
}
The structure above is a valid PHP class. Having the class final and it's properties constant there is only one way that this class can be executed. The execution can encapsulate the description of a person's routine. Roughly it can be translated as:
Being a villager, John's morning routine is to wake up and walk to the kitchen. His night routine is to brush his teeth, shower and go to bed.
The key point is that because class is final and its properties constants, the order of the array's elements matter, it describes the steps of a process in the order that are executed.
So having the whole Class immutable to any change we can represent natural language descriptions to data structures in code. Let's see some use cases.
Use cases.
To help ourselves more let's enrich the vocabulary of our definitions in order to make them more compact. So let's define:
-
@
as a symbol that references other ontologies -
{{
,}}
as symbols that encapsulate class properties that are referenced (e.x.{{another_property}}
). -
:
to illustrate the execution of a function of an ontology/class. We can indicate dependencies with{{another_property}}
in function parameters. No actual function parameters are depicted.So
*:someFunction()
has no dependencies,*:someFunction({{another_property}})
hasanother_property
as a dependency T::
to indicate a type. e.x.T::string()
is string type
Use case 1. Defining a file's structure
Nowdays configuration files that setup environments and handle deployments have increased in number and complexity. Software developers may need to make changes to those systems as part of the functionality that is being developed. How would you restrict or validate the contents of a configuration file? With Definitions we can define the structure that a configuration file should implement.
For example. Let's say that we have the following Kubernetes yaml file for Ingress
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: simple-fanout-example
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /foo
backend:
serviceName: service1
servicePort: 8080
- path: /bar
backend:
serviceName: service2
servicePort: 8080
We would like a developer to be only allowed to change "paths" and "name" but nothing else. Another restriction is that he can only use "service1" and "service2".
the Definition for this could be:
final class Kubernetes
{
const ingress = [
'apiVersion' => 'networking.k8s.io/v1beta1',
'kind' => 'Ingress',
'metadata' => [
'name' => 'T::string()',
'annotations' => [
'nginx.ingress.kubernetes.io/rewrite-target' => '/'
]
],
'spec' => [
'rules' => [
[
'host' => 'foo.bar.com',
'http' => [
'paths' => [
[
'path' => 'T::string()',
'backend' => [
'serviceName' => '{{service_names}}'
'servicePort' => 8080
]
]
]
]
]
]
]
];
const service_names = [
'service1', 'service2'
];
}
By doing that we avoid the creation of another tool or microservice that developers need to have in order to enter that information and it is extendable.
In the CI/CD process a step can be added that checks that Kubernetes Ingress yaml follows the Definition. If not the build stops.
Use case 2. Defining architecture
Let's say that we want to build a CMS following the MVC pattern. Now the MVC pattern is a description in a natural language on how to implement functionality. As described above Definitions can represent natural language descriptions as data structures.
In our example we have two namespaces, Definition
which has the Definitions and Base
which has the actual implementations.
So let's have the following definition of our app's architecture:
<?php
namespace Definition;
final class App
{
const MVC = [
'{{router}}' => [
'{{request}}' => [
'{{http}}',
'{{filter}}',
'{{format}}',
],
'{{analyze}}',
'{{route}}' => [
'{{controller}}' => [
'{{model}}',
'{{view}}',
'{{response}}'
]
]
],
];
const request = '@\Base\Request';
const router = '@\Base\Router';
const controller = '@\Definition\Controller';
const model = '@\Definition\Model';
const view = '@\Definition\View';
const response = '@\Base\Response';
const http = '@\Base\HTTP:get()';
const filter = '@\Base\Request:filter({{http}})';
const format = '@\Base\Request:format({{filter}})';
const analyze = '@\Base\Router:analyze({{request}})';
const route = '@\Base\Router:route()';
}
Taking into consideration that the order of elements in the arrays does matter as we have described previously the above definition could be interpreted as:
Our app being MVC starts from the router then formats the HTTP request analyzes it and routes it to a controller which eventually responds back to the client.
The
request
gets the raw HTTP request, then filters and formats it.A
controller
gets themodel
, makes aview
and responds back.. . .
From the Definition above we can see:
- the workflow of our application
- which implementations corresponds to which step of the process
- which steps have multiple implementations (
controller
,model
,view
reference theDefinition
namespace) - which functions are called and what dependencies (as in Inversion of Control) they have
So with this Definition we can describe our MVC's functionality and at the same time give many implementation details.
Like in our previous use case we can create a checker in order to validate that the implementation follows the Definition.
Download
In order to experiment with Definitions you can check the repo here
or
use composer install j0hnys/definitions
Applications
The main goal of Definitions is to define the way something is implemented ("model of implementation"). We have seen that they can be useful to represent natural language descriptions to data structures. More importantly they can be used to describe functionality that is implemented (features).
We can think a software application as a set of features that "solve" the Domain problem. Basically the way software automates an industry is by implementing a set of features that all together solve the main problem.
So having a way to programmatically define how something is implemented gives us a foundation that we can build more automations on top.
In our next article we will describe a way to automate software development having Definitions as a base of describing the main architecture and details of the process
Top comments (0)