StrangeIOC官方案例

思考并回答以下问题:

  • Mediators speak to the rest of the app。怎么理解?

StangeIOC

Strange attractors create predictable patterns, often in chaotic systems.

Strange is a super-lightweight and highly extensible Inversion-of-Control (IoC) framework, written specifically for C# and Unity. We’ve validated Strange on web, standalone, and iOS, and Android.

It contains the following features, most of which are optional:

  • A core binding framework that pretty much lets you bind one or more of anything to one or more of anything else.
  • Dependency Injection
    • Map as singleton, value or factory (get a new instance each time you need one)
    • Name injections and/or supply specific implementations to specific consumer classes
    • Perform constructor or setter injection
    • Tag your preferred constructor
    • Tag a method to fire after construction
    • Inject into MonoBehaviours
    • Bind polymorphically (bind any or all of your interfaces to a single concrete class)
  • Reflection binding dramatically reduces overhead of employing reflectivity
  • Two shared event bus systems, EventDispatcher and Signals. Both allow you to:
    • Dispatch events to any point in your application
    • Map local events for local communication
    • Map events to Commands classes to separate business logic
    • EventDispatcher transmits data payloads as primitives or ValueObjects
    • Signals transmits data in bindable, type-safe parameters
  • MonoBehaviour mediation
    • Facilitate separation of a view from the application using it
    • Keep Unity-specific code isolated from the rest of the app
  • Optional MVCS (Model/View/Controller/Service) structure
  • Multiple contexts
    • Allow subcomponents (separate Scenes) to function on their own, or in the context of larger apps.
    • Allow communication between contexts.
  • Promises
    • Similar to Javascript Q-Promises, these help control flow and error handling
    • Promises also fit some common signal use cases much more cleanly!
  • Annotated ‘Implicit’ Bindings
  • Reduce boiler plate code written in your Context and Mediators!
  • JSON-driven bindings
  • Dynamically load your bindings at runtime!
  • Don’t see what you need? The core binding framework is simple to extend. Build new Binders like:
    • A different type of dispatcher, like AS3-Signals <- WAIT A MOMENT! WE DID EXACTLY THAT!!!
    • An entity framework
    • A multi-loader

In addition to organizing your project into a sensible structure, Strange offers the following benefits:

  • Designed to play well with Unity3D. Also designed to play well without it.
  • Separate UnityEngine code from the rest of your app.
    • Improves portability
    • Improves unit testability
  • A common event bus makes information flow easy and highly decoupled. (Note: Unity’s SendMessage method does this, of course, but it’s dangerous as all get-out. I may write a whole article on just this topic at some point.)
  • The extensible binder really is amazing (a friend used to tell me “it’s good to like your own cookin’”). The number of things you can accomplish with the tiny core framework would justify Strange all on its own.
  • Multiple contexts allow you to “bootstrap” subcomponents so they operate fine either on their own or as an integrated part. This can hugely speed up your development process and allow developers to work in isolation, then integrate in later stages of development.
  • Get rid of platform-specific #IF…#ELSE constructs in your code. IoC allows you to write whole concrete implementations correctly, then bind the ones you want to use at compile time or at run time. (As with other forms of binding, #IF…#ELSE clauses can be isolated into a single class and away from the rest of your code.)

最简单的案例

The MyFirstProject example is a great place to start learning.

Inside is everything you need to create a very simple strange app which demonstrates the Root (MyFirstProjectRoot, start here), the Context (ExampleContext), which is where your bindings go,
a Command (StartCommand), a View (ExampleView) and a Mediator (ExampleMediator).

文件目录

初始化与客户端

MyFirstProjectRoot.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/// The Root is the entry point to a strange-enabled Unity3D app.
/// ===============
///
/// Attach this MonoBehaviour to a GameObject at the top of your Main scene.
///
/// It is considered a best practice to create ONE GameObject at the top of your
/// app and attach everything to it. (Note that you can create multiple Roots which
/// will result in multiple Contexts. This can be desirable, but it's an advanced use case.
/// Recommend you stick to a single Context until you're confident you know what you're doing.
///
/// The GameObject to which this MonoBehaviour is attached to called the 'ContextView'
/// and is injectable anywhere in the application. This is especially
/// useful in commands, where you can access the ContextView to attach further GameObjects
/// or MonoBehaviours.

using System;
using UnityEngine;
using strange.extensions.context.impl;

namespace strange.examples.myfirstproject
{
public class MyFirstProjectRoot : ContextView
{

void Awake()
{
//Instantiate the context, passing it this instance.
context = new MyFirstContext(this);

//This is the most basic of startup choices, and probably the most common.
//You can also opt to pass in ContextStartFlag options, such as:
//
//context = new MyFirstContext(this, ContextStartupFlags.MANUAL_MAPPING);
//context = new MyFirstContext(this, ContextStartupFlags.MANUAL_MAPPING | ContextStartupFlags.MANUAL_LAUNCH);
//
//These flags allow you, when necessary, to interrupt the startup sequence.
}
}
}

MyFirstContext.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/// The Context is where all the magic really happens.
/// ===========
/// Other than copying the constructors, all you really need to do when you create
/// your context is override Context or one of its subclasses, then set up
/// your list of mappings.
///
/// In an MVCSContext, like the one we're using, there are three types of
/// available mappings:
/// 1. Dependency Injection - Bind your dependencies to injectionBinder.
/// 2. View/Mediator Binding - Bind MonoBehaviours on your GameObjects to Mediators that speak to the rest of the app
/// 3. Event Binding - Bind Events to any/all of the following:
/// - Event/Method Binding - Firing the event will trigger the method(s).
/// - Event/Command Binding - Firing the event will instantiate the Command(s) and run its Execute() method.
/// - Event/Sequence Binding - Firing the event will instantiate a Command(s), run its Execute() method, and,
/// unless the sequence is interrupted, fire each subsequent Command until the
/// sequence is complete.

using System;
using UnityEngine;
using strange.extensions.context.api;
using strange.extensions.context.impl;
using strange.extensions.dispatcher.eventdispatcher.api;
using strange.extensions.dispatcher.eventdispatcher.impl;

namespace strange.examples.myfirstproject
{
public class MyFirstContext : MVCSContext
{
public MyFirstContext (MonoBehaviour view) : base(view)
{
}

public MyFirstContext (MonoBehaviour view, ContextStartupFlags flags) : base(view, flags)
{
}

protected override void mapBindings()
{
//Injection binding.
//Map a mock model and a mock service, both as Singletons
injectionBinder.Bind<IExampleModel>().To<ExampleModel>().ToSingleton();
injectionBinder.Bind<IExampleService>().To<ExampleService>().ToSingleton();

//View/Mediator binding
//This Binding instantiates a new ExampleMediator whenever as ExampleView
//Fires its Awake method. The Mediator communicates to/from the View
//and to/from the App. This keeps dependencies between the view and the app
//separated.
mediationBinder.Bind<ExampleView>().To<ExampleMediator>();

//Event/Command binding
commandBinder.Bind(ExampleEvent.REQUEST_WEB_SERVICE).To<CallWebServiceCommand>();
//The START event is fired as soon as mappings are complete.
//Note how we've bound it "Once". This means that the mapping goes away as soon as the command fires.
commandBinder.Bind(ContextEvent.START).To<StartCommand>().Once();

}
}
}

View

ExampleView.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/// An example view
/// ==========================
/// The view is where you program and configure the particulars of an item
/// in a scene. For example, if you have a GameObject with buttons and a
/// test readout, wire all that into this class.
///
/// By default, Views do not have access to the common Event bus. While you
/// could inject it, we STRONGLY recommend against doing this. Views are by
/// nature volatile, possibly the piece of your app most likely to change.
/// Mediation mapping allows you to automatically attach a 'Mediator' class
/// whose responsibility it is to connect the View to the rest of the app.
///
/// Building a view in code here. Ordinarily, you'd do this in the scene.
/// You could argue that this code is kind of messy...not ideal for a demo...
/// but that's kind of the point. View code is often highly volatile and
/// reactive. It gets messy. Let your view be what it needs to be while
/// insulating the rest of your app from this chaos.

using System;
using System.Collections;
using UnityEngine;
using strange.extensions.dispatcher.eventdispatcher.api;
using strange.extensions.mediation.impl;

namespace strange.examples.myfirstproject
{
public class ExampleView : View
{
internal const string CLICK_EVENT = "CLICK_EVENT";

[Inject]
public IEventDispatcher dispatcher{get;set;}

private float theta = 20f;
private Vector3 basePosition;

//Publicly settable from Unity3D
public float edx_WobbleSize = 1f;
public float edx_WobbleDampen = .9f;
public float edx_WobbleMin = .001f;

internal void init()
{
GameObject go = Instantiate(Resources.Load("Textfield")) as GameObject;

TextMesh textMesh = go.GetComponent<TextMesh>();
textMesh.text = "http://www.thirdmotion.com";
textMesh.font.material.color = Color.red;

Vector3 localPosition = go.transform.localPosition;
localPosition.x -= go.GetComponent<Renderer>().bounds.extents.x;
localPosition.y += go.GetComponent<Renderer>().bounds.extents.y;
go.transform.localPosition = localPosition;

Vector3 extents = Vector3.zero;
extents.x = go.GetComponent<Renderer>().bounds.size.x;
extents.y = go.GetComponent<Renderer>().bounds.size.y;
extents.z = go.GetComponent<Renderer>().bounds.size.z;
(go.GetComponent<Collider>() as BoxCollider).size = extents;
(go.GetComponent<Collider>() as BoxCollider).center = -localPosition;

go.transform.parent = gameObject.transform;

go.AddComponent<ClickDetector>();
ClickDetector clicker = go.GetComponent<ClickDetector>() as ClickDetector;
clicker.dispatcher.AddListener(ClickDetector.CLICK, onClick);
}

internal void updateScore(string score)
{
GameObject go = Instantiate(Resources.Load("Textfield")) as GameObject;
TextMesh textMesh = go.GetComponent<TextMesh>();
textMesh.font.material.color = Color.white;
go.transform.parent = transform;

textMesh.text = score.ToString();
}

void Update()
{
transform.Rotate(Vector3.up * Time.deltaTime * theta, Space.Self);
}

void onClick()
{
dispatcher.Dispatch(CLICK_EVENT);
startWobble();
}

private void startWobble()
{
StartCoroutine(wobble (edx_WobbleSize));
basePosition = Vector3.zero;
}

private IEnumerator wobble(float size)
{
while(size > edx_WobbleMin)
{
size *= edx_WobbleDampen;
Vector3 newPosition = basePosition;
newPosition.x += UnityEngine.Random.Range(-size, size);
newPosition.y += UnityEngine.Random.Range(-size, size);
newPosition.z += UnityEngine.Random.Range(-size, size);
gameObject.transform.localPosition = newPosition;
yield return null;
}
gameObject.transform.localPosition = basePosition;
}
}
}

ClickDetector.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// Just a simple MonoBehaviour Click Detector

using System;
using UnityEngine;
using strange.extensions.mediation.impl;

namespace strange.examples.myfirstproject
{
public class ClickDetector : EventView
{
public const string CLICK = "CLICK";

void OnMouseDown()
{
dispatcher.Dispatch(CLICK);
}
}
}

ExampleMediator.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/// Example mediator
/// =====================
/// Make your Mediator as thin as possible. Its function is to mediate
/// between view and app. Don't load it up with behavior that belongs in
/// the View (listening to/controlling interface), Commands (business logic),
/// Models (maintaining state) or Services (reaching out for data).

using System;
using UnityEngine;
using strange.extensions.dispatcher.eventdispatcher.api;
using strange.extensions.mediation.impl;

namespace strange.examples.myfirstproject
{
public class ExampleMediator : EventMediator
{
//This is how your Mediator knows about your View.
[Inject]
public ExampleView view{ get; set;}

public override void OnRegister()
{

//Listen to the view for an event
view.dispatcher.AddListener(ExampleView.CLICK_EVENT, onViewClicked);

//Listen to the global event bus for events
dispatcher.AddListener(ExampleEvent.SCORE_CHANGE, onScoreChange);

view.init ();
}

public override void OnRemove()
{
//Clean up listeners when the view is about to be destroyed
view.dispatcher.RemoveListener(ExampleView.CLICK_EVENT, onViewClicked);
dispatcher.RemoveListener(ExampleEvent.SCORE_CHANGE, onScoreChange);
Debug.Log("Mediator OnRemove");
}

private void onViewClicked()
{
Debug.Log("View click detected");
dispatcher.Dispatch(ExampleEvent.REQUEST_WEB_SERVICE);
}

private void onScoreChange(IEvent evt)
{
//float score = (float)evt.data;
string score = (string)evt.data;
view.updateScore(score);
}
}
}

Model

IExampleModel.cs

1
2
3
4
5
6
7
8
9
using System;

namespace strange.examples.myfirstproject
{
public interface IExampleModel
{
string data{get;set;}
}
}

ExampleModel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// Example Model
/// ======================
/// Nothing to see here. Just your typical place to store some data.

using System;

namespace strange.examples.myfirstproject
{
public class ExampleModel : IExampleModel
{
public string data {get;set;}

public ExampleModel ()
{
}
}
}

Service

IExampleService.cs

1
2
3
4
5
6
7
8
9
10
11
using System;
using strange.extensions.dispatcher.eventdispatcher.api;

namespace strange.examples.myfirstproject
{
public interface IExampleService
{
void Request(string url);
IEventDispatcher dispatcher{get;set;}
}
}

ExampleService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// Example Service
/// ======================
/// Nothing to see here. Just your typical place to store some data.

using System;
using System.Collections;
using UnityEngine;
using strange.extensions.context.api;
using strange.extensions.dispatcher.eventdispatcher.api;

namespace strange.examples.myfirstproject
{
public class ExampleService : IExampleService
{
[Inject(ContextKeys.CONTEXT_VIEW)]
public GameObject contextView{get;set;}

[Inject]
public IEventDispatcher dispatcher{get;set;}

private string url;

public ExampleService ()
{
}

public void Request(string url)
{
this.url = url;

//For now, we'll spoof a web service by running a coroutine for 1 second...
MonoBehaviour root = contextView.GetComponent<MyFirstProjectRoot>();
root.StartCoroutine(waitASecond());
}

private IEnumerator waitASecond()
{
yield return new WaitForSeconds(1f);

//...then pass back some fake data
dispatcher.Dispatch(ExampleEvent.FULFILL_SERVICE_REQUEST, url);
}
}
}

Controller

StartCommand.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/// An example Command
/// ============================
/// This Command puts a new ExampleView into the scene.
/// Note how the ContextView (i.e., the GameObject our Root was attached to)
/// is injected for use.
///
/// All Commands must override the Execute method. The Command is automatically
/// cleaned up when Execute has completed, unless Retain is called (more on that
/// in the OpenWebPageCommand).

using System;
using UnityEngine;
using strange.extensions.context.api;
using strange.extensions.command.impl;
using strange.extensions.dispatcher.eventdispatcher.impl;

namespace strange.examples.myfirstproject
{
public class StartCommand : EventCommand
{

[Inject(ContextKeys.CONTEXT_VIEW)]
public GameObject contextView{get;set;}

public override void Execute()
{
GameObject go = new GameObject();
go.name = "ExampleView";
go.AddComponent<ExampleView>();
go.transform.parent = contextView.transform;
}
}
}

ExampleEvent.cs

1
2
3
4
5
6
7
8
9
10
11
using System;

namespace strange.examples.myfirstproject
{
public class ExampleEvent
{
public const string SCORE_CHANGE = "SCORE_CHANGE";
public const string REQUEST_WEB_SERVICE = "REQUEST_WEB_SERVICE";
public const string FULFILL_SERVICE_REQUEST = "FULFILL_SERVICE_REQUEST";
}
}

CallWebServiceCommand.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/// An Asynchronous Command
/// ============================
/// This demonstrates how to use a Command to perform an asynchronous action;
/// for example, if you need to call a web service. The two most important lines
/// are the Retain() and Release() calls.

using System;
using System.Collections;
using UnityEngine;
using strange.extensions.context.api;
using strange.extensions.command.impl;
using strange.extensions.dispatcher.eventdispatcher.api;

namespace strange.examples.myfirstproject
{
public class CallWebServiceCommand : EventCommand
{

[Inject]
public IExampleModel model{get;set;}

[Inject]
public IExampleService service{get;set;}

static int counter = 0;

public CallWebServiceCommand()
{
++counter; //This counter is here to demonstrate that a new Command is created each time.
}

public override void Execute()
{
//Retain marks the Command as requiring time to execute.
//If you call Retain, you MUST have corresponding Release()
//calls, or you will get memory leaks.
Retain ();

//Call the service. Listen for a response
service.dispatcher.AddListener(ExampleEvent.FULFILL_SERVICE_REQUEST, onComplete);
service.Request("http://www.thirdmotion.com/ ::: " + counter.ToString());
}

//The payload is in the form of a IEvent
private void onComplete(IEvent evt)
{
//Remember to clean up. Remove the listener.
service.dispatcher.RemoveListener(ExampleEvent.FULFILL_SERVICE_REQUEST, onComplete);

model.data = evt.data as string;
dispatcher.Dispatch(ExampleEvent.SCORE_CHANGE, evt.data);

//Remember to call release when done.
Release ();
}
}
}
0%