More on this book
Community
Kindle Notes & Highlights
Read between
March 28 - October 28, 2018
like. Or the data can simply be arguments in function calls. Or you can pack it into a hashmap, or construct it into an object.
We don’t want to cheat and pass Entity objects or database rows.
For example, many database frameworks return a convenient data format in response to a query. We might call this a “row structure.” We don’t want to pass that row structure inward across a boundary.
The job of the Presenter is to repackage the OutputData into viewable form as the ViewModel, which is yet another plain old Java object.
Whereas the OutputData may contain Date objects, the Presenter will load the ViewModel with corresponding Strings already formatted properly for the user.
This leaves the View with almost nothing to do other than to move the data from the ViewModel into the HTML page.
By separating the software into layers and conforming to the Dependency Rule, you will create a system that is intrinsically testable, with all the benefits that implies.
Presenters are a form of the Humble Object pattern, which helps us identify and protect architectural boundaries.
Using the Humble Object pattern, we can separate these two kinds of behaviors into two different classes called the Presenter and the View.
The View is the humble object that is hard to test. The code in this object is kept as simple as possible. It moves data into the GUI but does not process that data.
The Presenter is the testable object. Its job is to accept data from the application and format it for presentation so that the View can simply move it to the screen.
Anything and everything that appears on the screen, and that the application has some kind of control over, is represented in the View Model as a string, or a boolean, or an enum. Nothing is left for the View to do other than to load the data from the View Model into the screen. Thus the View is humble.
It has long been known that testability is an attribute of good architectures.
For example, if the application needs to know the last names of all the users who logged in yesterday, then the UserGateway interface will have a method named getLastNamesOfUsersWhoLoggedInAfter that takes a Date as its argument and returns a list of last names.
The interactors, in contrast, are not humble because they encapsulate application-specific business rules. Although they are not humble, those interactors are testable, because the gateways can be replaced with appropriate stubs and test-doubles.
First, let’s get something straight: There is no such thing as an object relational mapper (ORM). The reason is simple: Objects are not data structures. At least, they are not data structures from their users’ point of view. The users of an object cannot see the data, since it is all private. Those users see only the public methods of that object. So, from the user’s point of view, an object is simply a set of operations.
A data structure, in contrast, is a set of public data variables that have no implied behavior.
ORMs would be better named “data mappers,” because they load data into data structures from ...
This highlight has been truncated due to consecutive passage length restrictions.
Indeed, ORMs form another kind of Humble Object boundary between the gateway interfaces and the database.
On the input side, the service listeners will receive data from the service interface and format it into a simple data structure that can be used by the application.
Full-fledged architectural boundaries are expensive. They require reciprocal polymorphic Boundary interfaces, Input and Output data structures, and all of the dependency management necessary to isolate the two sides into independently compilable and deployable components.
This kind of anticipatory design is often frowned upon by many in the Agile community as a violation of YAGNI: “You Aren’t Going to Need It.” Architects, however, sometimes look at the problem and think, “Yeah, but I might.” In that case, they may implement a partial boundary.
The reciprocal interfaces are there, the input/output data structures are there, and everything is all set up—but we compile and deploy all of them as a single component.
However, it does not require the administration of multiple components. There’s no version number tracking or release management burden. That difference should not be taken lightly.
The full-fledged architectural boundary uses reciprocal boundary interfaces to maintain isolation in both directions.
It is easy to think of systems as being composed of three components: UI, business rules, and database. For some simple systems, this is sufficient. For most systems, though, the number of components is larger than that.
For example, language is not the only axis of change for the UI. We also might want to vary the mechanism by which we communicate the text.
That means that there is a potential architectural boundary defined by this axis of change.
If we were to look inside GameRules, we would find polymorphic Boundary interfaces used by the code inside GameRules and implemented by the code inside the Language component. We would also find polymorphic Boundary interfaces used by Language and implemented by code inside GameRules.
This example is intended to show that architectural boundaries exist everywhere. We, as architects, must be careful to recognize when they are needed.
We also have to be aware that such boundaries, when fully implemented, are expensive. At the same time, we have to recognize that when such boundaries are ignored, they are very expensive to add in later—even in the presence of comprehensive test-suites and refactoring discipline.
So there you have it. O Software Architect, you must see the future. You must guess—intelligently. You must weigh the costs and determine where the architectural boundaries lie, and which should be fully implemented, and which should be partially implemented, and which should be ignored.
But this is not a one-time decision. You don’t simply decide at the start of a project which boundaries to implement and which to ignore. Rather, you watch. You pay attention as the system evolves. You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don’t exist.
At that point, you weigh the costs of implementing those boundaries versus the cost of ignoring them—and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing...
This highlight has been truncated due to consecutive passage length restrictions.
It is in this Main component that dependencies should be injected by a Dependency Injection framework.
Think of Main as a plugin to the application—a plugin that sets up the initial conditions and configurations, gathers all the outside resources, and then hands control over to the high-level policy of the application.
Since it is a plugin, it is possible to have many Main components, one for each configuration of your application.
• Services seem to be strongly decoupled from each other. As we shall see, this is only partially true. • Services appear to support independence of development and deployment. Again, as we shall see, this is only partially true.
The architecture of a system is defined by boundaries that separate high-level policy from low-level detail and follow the Dependency Rule.
It’s just that services, in and of themselves, do not define an architecture.
Service interfaces are no more formal, no more rigorous, and no better defined than function interfaces.
First, history has shown that large enterprise systems can be built from monoliths and component-based systems as well as service-based systems.
One of the taxi suppliers has agreed to participate in this program. Others are likely to follow. Still others may decline. Of course, some drivers may be allergic to cats, so those drivers should never be selected for this service. Also, some customers will undoubtedly have similar allergies, so a vehicle that has been used to deliver kittens within the last 3 days should not be selected for customers who declare such allergies. Look at that diagram of services. How many of those services will have to change to implement this feature? All of them.
This is the problem with cross-cutting concerns. Every software system must face this problem, whether service oriented or not. Functional decompositions, of the kind depicted in the service diagram in Figure 27.1, are very vulnerable to new features that cut across all those functional behaviors.
Those services do not define the architectural boundaries of the system; instead, the components within the services do.
Yes, that’s right: The tests are part of the system,
TESTS AS SYSTEM COMPONENTS
Tests, by their very nature, follow the Dependency Rule; they are very detailed and concrete; and they always depend inward toward the code being tested. In fact, you can think of the tests as the outermost circle in the architecture. Nothing within the system depends on the tests, and the tests always depend inward on the components of the system.
Tests are also independently deployable. In fact, most of the time they are deployed in test systems, rather than in production systems. So, even in systems where independent deployment is not otherwise necessary, the tests will still be independently deployed.
DESIGN FOR TESTABILITY