feat: anti corruption layer - Issue 1325. (#2777)

* add init impl

* add docs

* add test

* correct tests and specs

* add check style corrections

* add init impl

* add docs

* add test

* correct tests and specs

* add check style corrections

* issue_1325: revert accidental damage to the other repos

* issue_1325: sort the comments

---------

Co-authored-by: Boris Zhguchev <boris.zhguchev@gropyus.com>
This commit is contained in:
Boris ZhguchevandBoris Zhguchev authored and GitHub committed 2024-03-17 09:32:24 +02:00
1 parent 7034ca5e95
commit 5c47447d9b
19 files changed
+677

No files matched your search

@@ -0,0 +1,42 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2023 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.corruption;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* This layer translates communications between the two systems,
* allowing one system to remain unchanged while the other can avoid compromising
* its design and technological approach.
*/
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@@ -0,0 +1,28 @@
/**
* Context and problem
* Most applications rely on other systems for some data or functionality.
* For example, when a legacy application is migrated to a modern system,
* it may still need existing legacy resources. New features must be able to call the legacy system.
* This is especially true of gradual migrations,
* where different features of a larger application are moved to a modern system over time.
*
* <p>Often these legacy systems suffer from quality issues such as convoluted data schemas
* or obsolete APIs.
* The features and technologies used in legacy systems can vary widely from more modern systems.
* To interoperate with the legacy system,
* the new application may need to support outdated infrastructure, protocols, data models, APIs,
* or other features that you wouldn't otherwise put into a modern application.
*
* <p>Maintaining access between new and legacy systems can force the new system to adhere to
* at least some of the legacy system's APIs or other semantics.
* When these legacy features have quality issues, supporting them "corrupts" what might
* otherwise be a cleanly designed modern application.
* Similar issues can arise with any external system that your development team doesn't control,
* not just legacy systems.
*
* <p>Solution Isolate the different subsystems by placing an anti-corruption layer between them.
* This layer translates communications between the two systems,
* allowing one system to remain unchanged while the other can avoid compromising
* its design and technological approach.
*/
package com.iluwatar.corruption;
@@ -0,0 +1,44 @@
package com.iluwatar.corruption.system;
import com.iluwatar.corruption.system.legacy.LegacyShop;
import com.iluwatar.corruption.system.modern.Customer;
import com.iluwatar.corruption.system.modern.ModernOrder;
import com.iluwatar.corruption.system.modern.Shipment;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* The class represents an anti-corruption layer.
* The main purpose of the class is to provide a layer between the modern and legacy systems.
* The class is responsible for converting the data from one system to another
* decoupling the systems to each other
*
* <p>It allows using one system a domain model of the other system
* without changing the domain model of the system.
*/
@Service
public class AntiCorruptionLayer {
@Autowired
private LegacyShop legacyShop;
/**
* The method converts the order from the legacy system to the modern system.
* @param id the id of the order
* @return the order in the modern system
*/
public Optional<ModernOrder> findOrderInLegacySystem(String id) {
return legacyShop.findOrder(id).map(o ->
new ModernOrder(
o.getId(),
new Customer(o.getCustomer()),
new Shipment(o.getItem(), o.getQty(), o.getPrice()),
""
)
);
}
}
@@ -0,0 +1,25 @@
package com.iluwatar.corruption.system;
import java.util.HashMap;
import java.util.Optional;
/**
* The class represents a data store for the modern system.
* @param <V> the type of the value stored in the data store
*/
public abstract class DataStore<V> {
private final HashMap<String, V> inner;
public DataStore() {
inner = new HashMap<>();
}
public Optional<V> get(String key) {
return Optional.ofNullable(inner.get(key));
}
public Optional<V> put(String key, V value) {
return Optional.ofNullable(inner.put(key, value));
}
}
@@ -0,0 +1,25 @@
package com.iluwatar.corruption.system;
/**
* The class represents an general exception for the shop.
*/
public class ShopException extends Exception {
public ShopException(String message) {
super(message);
}
/**
* Throws an exception that the order is already placed but has an incorrect data.
*
* @param lhs the incoming order
* @param rhs the existing order
* @return the exception
* @throws ShopException the exception
*/
public static ShopException throwIncorrectData(String lhs, String rhs) throws ShopException {
throw new ShopException("The order is already placed but has an incorrect data:\n"
+ "Incoming order: " + lhs + "\n"
+ "Existing order: " + rhs);
}
}
@@ -0,0 +1,19 @@
package com.iluwatar.corruption.system.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents an order in the legacy system.
* The class is used by the legacy system to store the data.
*/
@Data
@AllArgsConstructor
public class LegacyOrder {
private String id;
private String customer;
private String item;
private int qty;
private int price;
}
@@ -0,0 +1,28 @@
package com.iluwatar.corruption.system.legacy;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* The class represents a legacy shop system. The main purpose is to place the order from the
* customers.
*/
@Service
public class LegacyShop {
@Autowired private LegacyStore store;
/**
* Places the order in the legacy system. If the order is already present in the modern system,
* then the order is placed only if the data is the same. If the order is not present in the
* modern system, then the order is placed in the legacy system.
*/
public void placeOrder(LegacyOrder legacyOrder) {
store.put(legacyOrder.getId(), legacyOrder);
}
/** Finds the order in the legacy system. */
public Optional<LegacyOrder> findOrder(String orderId) {
return store.get(orderId);
}
}
@@ -0,0 +1,13 @@
package com.iluwatar.corruption.system.legacy;
import com.iluwatar.corruption.system.DataStore;
import org.springframework.stereotype.Service;
/**
* The class represents a data store for the legacy system.
* The class is used by the legacy system to store the data.
*/
@Service
public class LegacyStore extends DataStore<LegacyOrder> {
}
@@ -0,0 +1,13 @@
package com.iluwatar.corruption.system.modern;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents a customer in the modern system.
*/
@Data
@AllArgsConstructor
public class Customer {
private String address;
}
@@ -0,0 +1,20 @@
package com.iluwatar.corruption.system.modern;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents an order in the modern system.
*/
@Data
@AllArgsConstructor
public class ModernOrder {
private String id;
private Customer customer;
private Shipment shipment;
private String extra;
}
@@ -0,0 +1,47 @@
package com.iluwatar.corruption.system.modern;
import com.iluwatar.corruption.system.AntiCorruptionLayer;
import com.iluwatar.corruption.system.ShopException;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* The class represents a modern shop system.
* The main purpose of the class is to place orders and find orders.
*/
@Service
public class ModernShop {
@Autowired
private ModernStore store;
@Autowired
private AntiCorruptionLayer acl;
/**
* Places the order in the modern system.
* If the order is already present in the legacy system, then no need to place it again.
*/
public void placeOrder(ModernOrder order) throws ShopException {
String id = order.getId();
// check if the order is already present in the legacy system
Optional<ModernOrder> orderInObsoleteSystem = acl.findOrderInLegacySystem(id);
if (orderInObsoleteSystem.isPresent()) {
var legacyOrder = orderInObsoleteSystem.get();
if (!order.equals(legacyOrder)) {
throw ShopException.throwIncorrectData(legacyOrder.toString(), order.toString());
}
} else {
store.put(id, order);
}
}
/**
* Finds the order in the modern system.
*/
public Optional<ModernOrder> findOrder(String orderId) {
return store.get(orderId);
}
}
@@ -0,0 +1,12 @@
package com.iluwatar.corruption.system.modern;
import com.iluwatar.corruption.system.DataStore;
import org.springframework.stereotype.Service;
/**
* The class represents a data store for the modern system.
*/
@Service
public class ModernStore extends DataStore<ModernOrder> {
}
@@ -0,0 +1,16 @@
package com.iluwatar.corruption.system.modern;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* The class represents a shipment in the modern system.
* The class is used by the modern system to store the data.
*/
@Data
@AllArgsConstructor
public class Shipment {
private String item;
private int qty;
private int price;
}
Whitespace-only changes.
@@ -0,0 +1,87 @@
package com.iluwatar.corruption.system;
import com.iluwatar.corruption.system.legacy.LegacyOrder;
import com.iluwatar.corruption.system.legacy.LegacyShop;
import com.iluwatar.corruption.system.modern.Customer;
import com.iluwatar.corruption.system.modern.ModernOrder;
import com.iluwatar.corruption.system.modern.ModernShop;
import com.iluwatar.corruption.system.modern.Shipment;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class AntiCorruptionLayerTest {
@Autowired
private LegacyShop legacyShop;
@Autowired
private ModernShop modernShop;
/**
* Test the anti-corruption layer.
* Main intention is to demonstrate how the anti-corruption layer works.
* <p>
* The 2 shops (modern and legacy) should operate independently and in the same time synchronize the data.
* To avoid corrupting the domain models of the 2 shops, we use an anti-corruption layer
* that transforms one model to another under the hood.
*
*/
@Test
public void antiCorruptionLayerTest() throws ShopException {
// a new order comes to the legacy shop.
LegacyOrder legacyOrder = new LegacyOrder("1", "addr1", "item1", 1, 1);
// place the order in the legacy shop.
legacyShop.placeOrder(legacyOrder);
// the order is placed as usual since there is no other orders with the id in the both systems.
Optional<LegacyOrder> legacyOrderWithIdOne = legacyShop.findOrder("1");
assertEquals(Optional.of(legacyOrder), legacyOrderWithIdOne);
// a new order (or maybe just the same order) appears in the modern shop.
ModernOrder modernOrder = new ModernOrder("1", new Customer("addr1"), new Shipment("item1", 1, 1), "");
// the system places it, but it checks if there is an order with the same id in the legacy shop.
modernShop.placeOrder(modernOrder);
Optional<ModernOrder> modernOrderWithIdOne = modernShop.findOrder("1");
// there is no new order placed since there is already an order with the same id in the legacy shop.
assertTrue(modernOrderWithIdOne.isEmpty());
}
/**
* Test the anti-corruption layer.
* Main intention is to demonstrate how the anti-corruption layer works.
* <p>
* This test tests the anti-corruption layer from the rule the orders should be the same in the both systems.
*
*/
@Test(expected = ShopException.class)
public void antiCorruptionLayerWithExTest() throws ShopException {
// a new order comes to the legacy shop.
LegacyOrder legacyOrder = new LegacyOrder("1", "addr1", "item1", 1, 1);
// place the order in the legacy shop.
legacyShop.placeOrder(legacyOrder);
// the order is placed as usual since there is no other orders with the id in the both systems.
Optional<LegacyOrder> legacyOrderWithIdOne = legacyShop.findOrder("1");
assertEquals(Optional.of(legacyOrder), legacyOrderWithIdOne);
// a new order but with the same id and different data appears in the modern shop
ModernOrder modernOrder = new ModernOrder("1", new Customer("addr1"), new Shipment("item1", 10, 1), "");
// the system rejects the order since there are 2 orders with contradiction there.
modernShop.placeOrder(modernOrder);
}
}