My question, overall, is in regards to best practices and efficiency. Today, my teacher and I had a discussion about OOP in MVC Legacy. We were going through a previous project of mine and the question is "What's the point?"
The way my project (and all my projects) are structured, it doesn't make sense to me. Here is an example followed by my code.
Controller- Gets the String values from the form/View and passes them to the Service class. Single Responsibility would state that this is all it is responsible for, not creating an object to pass, but creating a < Map < String,Object >> would be totally fine from my understanding.
Service Class- Following best practices/single responsibility these methods are not supposed to do anything other than call the requested method/pass values.
DAO- The DAO is supposed to be responsible for transferring all data/objects into usable form for the D.B accessor and to return them as such.
But why build an object just to tear it down? Especially when you could just pass a list of values down as a Map< String,Object > so all the values and columns match up?
The following are code snippets to help illustrate my question:
Service Class:
public class ClientService {
private Client_SQL_DAO_Strategy dao;
public ClientService(Client_SQL_DAO_Strategy dao){
setDaoStrategy(dao);
}
public void sendClientToStorage(Client client) throws SQLException, ClassNotFoundException{
dao.sendClientToDatabase(client);
}
public void updateClient(List values) throws SQLException, ClassNotFoundException {
dao.updateClient(values);
}
}
DAO
public void saveClient(Client client) throws ClassNotFoundException, SQLException {
List columns = new ArrayList<>();
columns.add("Last_Name");
columns.add("First_Name");
columns.add("Business_Name");
columns.add("Phone");
List<Object> values = new ArrayList<>();
values.add(client.getClientLastName());
values.add(client.getClientFirstName());
values.add(client.getClientBusiness());
values.add(client.getClientPhone());
accessor.createRecord(TABLE_NAME, columns, values);
}
public void updateClient(List listOfValues) throws ClassNotFoundException, SQLException{
List<Object> columns = new ArrayList<>();
columns.add("Last_Name");
columns.add("First_Name");
columns.add("Business_Name");
columns.add("Phone");
int primaryKey = Integer.valueOf(listOfValues.get(0).toString());
accessor.updateRecord(TABLE_NAME, columns, listOfValues, PK_COLUMN, primaryKey);
}
Comparing the two methods provided in the DAO which approach makes more sense? Creating the Client to tear it down or passing associated values and columns to the accessor? This Map< String,Object > seems ideal for both methods.
And yes, I'm aware of the newer techniques as well, but at the current time in the semester Legacy is the lesson of the week.
Spring MVC implements the Model-View-Controller design pattern.
The responsibility of the Controller is to obtain/create/populate the Model and prepare the environment for the View.
The View is responsible for displaying Model data, in Spring MVC usually through JSP, but you can also specify View classes which do things like render Excel or PDF, for example.
The Model implements the domain logic. Depending on your implementation, this could be a "view model" containing only logic for the front end, or it can contain the real business rules. It should be a real class. Do not EVER use Map<String, Object>. Such usage of maps sacrifices type safety and is not OOP.
The Service class is like a controller class for external service coordination, like persistence, email, payment, etc.
The DAO class is a service provider just for persistence. It translates the object representation to database operations. This layer can be replaced by an ORM. Do not pass around Map<String, Object>!
If the only external service used by your app is persistence, you can avoid separate Service and DAO classes, and defer separation until you require more services.
For more information about this type of object modeling, check out Domain Driven Design.
Related
I've been trying to start a REST api with Spring Boot and I'm a bit strugling with the separation of my resources and which endpoint should be in which file.
Let's say we have an api enpoint to deal with a user and achievements from this user:
/user/{id} GET - to fetch user by id
/achievement/{id} GET - to fetch by achievement
Which are both in their separates resources file:
UserResource
#RestController
public class UserResource {
public UserResource() {...}
#GetMapping("/users/{id}")
public UserDTO getUser(String id) {
log.debug("REST request to get User : {}", login);
return userService.getUserWithAuthoritiesById(id).map(AdminUserDTO::new));
}
And AchievementResource
#RestController
public class AchievementResource {
public AchievementResource(...) {...}
#GetMapping("/achievements/{id}")
public ResponseEntity<Achievement> getAchievement(#PathVariable Long id) {
return achievementRepository.findById(id);
}
}
So far so good, pretty simple. My problem comes when I must get all achievements from a User. Naming covention says I should have an endpoint such as:
/user/{id}/achievements GET
But where should this endpoint be? I feel like both Resources could be good since for the UserResource, the root of the endpoint is the user, but the AchievementResource could be logical too since we are returning achievements.
Easy answer: you have the wrong problem
But where should this endpoint be?
The definition of the resource should be in your machine readable api definition. You produce the class files you need by feeding your definition into a code generator for your choice of language. The generator will put the classes it creates in files somewhere, and you leave them in this default arrangement until some point in the future when you have a compelling reason to arrange them differently (at which point, you fork the code generator and make your preferred design the default).
That said, when designing by hand there's nothing particularly special about "REST endpoints". The guidelines for where resource classes belong is no different from any other classes in Java....
That said, I find that the literature around file layout heuristics rather disappointing. There doesn't seem to be a lot of material discussing the trade offs of different designs, or contexts in which one choice might be more compelling than another.
For your specific situation, I would advise putting the new resource into a file of its own. The argument here being that your UserResource has User dependencies, and your AchievementsResource has achievements dependencies, but your new thing has both, and as a matter of (hand waves) principle, we should avoid bringing unneeded achievements dependencies into the namespace of the UserResource (and vice versa).
In other words, if we find ourselves adding imports to an existing file to implement a new thing, that's a hint that the new thing may be better placed somewhere else.
Using separate files also has nice mechanical advantages - it reduces merge collisions, each file will have its own source control history (meaning that the history of Users isn't cluttered with a bunch of commits that are exclusively about new thing). See Adam Tornhill's work over at CodeScene, for example.
As you separated the controllers, it is not wrong, you should classify the methods by their general entity, "if I need to recover the user's achievements", it is related to both, however, where does she get this data from? of the Achievements knowing that each achievement must have a relationship in the database with the user, you can very well look it up in the achievement controller with a List returnAchievementsByUser (Integer Id) method.
It depends on your point of view and the business behind the scene. You can use just one endpoint in many cases; if "users" are the main resources who have achievements, then "/users/{user-id}" and {users/{user-id}/achievements/{achievement-id} get the user by Id and special achievement of the user
#RestController
#RequestMapping("users")
public class UsersRestController{
#GetMapping("/{user-id}")
public UserDTO getUser(#PathVariable("user-id") String id) {
code...
}
#GetMapping("/{user-id}/achievements/{achievement-id}")
public AchievementDTO getAchievement(#PathVariable("user-id") String userId,
#PathVariable("achievement-id") String achievementId) {
code...
}
}
And if locating "achievements" on top of "users" in their entity hierarchy has meaning to you and your business, then /achievements/{achievement-id}/users/{user-id} can be a rest presentation:
#RestController
#RequestMapping("achievements")
public class AchievementsRestController{
#GetMapping("/{achievement-id}")
public UserDTO getAchievement(#PathVariable("achievements-id") String id) {
code
}
#GetMapping("/{achievements-id}/users/{user-id}")
public AchievementDTO getAchievement(#PathVariable("user-id") String userId,
#PathVariable("achievement-id") String achievementId) {
code
}
}
finally ,whenever they are not in an entity hierarchy, you can pass userId to
"/achievements/{achievements-id}" (or achievement-id to "/users/{user-id}") as a RequestParam.
I read "Clean Code" book ((c) Robert C. Martin) and try to use SRP(single responsibility principle). And I have some questions about it. I have some service in my application, and I do not know how can I refactor it so it matched the right approach. For example, I have service:
public interface SendRequestToThirdPartySystemService {
void sendRequest();
}
What does it do if you look at the class name? - send a request to the third party system. But I have this implementation:
#Slf4j
#Service
public class SendRequestToThirdPartySystemServiceImpl implements SendRequestToThirdPartySystemService {
#Value("${topic.name}")
private String topicName;
private final EventBus eventBus;
private final ThirdPartyClient thirdPartyClient;
private final CryptoService cryptoService;
private final Marshaller marshaller;
public SendRequestToThirdPartySystemServiceImpl(EventBus eventBus, ThirdPartyClient thirdPartyClient, CryptoService cryptoService, Marshaller marshaller) {
this.eventBus = eventBus;
this.thirdPartyClient = thirdPartyClient;
this.cryptoService = cryptoService;
this.marshaller = marshaller;
}
#Override
public void sendRequest() {
try {
ThirdPartyRequest thirdPartyRequest = createThirdPartyRequest();
Signature signature = signRequest(thirdPartyRequest);
thirdPartyRequest.setSignature(signature);
ThirdPartyResponse response = thirdPartyClient.getResponse(thirdPartyRequest);
byte[] serialize = SerializationUtils.serialize(response);
eventBus.sendToQueue(topicName, serialize);
} catch (Exception e) {
log.error("Send request was filed with exception: {}", e.getMessage());
}
}
private ThirdPartyRequest createThirdPartyRequest() {
...
return thirdPartyRequest;
}
private Signature signRequest(ThirdPartyRequest thirdPartyRequest) {
byte[] elementForSignBytes = marshaller.marshal(thirdPartyRequest);
Element element = cryptoService.signElement(elementForSignBytes);
Signature signature = new Signature(element);
return signature;
}
What does it do actually? - create a request -> sign this request -> send this request -> to send the response to Queue
This service inject 4 another services: eventBus, thirdPartyClient, cryptoSevice and marshaller. And in sendRequest method calls each this service.
If I want to create a unit test for this service, I need mock 4 services. I think it's too much.
Can somebody indicate how can this service be changed?
Change the class name and leave as is?
Split into several classes?
Something else?
The SRP is a tricky one.
Let's ask two questions:
What is a responsibility?
What are the different types of responsibilities?
One important thing about responsibilities is that they have a Scope and you can define them in different levels of Granularity. and are hierarchical in nature.
Everything in your application can have a responsibility.
Let's start with Modules. Each module has responsibilities an can adhere to the SRP.
Then this Module can be made of Layers. Each Layer has a responsibility and can adhere to the SRP.
Each Layer is made of different Objects, Functions etc. Each Object and/or Function has responsibilities and can adhere to the SRP.
Each Object has Methods. Each Method can adhere to the SRP. Objects can contain other objects and so on.
Each Function or Method in an Object is made of statements and can be broken down to more Functions/Methods. Each statement can have responsibilities too.
Let's give an example. Let's say we have a Billing module. If this module is implemented in a single huge class, does this module adhere to the SRP?
From the point of view of the system, the module does indeed adhere to the SRP. The fact that it's a mess doesn't affect this fact.
From the point of view of the module, the class that represents this module doesn't adhere to the SRP as it will do a lot of other things, like communicate with DB, send Emails, do business logic etc.
Let's take a look at the different types of responsibilities.
When something should be done
How it should be dome
Let's take an example.
public class UserService_v1 {
public class SomeOperation(Guid userID) {
var user = getUserByID(userID);
// do something with the user
}
public User GetUserByID(Guid userID) {
var query = "SELECT * FROM USERS WHERE ID = {userID}";
var dbResult = db.ExecuteQuery(query);
return CreateUserFromDBResult(dbResult);
}
public User CreateUserFromDBResult(DbResult result) {
// parse and return User
}
}
public class UserService_v2 {
public void SomeOperation(Guid userID) {
var user = UserRepository.getByID(userID);
// do something with the user
}
}
Let's take a look at these two implementations.
UserService_v1 and UserService_v2 do exactly the same thing but different ways. From the point of view of the System, these services adhere to the SRP as they contain operations related to Users.
Now let's take a look at what they actually do to complete their work.
UserService_v1 does these things:
Builds a SQL query string.
Calls the db to execute the query
Takes the specific DbResult and creates a User from it.
Does the operation on the User
UserService_v2 does these things:
1. Requests from the repository the User by ID
2. Does the operation on the User
UserService_v1 contains:
How specific query is build
How the specific DbResult is mapped to a User
When this query need to be called (in the begging of the operation in this case)
UserService_v1 contains:
When a User should be retrieved from the DB
UserRepository contains:
How specific query is build
How the specific DbResult is mapped to a User
What we do here is to move the responsibility of How from the Service to the Repository. This way each class has one reason to change. If how changes, we change the Repository. If when changes, we change the Service.
This way we create objects that collaborate with each other to do specific work, by dividing responsibilities. The tricky parts is: what responsibilities we divide?
If we have a UserService and OrderService we don't divide when and how here. We divide what so we can have one service per Entity in our system.
It's natural for there services to need other objects to do their work. We can of course add all of the responsibilities of what, when and how to a single object but that just makes to the messy, unreadable and hard to change.
In this regard the SRP helps us to achieve cleaner code by having more smaller parts that collaborate with and use each other.
Let's take a look at your specific case.
If you can move the responsibility of how the ClientRequest is created and signed by moving it to the ThirdPartyClient, your SendRequestToThirdPartySystemService will only tell when this request should be sent. This will remove Marshaller, and CryptoService as dependencies from your SendRequestToThirdPartySystemService.
Also you have SerializationUtils that you probably rename to Serializer to capture the intent better as Utils is something that we stick to objects that we just don't know how to name and contains a lot of logic (and probably multiple responsibilities).
This will reduce the number of dependencies and your tests will have less things to mock.
Here's a version of the sendRequest method with less responsibilities.
#Override
public void sendRequest() {
try {
// params are not clear as you don't show them to your code
ThirdPartyResponse response = thirdPartyClient.sendRequest(param1, param2);
byte[] serializedMessage = SerializationUtils.serialize(response);
eventBus.sendToQueue(topicName, serialize);
} catch (Exception e) {
log.error("Send request was filed with exception: {}", e.getMessage());
}
}
From your code I'm not sure if you can also move the responsibility of serialization and deserialization to the EventBus, but if you can do that, it will remove Seriazaliation from your service also. This will make the EventBus responsible for how it serialized and stores the things inside it making it more cohesive. Other objects that collaborate with it will just tell it to send and object to the queue not caring how this objects get's there.
I'm writing an application meant to manage a database using both JDBC and JPA for an exam. I would like the user to select once at the beginning the API to use so that all the application will use the selected API (whether it be JPA or JDBC).
For the moment I decided to use this approach:
I created an interface for each DAO class (e.g. interface UserDAO) with all needed method declarations.
I created two classes for each DAO distinguished by the API used (e.g UserDAOImplJDBC and UserDAOImplJPA). Both of them implement the interface (in our case, UserDAO).
I created a third class (e.g. UserDAOImpl) that extends the JDBC implementation class. In all my code I've been always using this class. When I wanted to switch to the JPA I just had to change in all DAO classes the extends ***ImplDAOJDBC to extends ***ImplDAOJPA.
Now, as I'm starting having many DAO classes it's starting being complicate to modify the code each time.
Is there a way to change all extends faster?
I was considering adding an option in the first screen (for example a radioGroup) to select JDBC or JPA. But yet I have no idea how to make it work without having to restructure all code. Any idea?
Use a factory to get the appropriate DAO, every time you need one:
public class UserDaoFactory {
public UserDao create() {
if (SomeSharedSingleton.getInstance().getPersistenceOption() == JDBC) {
return new UserDAOImplJDBC();
}
else {
return new UserDAOImplJPA();
}
}
}
That's a classic OO pattern.
That said, I hope you realize that what you're doing there should really never be done in a real application:
there's no reason to do the exact same thing in two different ways
the persistence model of JPA and JDBC is extremely different: JPA entities are managed by the JPA engine, so every change to JPA entities is transparently made persistent. That's not the case with JDBC, where the data you get from the database is detached. So the way to implement business logic is very different between JPA and JDBC: you typically never need to save any change when using JPA.
You got 1 and 2 right, but 3 completely wrong.
Instead of having Impl extending one of the other implementations, choose which implementation to initialize using a utility method, for example. That's assuming you don't use Dependency Injection framework such as Spring.
UserDAO dao = DBUtils.getUserDAO();
public class DBUtils {
public static boolean shouldUseJdbc() {
// Decide on some configuration what should you use
}
public static UserDAO getUserDAO() {
if (shouldUseJdbc()) {
return new UserDAOImplJDBC();
}
else {
return new UserDAOImplJPA();
}
}
}
This is still jus an examle, as your DAOs don't need to be instantiated each time, but actually should be singletons.
In most of Java cases there exist two classes: one responsible for apply my business rules - Service layer - and another one responsible for interacting with my database - Dao/Repository layer. However, in PHP cases I just have one class that represents model Layer. My question is, assuming Laravel Framework, should I put my business rules inside a unique model class or there is another approach similar with JSF for instance? Can I use a Middleware class as a Service layer?
To be honest you can use Service/Repo Layers in PHP as well.
So what happens is
Controller passes the inputs to the service and service decides what action is to be done.
The Service Layer then calls the repo for receiving entries from database wherever necessary and perform all the business logic.
The Repo calls the model and data from the model is returned.
The Model only keeps Model specific data (like relations, appended attributes, casts array etc etc...)
To follow this approach, something like this can be done.
Controller
use App\Services\PostService;
class PostController
{
public function __construct()
{
$this->postService = new PostService;
}
public function show($id)
{
$viewData = $this->postService->getPostData($id);
return view('posts.show', $viewData);
}
}
Service Layer
use App\Repositories\PostRepository;
use App\Repositories\CommentRepository;
class PostService
{
public function __construct()
{
$this->postRepo = new PostRepository;
$this->commentRepo = new CommentRepository;
}
public function getPostData($id)
{
$post = $this->postRepo->get($id);
$recentComments = $this->commentsRepo->getRecentComments();
return collect(compact('post', 'recentComments'));
}
}
Repository Layer
use App\Models\Post;
public function PostRepository
{
public function get()
{
return Post::findOrFail($id);
}
}
Also, for your last question, I'd like to say, Middlewares are meant to be used as a per-requisite only. In other words, lets say you want to ensure a user is logged in to view that particular route, then you'll apply the auth middleware and protect your routes from other not-logged in users... According to me, using Service Layer as Middleware isn't really required. You can obviously call a service layer in a Middleware by $this->myService = new Service but making it as a middleware doesn't really sound a good practice.
Hope I answered your question well enough :)
Many Architects and Engineers recommend Dependency Injection and other Inversion of Control patterns as a way to improve the testability of your code. There's no denying that Dependency Injection makes code more testable, however, isn't it also a completing goal to Abstraction in general?
I feel conflicted! I wrote an example to illustrate this; it's not super-realistic and I wouldn't design it this way, but I needed a quick and simple example of a class structure with multiple dependencies. The first example is without Dependency Injection, and the second uses Injected Dependencies.
Non-DI Example
package com.stackoverflow.di;
public class EmployeeInventoryAnswerer()
{
/* In reality, at least the store name and product name would be
* passed in, but this example can't be 8 pages long or the point
* may be lost.
*/
public void myEntryPoint()
{
Store oaklandStore = new Store('Oakland, CA');
StoreInventoryManager inventoryManager = new StoreInventoryManager(oaklandStore);
Product fancyNewProduct = new Product('My Awesome Product');
if (inventoryManager.isProductInStock(fancyNewProduct))
{
System.out.println("Product is in stock.");
}
}
}
public class StoreInventoryManager
{
protected Store store;
protected InventoryCatalog catalog;
public StoreInventoryManager(Store store)
{
this.store = store;
this.catalog = new InventoryCatalog();
}
public void addProduct(Product product, int quantity)
{
this.catalog.addProduct(this.store, product, quantity);
}
public boolean isProductInStock(Product product)
{
return this.catalog.isInStock(this.store, this.product);
}
}
public class InventoryCatalog
{
protected Database db;
public InventoryCatalog()
{
this.db = new Database('productReadWrite');
}
public void addProduct(Store store, Product product, int initialQuantity)
{
this.db.query(
'INSERT INTO store_inventory SET store_id = %d, product_id = %d, quantity = %d'
).format(
store.id, product.id, initialQuantity
);
}
public boolean isInStock(Store store, Product product)
{
QueryResult qr;
qr = this.db.query(
'SELECT quantity FROM store_inventory WHERE store_id = %d AND product_id = %d'
).format(
store.id, product.id
);
if (qr.quantity.toInt() > 0)
{
return true;
}
return false;
}
}
Dependency-Injected Example
package com.stackoverflow.di;
public class EmployeeInventoryAnswerer()
{
public void myEntryPoint()
{
Database db = new Database('productReadWrite');
InventoryCatalog catalog = new InventoryCatalog(db);
Store oaklandStore = new Store('Oakland, CA');
StoreInventoryManager inventoryManager = new StoreInventoryManager(oaklandStore, catalog);
Product fancyNewProduct = new Product('My Awesome Product');
if (inventoryManager.isProductInStock(fancyNewProduct))
{
System.out.println("Product is in stock.");
}
}
}
public class StoreInventoryManager
{
protected Store store;
protected InventoryCatalog catalog;
public StoreInventoryManager(Store store, InventoryCatalog catalog)
{
this.store = store;
this.catalog = catalog;
}
public void addProduct(Product product, int quantity)
{
this.catalog.addProduct(this.store, product, quantity);
}
public boolean isProductInStock(Product product)
{
return this.catalog.isInStock(this.store, this.product);
}
}
public class InventoryCatalog
{
protected Database db;
public InventoryCatalog(Database db)
{
this.db = db;
}
public void addProduct(Store store, Product product, int initialQuantity)
{
this.db.query(
'INSERT INTO store_inventory SET store_id = %d, product_id = %d, quantity = %d'
).format(
store.id, product.id, initialQuantity
);
}
public boolean isInStock(Store store, Product product)
{
QueryResult qr;
qr = this.db.query(
'SELECT quantity FROM store_inventory WHERE store_id = %d AND product_id = %d'
).format(
store.id, product.id
);
if (qr.quantity.toInt() > 0)
{
return true;
}
return false;
}
}
(Please feel to make my example better if you have any ideas! It might not be the best example.)
In my example, I feel that Abstraction has been completely violated by EmployeeInventoryAnswerer having knowledge of underlying implementation details of StoreInventoryManager.
Shouldn't EmployeeInventoryAnswererhave the perspective of, "Okay, I'll just grab a StoreInventoryManager, give it the name of the product the customer is looking for, and what store I want to check, and it will tell me if the product is in stock."? Shouldn't it not know a single thing about Databases or InventoryCatalogs, as from its perspective, that's an implementation detail it need not concern itself with?
So, where's the balance between testable code with injected dependencies, and information-hiding as a principal of abstraction? Even if the middle-classes are merely passing-through dependencies, the constructor signature alone reveals irrelevant details, right?
More realistically, let's say this a long-running background application processing data from a DBMS; at what "layer" of the call-graph is it appropriate to create and pass around a database connector, while still making your code testable without a running DBMS?
I'm very interested in learning about both OOP theory and practicality here, as well as clarifying what seems to be a paradox between DI and Information Hiding/Abstraction.
The Dependency Inversion Principle and, more specifically, Dependency Injection tackle the problem of how make application code loosely coupled. This means that in many cases you want to prevent the classes in your application from depending on other concrete types, in case those dependent types contain volatile behavior. A volatile dependency is a dependency that, among other things, communicates with out-of-process resources, is non-deterministic, or needs to be replaceable. Tightly coupling to volatile dependencies hinders testability, but also limits the maintainability and flexibility of your application.
But no matter what you do, and no matter how many abstractions you introduce, somewhere in your application you need to take a dependency on a concrete type. So you can’t get rid of this coupling completely—but this shouldn't be a problem: An application that is 100% abstract is also 100% useless.
What this means is that you want to reduce the amount of coupling between classes and modules in your application, and the best way of doing that is to have a single place in the application that depends on all concrete types and will instantiate that for you. This is most beneficial because:
You will only have one place in the application that knows about the composition of object graphs, instead of having this knowledge scattered throughout the application
You will only have one place to change if you want to change implementations, or intercept/decorate instances to apply cross-cutting concerns.
This place where you wire everything up should be in your entry-point assembly. It should be the entry-point assembly, because this assembly already depends on all other assemblies anyway, making it already the most volatile part of your application.
According to the Stable-Dependencies Principle (2) dependencies should point in direction of stability, and since the part of the application where you compose your object graphs will be the most volatile part, nothing should depend on it. That’s why this place where you compose your object graphs should be in your entry point assembly.
This entry point in the application where you compose your object graphs is commonly referred to as the Composition Root.
If you feel that EmployeeInventoryAnswerer should not know anything about databases and InventoryCatalogs, it might be the case that the EmployeeInventoryAnswerer is mixing infrastructural logic (to build up the object graphs) and application logic. Iin other words, it might be violating the Single Responsibility Principle. In that case your EmployeeInventoryAnswerer should not be the entry point. Instead you should have a different entry point and the EmployeeInventoryAnswerer should only get a StoreInventoryManager injected. Your new entry point can than build up the object graph starting with EmployeeInventoryAnswerer and call its AnswerInventoryQuestion method (or whatever you decide to call it).
where's the balance between testable code with injected dependencies,
and information-hiding as a principal of abstraction?
The constructor is an implementation detail. Only the Composition Root knows about concrete types and it is, therefore, the only one calling those constructors. When a consuming class depends on abstractions as its incoming/injected dependencies (e.g. by specifying its constructor arguments as abstractions), the consumer knows nothing about the implementation and that makes it easier to prevent leaking implementation details onto the consumer. If the abstraction itself would leak implementation details, on the other hand, it would violate the Dependency Inversion Principle. And if the consumer would decide to cast the dependency back to the implementation, it would in turn violate the Liskov Substitition Principle. Both violations should be prevented.
But even if you would have a consumer that depends on a concrete component, that component can still do information-hiding—it doesn't have to expose its own dependencies (or other values) through public properties. And the fact that this component has a constructor that takes in the component's dependencies, does not make it violate information-hiding, because it is impossible to retrieve the component's dependencies through its constructor (you can only insert dependencies through the constructor; not receive them). And you can't change the component's dependencies, because that component itself will be injected into the consumer, and you can't call a constructor on an already created instance.
As I see it, when talking about a "balance," you are providing a false choice. Instead, it's a matter of applying the SOLID principles correctly, because without applying the SOLID principles, you'll be in a bad place (from a maintainability perspective) anyway—and application of the SOLID principles undoubtedly leads to Dependency Injection.
at what "layer" of the call-graph is it appropriate to create and pass around a database connector
At the very least, the entry point knows about the database connection, because it is only the entry point that should read from the configuration file. Reading from the configuration file should be done up front and in one single place. This allows the application to fail fast if it is misconfigured and prevents you from having reads to the config file scattered throughout your application.
But whether the entry point should be responsible of creating the database connection, that can depend on a lot of factors. I usually have some sort of ConnectionFactory abstraction for this, but YMMV.
UPDATE
I don't want to pass around a Context or an AppConfig to everything and end up passing dependencies classes don't need
Passing dependencies a class itself doesn't need is typically not the best solution, and might indicate that you are violating the Dependency Inversion Principle and applying the Control Freak anti-pattern. Here's an example of such problem:
public class Service : ServiceAbs
{
private IOtherService otherService;
public Service(IDep1 dep1, IDep2 dep2, IDep3 dep3) {
this.otherService = new OtherService(dep1, dep2, dep3);
}
}
Here you see a class Service that takes in 3 dependencies, but it doesn't use them at all. It only forwards them to the OtherService's constructor which it creates. When OtherService is not local to Service (i.e. lives in a different module or layer), it means that Service violates the Dependency Inversion Principle—Service is now tightly coupled with OtherService. Instead, this is how Service should look like:
public class Service : IService
{
private IOtherService otherService;
public Service(IOtherService otherService) {
this.otherService = otherService;
}
}
Here Service only takes in what it really needs and doesn't depend on any concrete types.
but I also don't want to pass the same 4 things to several different classes
If you have a group of dependencies that are often all injected together into a consumer, changes are that you are violating the Single Responsibility Principle: the consumer might do too much—know too much.
There are several solutions for this, depending on the problem at hand. One thing that comes to mind is refactoring to Facade Services.
It might also be the case that those injected dependencies are cross-cutting concerns. It's often much better to apply cross-cutting concerns transparently, instead of injecting it into dozens or hundreds of consumers (which is a violation of the Open/Closed principle). You can use the Decorator design pattern, Chain-of-Responsibility design pattern, or dynamic interception for this.