Upgrade to Pro — share decks privately, control downloads, hide ads and more …

IODA Architecture for loosely-coupled programs

IODA Architecture for loosely-coupled programs

No matter how hard we try, over time our programs seem to drown in a tangle of dependencies. Services have shifting responsibilities, new responsibilities keep appearing and the dependency graph keeps growing in complexity. Services end up depending on other services which themselves depend on others, growing in an endless dependency chain which is very hard to break. Changing an interface impacts dependent services throughout the codebase and even with good coverage it can be a scary operation. We rely on advanced injection and mocking frameworks to survive in this growing jungle and still we are struggling. IODA architecture breaks with the traditional view of dependencies in software. It proposes separating logic, integration and data. In IODA architecture style, data flows between functional units which are mutually oblivious of each other. Integration units assemble functional bricks into modules with progressively higher levels of responsibility. Dependencies between functional modules are expressed in terms of data exchanges and no longer in terms of behavior (services calling others). This approach relies on the best of the functional and object worlds: higher-order functions raise the level of abstraction of the program's logic, and reactive flows describe data circulating in the system. Classes are used as integration containers for functional units and to define module boundaries. Even in existing programs and architectures, these techniques can help us from day one in reducing duplication and coupling. Unit testing also becomes easier, with targeted functional tests that resist change.

Jonas Chapuis

November 10, 2015
Tweet

Other Decks in Programming

Transcript

  1. “I thought of objects being like biological cells and/or individual

    computers on a network, only able to communicate with messages” “OOP to me means only messaging, local retention and protection and hiding of state-process” Alan Kay
  2. class A { private B b; public void Do() {

    var x = ...; b.Do(x); } } class B { public void Do(int x) { ... } }
  3. class A { private B b; public void Do() {

    var x = ...; b.Do(x); } } class B { public void Do(int x) { ... } }
  4. class Validator<T> { ... public void Validate(T data) { var

    error = string.Empty; if (DoValidate(data, out error)) { repository.Save(data); } else { reporter.ReportError(error); } } ... }
  5. class Validator<T> { ... public void Validate(T data) { var

    error = string.Empty; if (DoValidate(data, out error)) { repository.Save(data); } else { reporter.ReportError(error); } } ... }
  6. class Processor<T> { readonly IValidator<T> validator; readonly IRepository<T> repository; readonly

    IErrorReporter reporter; public Processor(IValidator<T> validator, IRepository<T> repository, IErrorReporter reporter) { this.validator = validator; this.repository = repository; this.reporter = reporter; } public void Process(T data) { string error = string.Empty; if (validator.Validate(data, out error)) { repository.Save(data); } else { reporter.ReportError(error); } } }
  7. class Processor<T> { readonly IValidator<T> validator; readonly IRepository<T> repository; readonly

    IErrorReporter reporter; public Processor(IValidator<T> validator, IRepository<T> repository, IErrorReporter reporter) { this.validator = validator; this.repository = repository; this.reporter = reporter; } public void Process(T data) { string error = string.Empty; if (validator.Validate(data, out error)) { repository.Save(data); } else { reporter.ReportError(error); } } }
  8. How about a higher-order function? void Validate(T data, Action<T> onValid,

    Action<string> onInvalid) { if (...) { onValid(data); } else { onInvalid("..."); } }
  9. class Processor<T> { readonly IValidator<T> validator; readonly IRepository<T> repository; readonly

    IErrorReporter reporter; Processor(IValidator<T> validator, IRepository<T> repository, IErrorReporter reporter) { this.validator = validator; this.repository = repository; this.reporter = reporter; } void Process(T data) { validator.Validate(data, onValid: repository.Save, onInvalid: reporter.ReportError); } }
  10. class Processor<T> { readonly IValidator<T> validator; readonly IRepository<T> repository; readonly

    IErrorReporter reporter; Processor(IValidator<T> validator, IRepository<T> repository, IErrorReporter reporter) { this.validator = validator; this.repository = repository; this.reporter = reporter; } void Process(T data) { validator.Validate(data, onValid: repository.Save, onInvalid: reporter.ReportError); } }
  11. Alternative: events class Validator<T> { public void Validate(T data) {

    if (...) { OnValid(data); } else { OnInvalid("..."); } } public event Action<T> OnValid; public event Action<string> OnInvalid; } Alternative: observables IObservable<T> Validate<T>(T data) { ... } Alternative: actors (Akka.Net) class ValidatorActor : ReceiveActor { ... }
  12. IODA: a new perspective › Integration • Declarative • Assembles

    operations and lower-level integration blocks › Operations • Imperative, work logic • Does not know the outside › Data • Payload, what the operations exchange and work with • No logic (except to describe structure) Processor<T> IValidator<T>, IRepository<T>, IErrorReporter <T>
  13. Traditional architectures › All about structuring behavior • Separation of

    concerns • Independence of platform, DB, UI, framework, etc. • Oriented coupling between services › IODA is about structuring code • Separate integration, operation and data description code • Notion of scale (hierarchy of subsystems) • Decoupling (operations wired up by integration) • Emphasis on data flows
  14. Code public static void CheckForResponse( this string message, string responseKeyword,

    Action<Response> handleResponse, Action<string> handlerOther, Action<string, XmlException> handleException) { message.CheckForXml( handleXml: xmlReader => xmlReader.CheckForXmlResponse( message, responseKeyword, handleResponse, handlerOther, handleException), handleXmlException: handleException); }
  15. Benefits › Architecture • Forces us to define how data

    flows in the system • Forces us to think in terms of blocks and reusable functional units
  16. Benefits › Resilience to change • Impact of logic changes

    are contained to the vicinity of the functional unit • Changes to integration levels are mostly just about rewiring • Introducing asynchronicity or parallelization is a “configuration” change
  17. Benefits › Readability • Separate logic makes for another semantic

    layer, with domain- specific names • Details (e.g. logging calls) are abstracted away by the higher-order functions • Can take advantage of extensions methods to enrich syntax
  18. Benefits › Reuse (higher abstraction) • Logic sits on its

    own defined with its own data types (without dependencies) • I can reuse the logic without dragging dependencies • Like small generic pluggable lego bricks of code I can assemble in various ways!
  19. Benefits › Focused testing • Unit tests for logic •

    Integration tests for the whole system • Less reliance on mocks
  20. Downsides › Flow is harder to follow at runtime (no

    debugger support) › Counter-intuitive at first › Coding feels slow • But pays off quickly with testing code and reuse
  21. Reference › Ralf Westphal is a freelance consultant, project coach,

    and trainer on software architectural topics and team organization. He is the co-founder of the "Clean Code Developer" initiative (www.clean-code-developer.de) to increase software quality. • http://geekswithblogs.net/theArchitectsNapkin/archive/2015/04/29 /the-ioda-architecture.aspx • http://geekswithblogs.net/theArchitectsNapkin/archive/2015/05/12 /actors-in-a-ioda-architecture-by-example.aspx • https://leanpub.com/messaging_as_a_programming_model