Structure Requirements in Functions
Did you ever read a requirements specification with dozens if not hundreds of requirements that come in form of an excel sheet or word document? It takes a huge effort and a long time to understand the intent of the author. The result is most of the time endless discussions that reduce too short development time even further. The other alternative is that the implementation is not what was expected.
How can we do better?
When I think about what a product or component shall do, I usually start with an empty rectangle and write its name at the top. Even if it sounds simple, drawing a rectangle immediately lets me think about what is in and what is out of scope. For the time being, let's add a user outside of the component to illustrate someone that is going to use the component.
Then, I start to add functions to note down my initial ideas about what the component shall do.
As an example, let's use the "Capture Requirements" function for the Requirements Management Tool.
How does the user interact with the function? Thinking in an abstract way. The user must use an interface (input) to invoke the function and receives feedback by checking the output.
This can be illustrated by adding interfaces to the function. The user invokes an interface to execute the function and receives feedback about the result.
The beauty of this approach is that user interaction can be defined in the same way as interaction of software components without any user interaction.
Without a clearly defined scope it is hard to come up with meaningful requirements. But if we define functions first, we can concentrate on a specific aspect and can define requirements to detail what we want to achieve with this functionality:
What information must be provided?
What feedback do we want to provide to the user?
What steps do we expect to happen when the function "Capture Requirements" is executed? This can be validation of input data, checking user permission, checking other data, or setting default values in case something is unspecified. What to do in case errors occur?
Most people find it hard to define non-functional requirements. Non-functional requirements define characteristics and design constraints for a function. Characteristics can be performance or security aspects. Design constraints provide rules on how the function must be implemented, e.g. UI design guidelines, using certain design patterns, available services, etc.
With a clear definition of a function scope, it becomes much easier to define it:
What is the maximum response time?
How does the input interface to the user look like? (can be a UI mockup or an API definition)
How to present error conditions to the user?
Are there any restrictions on how to store a captured requirement?
Does a user need to have a specific role to execute the function?
...
Let's dig into the example to make the idea clear. For the function "Capture Requirements", we want the user to be able to:
input the text of the requirement itself
optionally add a description
optionally add a priority
type (functional, characteristic, design constraint)
Again, we use ChatGPT to generate a well-formatted list of requirements that describe the input:
Capture Requirements shall accept a string input for the text of the requirement.
Capture Requirements shall accept a string input for the type of the requirement, with the options of "functional," "characteristic," or "design constraint."
Capture Requirements shall accept an optional string input for the description of the requirement, with a default value of an empty string.
Capture Requirements shall accept an optional string input for the priority of the requirement, with the options of "low," "medium," or "high," and a default value of "medium."
The function shall return an indication if the capturing was executed without an error or provide error details. Again, let ChatGPT do the job:
Capture Requirements shall return a status indicating whether the requirement was successfully captured.
If the requirement was successfully captured, the return value shall be "Success."
If the requirement was not successfully captured, the return value shall be "Error," and details of the error shall be provided.
Finally, let's detail what the function shall do:
validate the input
if optional parameters are not provided, it shall use an empty description, a medium priority
check if the user has the right to add requirements
create a unique identifier for the requirement
add the time stamp when the requirement was created
add the handle of the user who created it
persistently store the requirement
Capture Requirements shall validate the input to ensure that the required information is provided and that it meets any specified formatting requirements.
Capture Requirements shall check if the user has the right to add requirements.
Capture Requirements shall create a unique identifier for the requirement.
Capture Requirements shall add a time stamp when the requirement was created.
Capture Requirements shall add the handle of the user who created the requirement.
Capture Requirements shall persistently store the requirement.
Capture Requirements shall return a status indicating whether the requirement was successfully captured.
With the definition of functions, a description of their inputs, outputs, and the expectation of what the function shall do, we can create meaningful and well-structured requirements.
Since we have nicely specified the function, let's check if ChatGPT can be utilized to implement the function.
I asked it to write a python fastapi version of the "Capture Requirements" function that stores the requirements in an SQLite database:
import sqlite3
from fastapi import FastAPI, HTTPException
app = FastAPI()
# Requirement model
class Requirement:
def __init__(self, text: str, description: str = "", priority: str = "medium", req_type: str = "functional"):
self.text = text
self.description = description
self.priority = priority
self.req_type = req_type
self.id = uuid.uuid4().hex
self.timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
self.created_by = "user_handle"
# Connect to database
conn = sqlite3.connect("requirements.db")
cursor = conn.cursor()
# Create requirements table if it doesn't exist
cursor.execute(
'''CREATE TABLE IF NOT EXISTS requirements (id text, text text, description text, priority text, req_type text, timestamp text, created_by text)'''
)
conn.commit()
@app.post("/requirement/")
async def capture_requirement(requirement: Requirement):
# Validate input
if not requirement.text:
raise HTTPException(status_code=400, detail="Requirement text is required")
if requirement.priority not in ["low", "medium", "high"]:
raise HTTPException(status_code=400, detail="Invalid priority value")
if requirement.req_type not in ["functional", "characteristic", "design constraint"]:
raise HTTPException(status_code=400, detail="Invalid requirement type")
# Check if user has permission to add requirement
if not user_has_permission():
raise HTTPException(status_code=403, detail="Permission denied")
# Add requirement to database
cursor.execute(
'''INSERT INTO requirements (id, text, description, priority, req_type, timestamp, created_by) VALUES (?,?,?,?,?,?,?)''',
(requirement.id, requirement.text, requirement.description, requirement.priority, requirement.req_type, requirement.timestamp, requirement.created_by)
)
conn.commit()
return {"status": "Success", "requirement_id": requirement.id}
Note that this example assumes that the
uuid
anddatetime
modules have been imported, and that a functionuser_has_permission()
has been defined to check if the current user has the right to add requirements.
It is of course not production-ready code, but it basically does what we specified in the function.
To sum up, defining functions with their corresponding input and outputs, additional characteristics and design constraints helps to define the functionality of a product in an implementation-independent way. The specification remains valid, no matter what technology is used to implement it.