Application Architecture¶
This chapter describes how to structure applications built with DSM and Viper. The architecture separates concerns into layers, enabling code reuse across platforms and languages.
The 3-Tier Architecture¶
Applications built with Viper follow a 3-tier pattern:
┌────────────────────────────────────────────────────────────┐
│ TIER 1: PRESENTATION (Platform-Specific UI) │
│ AppKit (macOS) / Qt (cross-platform) / PySide (Python) │
└────────────────────────────┬───────────────────────────────┘
│
┌────────────────────────────▼───────────────────────────────┐
│ TIER 2: LOGIC (Context + Bridges + Components) │
│ Application Context (orchestration), generated pools, │
│ hand-written business code │
└────────────────────────────┬───────────────────────────────┘
│
┌────────────────────────────▼───────────────────────────────┐
│ TIER 3: DATA (Viper Runtime) │
│ CommitDatabase, CommitStore, persistence, dsviper binding │
└────────────────────────────────────────────────────────────┘
Tier Responsibilities¶
Tier |
Responsibility |
Platform-Specific? |
|---|---|---|
Presentation |
UI framework, windows, dialogs |
Yes |
Logic |
Application Context, bridges, business components |
Partially |
Data |
Viper runtime (CommitDatabase, CommitStore), persistence |
No (shared) |
The Full Application Stack¶
For business applications with domain logic, the stack expands:
┌────────────────────────────────────────────────────────────┐
│ 1. UI Layer │
│ Platform-specific windows, views, controllers │
└────────────────────────────┬───────────────────────────────┘
│
┌────────────────────────────▼───────────────────────────────┐
│ 2. Application Context │
│ Singleton orchestrator: owns the CommitStore, the │
│ generated FunctionPools, and the application's domain │
│ state (e.g. current GraphKey) │
└────────────────────────────┬───────────────────────────────┘
│
┌────────────────────────────▼───────────────────────────────┐
│ 3. Bridge Layer │
│ Pool signatures → Business logic connection │
└────────────────────────────┬───────────────────────────────┘
│
┌────────────────────────────▼───────────────────────────────┐
│ 4. Business Logic Layer │
│ Hand-written domain code (Model_*) │
└────────────────────────────┬───────────────────────────────┘
│
┌────────────────────────────▼───────────────────────────────┐
│ 5. Generated Infrastructure │
│ Kibo output: Data, Commit, Serialization │
└────────────────────────────┬───────────────────────────────┘
│
┌────────────────────────────▼───────────────────────────────┐
│ 6. Viper Runtime │
│ Core C++ engine (CommitStore, CommitDatabase), │
│ dsviper Python binding │
└────────────────────────────────────────────────────────────┘
Layer Details¶
Layer 1: UI¶
Platform-specific presentation code:
Window management
Menu and toolbar actions
Dialog boxes
Framework-native widgets
This layer changes for each target platform but follows the same logical structure.
Layer 2: Application Context¶
The Context is the application-level singleton that orchestrates the runtime.
It is not a subclass of Viper::CommitStore: it owns a CommitStore
(provided by the Viper runtime) and adds the application-specific surface around
it — the generated FunctionPool / AttachmentFunctionPool instances, the
domain state (e.g. the current graph), and the action dispatch.
Responsibilities of the Context:
Own the
CommitStore(which itself holds theCommitDatabaseand the currentCommitState)Hold the generated tool / model / attachment
FunctionPoolsHold application-level domain state (e.g.
graphKey)Open / close / load the database
Dispatch user actions through
CommitMutableStateExpose framework-agnostic notifications via
CommitStoreNotifying
// Context pattern (C++) — see com.digitalsubstrate.ge / GE_Context.hpp
namespace GE {
class Context final {
public:
static std::shared_ptr<Context> Instance();
// Database lifecycle (owned via the store)
void use(std::shared_ptr<Viper::CommitDatabase> const & database);
void close();
void load();
static std::shared_ptr<Viper::CommitDatabase> createDatabase(
std::filesystem::path const & filePath);
// Domain operations
void newGraph();
void useNewGraph(std::string const & label);
void dispatchAction(std::string const & label,
std::shared_ptr<ContextAction> const & action);
// Owned components
std::shared_ptr<Viper::CommitStore> store;
std::shared_ptr<Viper::FunctionPool> tools;
std::shared_ptr<Viper::AttachmentFunctionPool> modelGraph;
std::shared_ptr<Viper::AttachmentFunctionPool> modelSelection;
std::shared_ptr<Viper::AttachmentFunctionPool> modelIntegrity;
std::shared_ptr<Viper::AttachmentFunctionPool> attachments;
// Domain state
GE::Graph::GraphKey graphKey;
};
} // namespace GE
Action dispatch always goes through the store’s CommitMutableState, so undo/redo
and notifications stay coherent:
void Context::dispatchAction(std::string const & label,
std::shared_ptr<ContextAction> const & action) {
auto const ms{store->mutableState()};
action->run(ms);
store->commitMutations(label, ms);
}
The previous “Store-as-application-singleton” pattern is superseded: the
generic store concerns (database, state, undo/redo, notifications) live in
Viper::CommitStore, and the application-specific concerns (which pools,
which domain state, which actions) live in the application’s Context.
Layer 3: Bridges¶
Bridges connect generated pool signatures to your business logic:
// GE_AttachmentFunctionPoolBridges.hpp (GENERATED)
namespace GE::AttachmentFunctionPoolBridges {
namespace ModelGraph {
Graph::VertexKey new_vertex(std::shared_ptr<Viper::AttachmentMutating> const & attachmentMutating,
Graph::GraphKey const & graphKey,
std::int64_t value,
Graph::Position const & position);
}
}
// GE_AttachmentFunctionPoolBridges.cpp (HAND-WRITTEN)
Graph::VertexKey new_vertex(std::shared_ptr<Viper::AttachmentMutating> const & attachmentMutating,
Graph::GraphKey const & graphKey,
std::int64_t value,
Graph::Position const & position) {
return Model::Vertex::add(attachmentMutating, graphKey, value, position,
Model::Random::makeColor());
}
Layer 4: Business Logic¶
Your domain-specific code:
// GE_Model_Vertex.cpp
VertexKey add(std::shared_ptr<AttachmentMutating> const & attachmentMutating,
GraphKey const & graphKey,
std::int64_t const & value,
Position const & position,
Color const & color) {
auto const vertexKey{create(attachmentMutating, value, position, color)};
Attachments::Graph_Topology::unionVertexKeys(attachmentMutating, graphKey, {vertexKey});
return vertexKey;
}
Layer 5: Generated Infrastructure¶
Kibo generates this layer from your DSM:
Data types (
*_Data.cpp)Commit accessors (
*_Commit.cpp)Serialization (
*_Reader.cpp,*_Writer.cpp)Database persistence (
*_Database.cpp)Python proxy
Layer 6: Viper Runtime¶
The core C++ engine providing:
Type and value system
CommitDatabase persistence
Stateful commits
Python binding (dsviper)
The Notifier Pattern¶
UI frameworks have different notification mechanisms. The Notifier pattern bridges this gap:
CommitStore (C++ core)
│
▼
CommitStoreNotifying (interface)
│
▼
DSCommitStoreNotifier (framework adapter)
│
▼
Framework-specific notifications
Common Notifications¶
All implementations expose the same signals:
Signal |
Purpose |
|---|---|
|
Database opened successfully |
|
Database closed |
|
State changed (undo/redo available) |
|
Schema updated |
|
Error occurred |
Multi-Language Support¶
The architecture supports multiple languages:
Layer |
C++ Application |
Python Application |
|---|---|---|
UI |
AppKit/Qt C++ |
PySide/Qt Python |
Context |
C++ singleton (owns CommitStore) |
Python class (owns store) |
Business |
C++ |
Python or dsviper |
Infrastructure |
C++ generated |
Python generated |
Runtime |
Viper C++ (CommitStore, …) |
dsviper |
Python Integration¶
The dsviper module exposes the entire Viper runtime, including
CommitStore and CommitDatabase. A Python application’s Context follows
the same pattern as the C++ one — it instantiates a CommitStore and wires
the generated pools around it:
import dsviper
database = dsviper.CommitDatabase.create("path/to/data.cdb",
"An Application Database")
store = dsviper.CommitStore.make()
store.set_database(database)
# Undo/Redo operations
if store.can_undo():
store.undo()
Design Principles¶
Separation of Concerns¶
Each layer has a single responsibility:
UI: Only presentation, no business logic
Context: Orchestration only — owns the
CommitStoreand the pools, does not reimplement themBridges: Connection, not logic
Business: Domain rules, not infrastructure
Generated: Infrastructure, not business
Platform Isolation¶
Platform-specific code lives only in Layer 1. The other layers are shared or generated.
Generated vs Hand-Written¶
Generated (Kibo) |
Hand-Written |
|---|---|
Data types |
Business logic |
Serialization |
Bridges |
Persistence |
Store orchestration |
Pool marshalling |
UI layer |
Minimal Porting Effort¶
To port to a new platform:
Implement the Notifier adapter (~50 lines)
Create UI components matching existing patterns
Wire up the main window
The business logic and infrastructure remain unchanged.
Summary¶
Principle |
Implementation |
|---|---|
Separation |
6 distinct layers with clear responsibilities |
Reuse |
Layers 3-6 shared across platforms |
Generation |
Kibo generates infrastructure (Layer 5) |
Multi-language |
Same architecture for C++ and Python |
Platform isolation |
Only Layer 1 is platform-specific |
This architecture enables building complex applications that work across platforms while minimizing duplicated code.
What’s Next¶
Python Guide - Use the dsviper Python API
Toolchain - Master the development tools