Wednesday, March 14, 2018

Conversational Bot - Architecture Decisions

The architecture of the Bot framework will be a combination of principles from Domain-Driven Design (DDD) and Onion Architecture (aka Hexagonal or Ports and Adapters architecture). Check the references section at the bottom for some enlightening links.
We will avoid using the traditional 3-layer architecture (eg. MVC) because although it provides Separation of Concerns (via modularity), it leads to tight coupling among the different layers. It's a database-centric architecture and is not suitable for our purpose.
The Onion Architecture is recommended by most experts for enterprise applications because it has a focus on loose-coupling and Separation of Concerns. It relies heavily on the Dependency Inversion Principle which gives us a pluggable system.
Although we will not follow DDD precisely (because of the size of our application), we will take the principles and use them as guidelines.

Class design guidelines

Dependency Injection
We will define two types of objects for dependency injection: Injectables and Newables
Injectables: objects that we must inject in constructors (eg. Parser, Authenticator, CreditCardProcessor, MusicPlayer, MailSender, OfflineQueue, AudioDevice, ... basically any kind of service)
Newables: objects that we can simply instantiate anywhere in code (eg. User, Account, Email, MailMessage, CreditCard, Song, Color, Temperature, Point, Money)
  • Rule1: Newables must not ask for injectables in their constructors. Newables must not hold injectables as field references. They can only accept injectables via method calls.
  • Rule2: Injectables must not ask for newables in their constructors. Injectables must not hold newables as their state (object attributes). They can only accept newables via method calls.
Notice that Injectables are objects that usually have an interface and a few implementations (eg. 1-5). Newables are objects that can have many instances (thousands or millions). That's why we cannot reasonably inject a newable into a constructor. Injectables, on the other hand, can be reasonably injected as a default service into a constructor.
If the two rules above are violated then it will quickly lead to code that is hard to unit test and results in passing of needless references around, violating the Law of Demeter.
Objects can be further categorized as services, entities or value objects.
  • Services: they have interfaces and are stateless (eg. Parser, Authenticator)
  • Entities: they have id, state and are mutable (eg. User, Song)
  • Value Objects: they don't have id, have state and are immutable (eg, Color, Point)
Services are injectables. Entities and Value Objects are newables.

Onion Architecture
Domain model (Core): The core of the application is an independent object model. It should only contain entites, value objects and interfaces. It has no dependencies.
Outer layers can depend on the inner layers, but no inner layer should have a dependency on the outer layers. Domain model is the innermost layer.
The Service layer contains domain services and application services.
Infrastructure: This layer contains classes that talk to the DB, FileSystem, web services, etc.
Each layer defines its API via interfaces. Outer layers implement those interfaces and talk to the inner layers via those interfaces.
Our Domain is "a conversational agent within HCM". Our domain is not HCM directly, the mobile API is taking care of that domain logic. Our domain logic is concerned with context management, recommendation, response construction, notification and service integration (eg calendar, traffic, etc), all within the scope of HCM concepts such as shift trading, vacation, approvals, etc.

Domain Driven Design

  • Don't inject repository into an aggregate. (violates SRP)
  • Don't inject domain services into an aggregate. 
  • Inject repositories and domain services into application services.
  • An aggregate should reference another aggregate by a globally unique id, not by object reference. (improves aggregate persistence scalability)
  • Aggregates should mostly be one root entity and some value objects.
  • Aggregates should contain only entities that need to be consistent with each other.

General

Always return new objects or immutable objects from methods. Try not to return entity objects from methods.
Separate methods into command and query (CQS).

Definitions

Entity: An object that models a domain concept (eg. User). It has an identity, state and behaviour.
Value Object: An object that models a domain concept (eg. Money). It has no identity, only value and may have behaviour.
Aggregate: A cluster of entity objects and value objects in DDD. It's a unit of composition that has transactional consistency. 
Aggregate root: The main entity in an Aggregate that talks to the outside world.
Domain Service: A service that coordinates aggregate roots to achieve some functionality.


References:
To New or not to New (Misko Hevery: agile coach at Google, creator of AngularJS)
Design for Testability and DDD (Misko Hevery: agile coach at Google, creator of AngularJS)
Implementing Domain-Driven Design: Aggregates (Vaughn Vernon: software craftsman, writer)
Test Induced Design Damage (Vladimir Khorikov: Pluralsight course author)
Organizing an ASP.Net MVC application with Onion Architecture (Steve Smith: channel9 speaker; software craftsman and trainer)

No comments:

Post a Comment