This week we met from 9am – 2pm on Wednesday to complete the introduction of Product as a super class of Account and Loan by adding the hibernate mapping for Product. The inheritance mapping type that we chose (in accordance to the discussion in the previous meeting) was the one-table-per-subclass mapping. The steps that we followed were as follows:

  1. Created a Product.hbm.xml file with the Product, Account and Loan hibernate mappings (they have to be in one file because Account and Loan are listed as subclasses under Product).
  2. Deleted the Account.hbm.xml and Loan.hbm.xml files.
  3. Changed the hibernate configuration file to load Product.hbm.xml and not load Account.hbm.xml and Loan.hbm.xml.
  4. Changed tables-schema.ddl and sample.sql to reflect the hibernate mappings changes.
  5. Changed the action classes and jsp pages that were using Account collections to use the appropriate Product properties (in cases where the property had been moved up into Product).
  6. Tested the chages by re-deploying the application and running though the various use-cases.

Before making our changes we synchronized with the repository as usual and to our surprise we saw that somone had reverted back changes that we had commited before Thanksgiving. After further investigation we found out that it was due to merging by the GL team. We will discuss this further during this weeks meeting.

Is Worse Better?

December 1, 2006

Yes, of-course! Worse is always better than worst!

But seriously:

The worse is better series of essays emphasize an important truth about software architecture and software development processes. The truth is that there is no silver bullet; there is no absolute best architecture or absolute best process. Richard Gabriel (the author of the essays) himself was undecided about whether the “Worse is Better” approach to software development was better than “The Right Thing” approach. He convincingly argues for and against both the approaches multiple times! So my conclusion from the series is that the best architecture and best process are different for software projects with different circumstances and requirements.

This brings us to the question that Prof Johnson put forth in class: “Does this mean that we should not study/research software architecture?”. I think that this provides a greater reason for studying/researching software architecture and software processes. Not only do we need to find better processes and architectures, we also need to determine the circumstances under which they will be better. This seems obvious, but sometimes when people develop a loyalty for a certain architecture or process, they forget that the architecture or process is only good for some circumstances.

The toybank project has many DAO classes. All these DAO classes extend GenericDAO and hence inherit the methods save, delete, findAll, findById, findByExample, findByCriteria. Therefore, we have implemented the TestGenericDAO test class which tests all those generic methods. TestGenericDAO is an abstract class which extends TestCase and tests all the methods provided by GenericDAO. The test classes for the DAO classes that extend GenericDAO should extend TestGenericDAO.

In order to extend TestGenericDAO, you need to implement the following five abstract methods:

protected abstract GenericDAO<PersistentType, IDType> getDAO();

protected abstract IDType getId(PersistentType persistentObject);

protected abstract boolean areIdentical(PersistentType persistentObject, PersistentType anotherPersistentObject);

protected abstract List<PersistentType> getTestingSamples();

protected abstract Map<Example, List<Integer>> getExamplesWithTestResults();

protected abstract Map<List<Criterion>, List<Integer>> getCriterionWithTestResults();

All these methods are well commented. The comments clearly explain the purpose/requirements for the implementations. You can look at TestPersonDAO for an example of how to implement and use the abstract methods.

The setUp method in TestGenericDAO initializes a protected instance of the appropriate DAO class. This is the instance that should be used in all the test methods. The tearDown method closes the session used by the protected DAO instance. This ensures that no two tests affect each other by using the same session.

An important requirement for all the test methods in any test class that extends TestGenericDAO is that they should leave the database at the same state that it was in at the start of the method. i.e. All the test methods should clean up all the changes that they make to the database. The generic test methods that are provided by TestGenericDAO follow this rule. You can look at them to see how it is done.

Another requirement for using TestGenericDAO is the existence of a database with the name testtoybank. This is the database on which all the tests are run. This database can be created by running the test-init-db ant task in build.xml. The test-init-db task checks to see if there is a database by the name testtoybank, if there isnt one, it creates one. Then it creates all the tables required for toybank in testtoybank. The sql script for creating the testdatabse is in test-db-schema.ddl and the sql script for creating the tables is in tables-schema.ddl. We have also changed the init-db task to use a similar process. So init-db now runs db-schema.dll, tables-schema.dll (common to both the tasks), sample.sql and gl_insert_accounts.sql.

Interesting excerpt:

“The ultimate purpose of software is to serve users. But first, that same software has to serve developers. This is especially true in a process that emphasizes refactoring. As a program evolves, developers will rearrange and rewrite every part. They will integrate the domain objects into the application and with new domain objects. Even years later, maintenance programmers will be changing and extending the code. People have to work with this stuff. But will they want to?”

Introduction:

These chapters focus on concepts that complement deep models by helping developers develop implementations that enable refactoring and iterative refinement and allow the continuous synchronous improvement of the domain model and implementation. Chapter 10 introduces patterns that contribute towards a supple design, chapter 11 is about applying analysis patterns, chapter 12 discusses design patters and chapter 13 is about refactoring towards deeper insight.

Supple Design:

Supple design is essential for the successful evolution of a system. Without a supple design, the system becomes more unwieldy with every change. In this chapter the author introduces the following patterns that contribute towards a supple design:

  • INTENTION-REVEALING INTERFACES
  • SIDE-EFFECT-FREE FUNCTIONS
  • ASSERTIONS
  • CONCEPTUAL CONTOURS
  • STANDALONE CLASSES
  • CLOSURE OF OPERATIONS

Analysis Patterns:

The author presents Analysis patterns as higher level patterns which encapsulate previous solutions to common problems at the level of responsibilities or relationships between domain objects. They can apply to a particular domain or multiple domains. The author quotes the book “Analysis Patterns: Reusable Object Models” by Martin Fowler multiple times. The main message that can be derived from the chapter is that an Analysis pattern will seldom provide an out-of-the-box solution to a problem; instead it will provide a good starting point from which a suitable solution can be built.

Design Patterns:

The author presents Design patterns as patterns which provide solutions to technical i.e. non domain problems. Design patterns can therefore be applied across most domains since they are domain agnostic. Some design patterns can be used as domain patterns by changing perspectives. The author gives the examples of POLICY/STRATEGY and COMPOSITE. Some others cannot be used as domain patterns, for example FLYWEIGHT.

Refactoring Towards Deeper Insight:

In this chapter the author presents a broader context for the process of refactoring. The author presents the following three things to focus on:

  1. Live in the domain.
  2. Keep looking at things a different way.
  3. Maintain an unbroken dialog with domain experts.

The source of refactoring is some dissatisfaction with the domain model. This dissatisfaction may not necessarily stem from bad code. It could be that new requirements are not jelling naturally or that the UBIQUITOUS LANGUAGE seems disconnected from the domain experts. The timing of the refactoring is important and the author presents the following clues for when refactoring should be performed:

  • The design does not express the team’s current understanding of the domain.
  • Important concepts are implicit in the design (and you see a way to make them explicit).
  • You see an opportunity to make some important part of the design suppler.

Sometimes when a huge gap/change in the domain model is recognised, it might seem like a crisis, but the author suggests that these instances should be looked at as opportunities to perform valuable refactoring that provides deeper insight.

Discussion: People who talk about XP often make re-factoring sound magical. Evans makes it sound like hard work, and, though necessary, not sufficient. What are some things he talks about in these chapters that you need in addition to re-factoring to come up with a great model?

Introduction:

As with most things, models are meant improve (mature) over time. These chapters discuss various ways in which this can happen or be achieved. Lots of these methods are eventually implemented by re-factoring the already existing model. The author (rightly) does not present re-factoring as a silver bullet that will solve all the problems with a model. Instead it is presented as a tool that needs to be used to implement necessary changes that are recognised through the various strategies discussed in these chapters. Depending on the situation, re-factoring can be hard or easy.

Breakthroughs and Deeper Models:

During the lifetime of most projects, if there is a constant effort to improve the model, there will be a time when a major breakthrough presents itself in terms of an opportunity to deepen the model and achieve a multitude of improvements that solve many problems. To demonstrate this, the author presents an excellent example of a breakthrough in one of the projects that he worked on. To improve the chances of achieving such a break through, the project members should focus on the basics like knowledge crunching and developing a robust UBIQUITOUS LANGUAGE and they should not shy away from making modest, piecemeal improvements to the model, because they will eventually lead to a breakthrough. When the opportunity for a breakthrough presents itself, it can sometimes seem daunting and risky due to the size of the changes required, but at the same time, it also presents an opportunity for significant improvements and future benefits in terms of modifiability and maintainability of the model.

Towards Deeper Models:

After illustrating what it means to make a model deeper and the benefits that can be obtained by having a deeper model, the author presents some techniques that can used to create a deeper model. These techniques are divided into two categories as follows:
Digging Out Concepts:

  • Listen to Language
  • Scrutinize Awkwardness
  • Contemplate Contradictions

These techniques are related to the UBIQUITOUS LANGUAGE and knowledge crunching and are about the better usage of UBIQUITOUS LANGUAGE and knowledge crunching in order to extract important domain concepts and hence deepen the domain model.

Modelling Less Obvious Concepts:

  • Explicit Constraints
  • Processes as Domain Objects
  • SPECIFICATION
    • Validation
    • Selection (Querying)
    • Building to Order (Generating)

These techniques are about better representations for less obvious concepts which can greatly improve the model. The author as usual, provides concrete examples for all these techniques.

Q & A: In what way are these patterns much more specific to business computing than those in the first reading?

The patterns in Part II are more specific to business computing because most of them are persistence aware and assume a layered architecture. Both those characteristics are common among business computing systems.

Interesting excerpts from Part II:

“Developing a good domain model is an art. But the practical design and implementation of a model’s individual elements can be relatively systematic. Isolating the domain design from the mass of other concerns in the software system will greatly clarify the design’s connection to the model. Defining model elements according to certain distinctions sharpens their meanings. Following proven patterns for individual elements helps produce a model that is practical to implement.”

Introduction

The previous part of the book introduced the patterns UBIQUITOUS LANGUAGE, MODEL-DRIVEN DESIGN and HANDS-ON MODELERS which target the central goal of having a common domain model which is consistent with the UBIQUITOUS LANGUAGE and implementation. This part of the book introduces fundamental and standard patterns which team members can use to produce such a model.

Chapter 4: Isolating the Domain

In most software projects, the code that directly deals with accomplishing domain activities is usually accompanied by code for many other supporting functions like user interface and persistence. If the domain code is not isolated from the other code, it becomes very had to reason about the domain code and maintain consistency with the domain model. This chapter introduces the LAYERED ARCHITECTURES pattern which can be used to isolate the domain from the rest of the implementation. This is a widely used pattern and the most commonly used layers are User Interface/Presentation Layer, Application Layer, Domain Layer and Infrastructure Layer. The most necessary layer is the domain layer. This layer isolates the domain code from the rest of the implementation and allows developers to focus on expressing the domain model accurately and hence enables MODEL-DRIVEN DESIGN.

This chapter also introduces the SMART UI anti-pattern in which the domain code and user interface code are not separated. The author says that this might be suitable for some (usually small) systems without complex domains. It is called an anti-pattern because it does not enable MODEL-DRIVEN DESIGN.

Chapter 5: A Model Expressed in Software

This chapter discuses the elements that can be used to build an implementation of a model and the patterns which can be used to define those elements. The patterns are:

ENTITY: An object that is distinguished by its identity rather than its attributes. The author provides the great example of transactions in a banking system. Even if two transactions for the same amount are executed at the same time, they are still considered distinct. ENTITY objects usually have a life cycle through which they go through different states.

VALUE OBJECT: An object that is distinguished solely by its attributes. VALUE OBJECTS don’t have life cycles and should therefore be immutable.

Associations: A relationship between elements (ENTITY objects and VALUE OBJECTS) which can have various traversal directions and multiplicities.

SERVICE: A stateless operation that does not conceptually belong to an ENTITY or VALUE OBJECT.

MODULE: A higher level domain concept which groups together of a set of cohesive elements. MODULE objects are used to present higher level relationships in the domain model and they can be seen as sub models which hide interior details from external observations and on the other hand allow people to look at a particular part of the model without being overwhelmed by the details of the entire model. MODULE OBJECTS should have low coupling and high cohesion.

Chapter 6: The Life Cycle of a Domain Object

This chapter introduces three patterns that can be used to manage domain objects with complex life cycles. They are:

AGGREGATES: Used to group ENTITIES and VALUE OBJECTS together and define clear boundaries around them.

FACTORIES: Used to manage the beginning of life cycles. FACTORIES are responsible for the creation of complex objects and AGGREGATES.

REPOSITORIES: Used to manage the middle and end of life cycles. REPOSITORIES are responsible for providing the means of finding and retrieving persistent objects and encapsulating the infrastructural complexity of those operations.

Interesting excerpts from Part I :

“Domain modeling is not a matter of making as “realistic” a model as possible. Even in a domain of tangible real-world things, our model is an artificial creation. Nor is it just the construction of a software mechanism that gives the necessary results. It is more like moviemaking, loosely representing reality to a particular purpose. Even a documentary film does not show unedited real life. Just as a moviemaker selects aspects of experience and presents them in an idiosyncratic way to tell a story or make a point, a domain modeler chooses a particular model for its utility.”

“On a project without a common language, developers have to translate for domain experts. Domain experts translate between developers and still other domain experts. Developers even translate for each other. Translation muddles model concepts, which leads to destructive refactoring of code. The indirectness of communication conceals the formation of schisms—different team members use terms differently but don’t realize it. This leads to unreliable software that doesn’t fit together.”

“If the people who write the code do not feel responsible for the model, or don’t understand how to make the model work for an application, then the model has nothing to do with the software. If developers don’t realize that changing code changes the model, then their refactoring will weaken the model rather than strengthen it. Meanwhile, when a modeler is separated from the implementation process, he or she never acquires, or quickly loses, a feel for the constraints of implementation.

Introduction

The introduction to this part contains a section called “The Heart of Software” which I found very insightful. It describes how developers tend to prefer honing their technical skills (by using new technologies, implementation methods etc) rather than spending time to learn more about the domain which is the heart of the software.

Chapter 1: Crunching Knowledge (The model is distilled knowledge)

One of the reasons I am enjoying reading this book is because of the concrete examples and stories/experiences in almost every chapter. This chapter starts of with a dialogue between a developer (the author himself) and domain experts who are together building an initial domain model for a PCB design system. Almost all developers would (should) have gone through such a dialogue at some point and will instantly relate to this example. From the example, the author extracts the ingredients required for effective modelling; at the centre of which lies knowledge crunching. Knowledge crunching can be summarised as the process of extracting relevant knowledge about the domain which naturally forms the domain model which should be the core of the development effort. It helps establish a common language for developers and domain experts to discuss/brainstorm about the project (this aspect is further elaborated in the next chapter). It is also a continuous process and plays the main role in the evolution of the project.

Chapter 2: Communication and the Use of Language (The model is the backbone of a language used by all team members)

This chapter extends upon the common language concept that was introduced earlier. The discussion in this chapter revolves around a pattern that is introduced by the author called UBIQUITOUS LANGUAGE. It is the language that should be used by anyone who is working on the project (developers, domain experts, managers etc) while they are discussing the project. The terms/words of the language should always be consistent with the terms/words used in model (they might be class names, roles etc in the model). So when someone uses a particular term, everyone else will understand it in the right context. If the model is changed, the language should also be changed and if the language is changed, the model should also be changed. So the language and the model should just be two different forms of the same concept (body of knowledge). As the author says “With a UBIQUITOUS LANGUAGE, the model is not just a design artifact. It becomes integral to everything the developers and domain experts do together. The LANGUAGE carries knowledge in a dynamic form. Discussion in the LANGUAGE brings to life the meaning behind the diagrams and code“.

Chapter 3: Binding Model and Implementation (The model and the heart of the design shape each other)

This chapter introduces two patterns. The first pattern is MODEL-DRIVEN DESIGN. This pattern states that the domain model should transcend the boundaries that are traditionally imposed between analysis, design and implementation. All the people involved with implementing the project should work off the same domain model. So the code should directly correspond to the domain model i.e. model and the code should be bound together. If any one of them is changed, the other should be changed as well to maintain consistency. So as a result, the UBIQUITOUS LANGUAGE, the code (implementation) and the domain model will just be different forms of the same concepts (relevant knowledge obtained from knowledge crunching).

The second pattern introduced in this chapter is HANDS-ON MODELERS. This pattern deals with the common/traditional practice of having different teams for analysis, design, development. The pattern states that such divisions damage the integrity of MODEL-DRIVEN DESIGN because whenever there is hand off from one team to the other, information is lost and the model becomes weaker. If the developers are not in some way involved with the creation/maintenance of the model, they are very unlikely to develop an implementation that is consistent with the model. In that case it also becomes very difficult for the person(s) in-charge of modelling to get feedback about the restrictions introduced by implementation concerns (so that they can update the model accordingly) . So both teams will go in two different directions and the model will become irrelevant. Therefore developers should always be involved with the creation/maintenance of the domain model and the people in-charge of the domain model should be involved with the development (implementation).

The second book that we will be reading in CS 527 is “Domain Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans. This book is highly recommended by Prof Johnson. I have really liked the parts of the book that I have already read and I would definitely recommend it to any Kiwiplan/SE buddies reading this blog.

The following sections of the preface caught my attention:

Contrasting Three Projects: This is the first section of the preface and it sets the stage well for the rest of the book. We are informed about three different projects that the author was involved with. All three of them take different approaches which lead to different results. The message from this section is that focusing on developing a good domain model greatly increases the chances of success, but a good domain model alone is not sufficient for success, it has to be accompanied by a suitable development process.

The Challenge of Complexity: This section is about the complexities faced by all software projects. Complexity in a software project is of two different forms, technical complexity and domain complexity. Technical complexity is well known and lots of advances have been made to help projects manage this complexity. For example, the complexity of databases and persistence in general, we have tools like Hibernate which help us deal with this complexity effectively. Most developers are aware of technical complexity and hence many of them are well trained in dealing with it. But domain complexity, which stems from the actual activity/process/problem which the software is targeted towards, is not that well understood and often leads to the downfall of a project. Therefore, the author states that the premise of the book is two fold:

  1. For most software projects, the primary focus should be on the domain and domain logic.
  2. Complex domains should be based on a model.

At the end of this section we are told “Domain-driven design is both a way of thinking and a set of priorities, aimed at accelerating software projects that have to deal with complicated domains“.

Design vs Development Process: This section carries on from the point made in the first section. The author believes that design and process are inextricable and states the following pre-requisites for applying the techniques in the book:

  1. Development must be iterative.
  2. Developers and domain experts should have a close relationship.

Processes that provide these two pre-requisites are usually agile processes, XP being the most well known among them.

That is all I have say about the preface of the book. The following posts will be regarding the chapters in the book.

FYI: From this post onwards, if any text is in italics, that means that it is directly quoted from the book.

Components

As Prof Johnson mentioned in class, it is indeed strange that the authors choose to mention Components so late in the book, almost as an after thought. They should have at-least dealt with components in the Software Reuse chapter (if not in the Architecture Design chapter), since using components is one of the major ways in which we can increase software reuse.

Most of the components chapter discusses the following aspects regarding dealing with the Architectural Mismatches that are encountered while using components:

  1. Avoiding mismatches.
  2. Detecting mismatches.
  3. Repairing mismatches.

The sections regarding avoiding and detecting mismatches just overstate the obvious procedures that need to be followed while choosing a component. As Prof Johnson pointed out in class, most of the time, architects choose to use a component because they have heard good things about it from people whom they trust/respect or because they have prior experience with using the component. But in every case there is always a trial phase during which the architect has to judge whether a component is well suited to achieve the task that it was selected for. This is because it is very difficult to judge a component without using it for sometime. Even if a component satisfies a necessary specification or standard, there can be many subtle mismatches that can only be detected by using the component. One way in which such mismatches can be detected is by talking to people who have already used the component in similar situations, but even that does not guarantee anything.

The most interesting section is the section regarding repairing mismatches. The authors present three methods that can be used for repairs:

  1. Wrappers.
  2. Bridges.
  3. Mediators.

Most of the time, in medium/large sized projects, external/OTS components are used with some kind of generalizing wrappers around them. Using a wrapper allows for lower coupling between the the system and the component. Bridges allow for even lesser coupling because they are just used to convert the data obtained from the component into a format required by the system using the component. Mediators as described by the authors are wrappers which decide which bridge should be used (from a choice of many bridges).

Later on in the chapter, the authors present component based design as a formal search process. The six steps that they propose can be approximately summarised as: if you want to use a component, evaluate it by trying it out in a small(mock) project and decide to use it if it turns out to be suitable.

Software Architecture in the Future

The chapter starts off with a discussion about increasing levels of abstraction in software development. The authors predict the advent of more powerful abstractions. I think that is a safe prediction. It will be interesting to see what direction the next generation of abstractions will take. The authors mention generators and systems of systems (alluding towards greater interoperability). Looking at current trends, I think there will definitely be greater levels of interoperability in the future and architects might have to make decisions about using an entire systems within their system rather than just components.

In the rest of the chapter, the authors revisit some of the earlier topics like ABC, CBAM, Architecture Design, Tools support for maintaining architectures. They make some confessions and predictions about those topics. One confession I liked was:

“How are tactics combined into patterns? In our garage door examples, tactics were chosen and then, almost magically, combined into a pattern. “

Conclusion

Overall, the book did a fair job of handling a very difficult subject. The authors have made a valiant attempt to standardize/formalize a field which has not been standardized before. They introduce a few useful methods like ATAM and CBAM. But sometimes they try to formalize processes that cannot be formalized and end up trivializing the difficulties involved in those processes (for example Chapter 7: Designing the Architecture and Chapter 18: Components). As mentioned in the conclusion of the book (derived from the words of Fred Brooks), software architecture is more about people than about programming. It is about architectures that are created by people, maintained by people, reused by people, used by people and paid for by people. Therefore, it is not possible to have one standard process for creating/designing all kinds of architectures, since technologies change and people themselves change. Instead, we need general guidelines that can guide us towards better architectures rather than fixed steps that will lead to a great architecture for all cases (because such steps do not exist). Another thing I noticed was that the case studies used in the book, analyse architectures that were created using other processes, in terms of the processes introduced in the book. It would have been more interesting to see case studies where the processes introduced in the book were used to create/design the architecture.

The J2EE case study gives us a chance to evaluate a popular/well known software architecture in terms of the measures and procedures promoted by SAIP. The case study starts off with the Architecture Business Cycle that motivated/inspired the J2EE architecture. The main motivation for the architecture was the need to build distributed systems on top of a heterogeneous environment (in terms of hardware, operating systems, network protocols etc). The other major motivator was competition from other systems/architectures with the same goal, like CORBA and COM/DCOM.

An interesting topic to discuss is the reasons behind the success of J2EE in comparison with CORBA and COM. As Prof Johnson mentioned in the class, it definitely helped that the whole J2EE architecture was released together in one go. This was much better than the process followed by CORBA where the initial release was minimal and new additions were made regularly. Another factor that gave J2EE the competitive advantage is the focus on one language (Java) which could run on any machine as opposed to the any language approach adopted by CORBA. Focusing on Java resulted in much greater adoption because everybody could concentrate their efforts on a particular language and therefore share information about their experiences. Yet another factor in favor of J2EE is the fact that it had support for the web interfaces right from the beginning in terms of JSP and Servlets.

After talking about the ABC (Architecture Business Cycle) , the authors move onto the Quality Attributes that were required for J2EE. The quality attribute that seemed really strange was “Usability: Different users must be able to access different content in different forms”. When people talk about usability it is usually about the ease of use of a particular interface for a particular kind of user. The description given seems to incline more towards portability. Across the whole book; the authors seem to name quality attributes in a very general sense and sometimes they even change the meaning of an attribute to suite a particular case study.

After discussing the quality attributes we are presented with a clear description of the J2EE architecture and how it achieves all the attributes stated before. In the deployment view of the architecture, I believe that they got the lines going into and out of the Business Component Tier wrong. The lines should stop and start at the outer boundaries of the Tier rather than the components inside it. The rest of the chapter provides a good introduction to J2EE in general and EJBs in particular. Sometimes it starts feeling like an EJB/J2EE tutorial and I started wondering whether this much implementation detail was necessary to discuss the architecture.

Regarding the Luther case study, like everyone else, I am also a little perplexed about why they chose J2EE since we are not given enough information to deduce this. There could have been many reasons for this. As Prof Johnson mentioned, there might have been other Java systems that they needed to interface with. Another reason could have been that the architect in charge of the system might have been a J2EE expert who just decided to stick with what he/she knew best.

At the end of the Luther case study chapter there is a question which Prof Johnson asked us in class. The question asks us why it is important to separate the producers of the data from the consumers of data in a system. I think the separation is important because there are usually multiple consumers of data who require different views (with different levels of access and different manipulations) of the data produced by the producers. Some of the tactics that we can use to achieve this are modifiability tactics (prevent ripple effects) and security tactics (authenticate users and maintain data confidentiality).

Follow

Get every new post delivered to your Inbox.