Capitulo 5: Afiches Artisticos
5.1 La paleta tipográfica adecuada
layer uses the business logic layer and the business logic layer uses the data access layer. But layering rules (and non-circular-dependencies common sense) detail that the UI layer does not use the data access layer; the business Logic Layer does not use the UI layer; and the data access layer does not use UI layer. But on almost every project I've seen this used, at least one layer pattern violation occurred. Typically, this was due to shared models, where the shared model lived in a data layer and the UI layer ended up needing a reference to the data layer. The models lived in the business logic layer and the data Layer needed a circular dependence, etc.
There's nothing wrong with the layers pattern, just be sure to use it correctly so it doesn't introduce technical debt where it's supposed to solve it.
Mediator
In many domains you can model business logic within individual classes (called domain classes). Each one has a single responsibility. But domain classes are never used in isolation; they are used in a system in groups. You may have a Customer entity that's used along with an Invoice entity to handle invoicing. Neither entity may interact directly with the other, and the behavior being modeled isn't part of the modeled entity. (I typically use the example of an office chair, as it can move, be delivered, etc., but that's not the responsibility of the chair. Something else performs that behavior on the chair. Other entities like Invoice seem to be more commonly thought of having non-real-world behaviors like Print . But, I digress.) An invoice may use a Customer entity and a user may need to print an Invoice, but it's something else that prints the invoice.
138
The mediator is the something else that would print the invoice (maybe an InvoicePrinter class). Its responsibility would be to know about the invoice and maybe the Customer and know what needed to be interacted with to print the invoice. It's different from the façade pattern in that it provides extra functionality.
The mediator pattern is useful for supporting the single responsibility principle and keeping domain logic separate from infrastructure or support/service logic.
Memento
Have you ever wanted to provide the ability to go from one state to a past state? The memento pattern details an abstraction for that. Typically, this is used to perform functionality like undo/redo, but think of anything that needs to go back to a previous state (like rollback). Memento provides the ability to provide a consistent interface regardless of the implementation (undo, rollback, etc.).
Effectively, the class that owns the state that needs to change will provide an ability to save and restore state (this class is considered the originator ), that state will be requested of the originator (the class that asks is considered the caretaker ), and the encapsulation that stores the state is called the memento .
The caretaker requests the current state (or a memento) from the originator and when state needs to return back to that state in the future, the caretaker sends the state back to the originator to return to that state. The originator is responsible for doing everything it needs to do to return to that state. In the simplest case, that would just be setting fields/properties; in a more complex state that could involve a transaction or compensating actions that need to be performed to return to that state.
Listing 6-11 shows a simple originator and memento implementation that provides an undo ability.
Listing 6-11. An Originator and Memento Implementation that Provides an Undo Ability
public class Document {
private List<string> lines = new List<string>(); public class Memento
{
internal Memento(Document document) {
Lines = new List<string>(document.lines); }
public List<string> Lines { get; private set; } }
public void AddLine(string line) {
lines.Add(line); }
public Memento GetMemento() {
return new Memento(this); }
public void Undo(Memento memento) {
lines = new List<string>(memento.Lines); }
139
public IEnumerable<string> GetLines(){
return lines; }
//... }
Listing 6-12 shows sample caretaker code that works with Document and its memento pattern to perform an Undo function.
Listing 6-12. A Sample Caretaker Code that Works with Document and its Memento Pattern to Perform an Undo Function
var document = new Document(); document.AddLine("first");
var memento = document.GetMemento(); document.AddLine("third"); Debug.Assert( document.GetLines().Aggregate((c,n)=>c+n) == "firstthird"); document.Undo(memento); Debug.Assert( document.GetLines().Aggregate((c, n) => c + n) == "first"); document.AddLine("second"); //...
In .NET, it's possible to make type serializable. With types like this the memento becomes that serialized state and the caretaker simply serializes the type to a caretaker-specific place to deserialize the state that needs to be restored. In other words, you don't have to implement a method on the originator to save/restore the memento.
Model
This is typically part of other patterns like model-view-controller (MVC) or model-view-presenter (MVP) , but I've chosen to treat it independently (along with view) because this pattern is not unique to user interface design and offers benefits in many other places.
In the model/view/* patterns, model takes on one of two distinct responsibilities (probably one reason this pattern sometimes takes a while to master). The first is typically a logical/nebulous "model" that represents a "domain" or some "business logic". The second is an abstraction of data (sometimes via a data access layer).
In either case, what model/view/* is trying to facilitate is a layered architecture where the model is a lower-level layer to the UI (the view). This means that the model cannot depend on the view; the view can only use the model (not vice versa). In the domain version of the model, the view simply uses a bunch of domain class instances that model business behavior and data encapsulations. This is typically used in monolithic-type applications where a "layer" becomes a bit muddied.
The data-centric model begins to arise when you have strict layers or a tiered architecture where the model lives in another layer from the view and the view should depend on abstractions (i.e., interfaces) rather than concretions (like a specific repository type). In order to maintain the strict layering dependency directions, model classes either end up living in the data layer and become nothing but data containers (no logic) to maintain being abstractions, or live outside of both the UI layer and the data layer. The important part here is the model classes simply become data containers to easily maintain that separation.
140
With model/view/controller, the controller has the responsibility of interacting with the "user" via the web requests. With model/view/presenter, it is the view that interacts with the "user," typically via GUI elements. In either case, if we’re looking for some consistency of "model" across these two types of patterns, the common denominator is that model is data-centric. Software with any level of complexity often begins to have issues with monolithic architectures, so I generally favor the model being data-centric. But this offers up some great pattern reuse when we start to recognize the view as being much more than just a user interface metaphor.
As we've seen with either MVP and MVC is that a "view" of the data is presented. In MVC, that view is more API-based (via HTTP requests and HTML responses) than MVP, where the view is stateful and has its own logic. It's when we start to recognize that any over-the-wire-/API-based presentation of data can benefit from a model/view pattern in that the model can provide the abstraction for the view of data so that our domain or business logic can remain decoupled from APIs (including front-ends). That is, the API is the view.
From the standpoint of how you would organize that in your Visual Studio projects, I've been advocating that the model classes live within the same project as the classes that populate the members of the model classes, or otherwise what implements the API. This can make it more apparent when the model classes have become part of a circular dependence. This also means that each API has a distinct set of model classes to ensure no cross-dependence and ensure single responsibility in each API. For example, in Figure 6-3 if the API projects shared a models project, one API project can influence the development of the other API project. If I change how the RESTful API responds and I change a model, the .NET API now has two reasons to change, which violates the single responsibility principle.
Figure 6-3. Shared model
In this simple example, that may seem fairly benign, but your reason to change the model for a particular API may be at odds with the other in many ways. Take, for example, serialization of a model. Let's say that our RESTful API needs to support XML and JSON serialization. You often have to use attributes to provide the metadata to serialization engines to serialize correctly. If we then look at the other API that may need to serialize as binary data like Protobuf, you quickly run into competing attributes. If two serializers use DataContractAttribute but need to use them in two different ways, you've just reached an insurmountable problem with a shared model project. This becomes even more apparent if you look at how RESTful implements versioning compared to Protobuf (where one has different versions of a Model and one versions members of one Model). It's just easier to recognize the single responsibility principle and have the model dependencies detailed in Figure 6-4 .
141
Retry
I include a pattern for retry because it’s an often forgotten cross-cutting concern that is very prevalent with complex systems that rely on network communications, when they make network requests (and remember that many occur outside of using HttpClient / WebRequest ) with database access, message queue access, e-mail access, etc.
The intent of this pattern is to encapsulate the policy (or polices) around retry logic and the action of retrying from the particular action or request that need to potentially retry. This is very similar to circuit breaker.
Making a request to a RESTful API can fail for a variety of reasons. Some of those reasons are "fatal," and some of those reasons are transient . A transient error is a temporary error so that even though this request might have failed, doing it again in the future may work fine. An ability to pause for a certain amount of time before retrying an operation could allow a larger process flow to continue eventually without completely failing.
As already noted, this can easily apply to raw HTTP communications. This pattern can be applied to any situation where transient errors can occur. Over-the-wire type communication is one of the best examples because intermittent network errors can and often do happen. This type of pattern would not necessarily be useful when there is a direct interaction with a user, in which case the user interface can simply inform the user what is happening and leave it up to them to decide what to do next.
Listing 6-13 is an example of a simple class to encapsulate retry policies and logic.
Listing 6-13. A Simple Class to Encapsulate Retry Policies and Logic
public class RetryExecutor {
readonly Action action;
private readonly int maxTryCount; private readonly TimeSpan interTryDelay; private int tryCount;
public RetryExecutor(Action action, int maxTryCount, TimeSpan interTryDelay) {
if (action == null) throw new ArgumentNullException(nameof(action)); this.action = action;
this.maxTryCount = maxTryCount; this.interTryDelay = interTryDelay; }
142
public void Execute() { do { tryCount++; try { action(); return; } catch (Exception) { if (tryCount >= maxTryCount) { throw; } } Thread.Sleep(interTryDelay); } while (tryCount < maxTryCount); }
}
This, of course, is overly simplistic and uses hard-coded values but gets the idea across. A true implementation will need to consider where the policies are stored (config, etc.) and the other types of delegates that you may need to execute (like Func<T> or asynchronous code).
With the act of retrying you may eventually find the need to give up. Under those circumstances you may want to work with the circuit breaker pattern to stop all similar requests until the problem is resolved. With transient errors that could mean simply waiting longer than what is programmatically configured, with other types of errors, interaction with a service provider may be required. In either case, the circuit breaker would be closed when those types of operations are expected to succeed again.
View
The view pattern has been closely aligned with MV* patterns like model view controller and model view presenter. But, as I detailed in the model pattern, those are really only two ways to interpret view with respect to a model.
In the traditional MVC pattern, the view is content that is communicated back to a browser and then displayed out-of-band from the application logic. Which is different from the MVP pattern, where the view directly interacts with the user (through UI controls) and with application logic (the presenter). MVP hasn't really been adopted in the .NET community, so we won't get into any more detail on it.
The often-overlooked applicability of view is really anywhere software provides a data-only response. In MVC, the model comes into play in both the inputs to the controller and the outputs of the controller (that feed into the view). But, as we detailed in the model pattern, that's exactly like a Web API. This use of the view pattern and the model pattern can also be used anywhere we have an API. The logic that the API performs decouples itself from clients of the API by using model classes at seams and mapping them to internal structures before performing any logic. This use of view and models is very similar to what the bridge pattern accomplishes, only at the seams. Effectively bridging the outward facing API (view) from the implementation.